From 1a45b27d8b692853036ea3acafd50e873d8d0e85 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Wed, 15 Aug 2018 15:26:00 -0600 Subject: [PATCH 001/283] Move CharArrays to core lib (#32851) This change cleans up some methods in the CharArrays class from x-pack, which includes the unification of char[] to utf8 and utf8 to char[] conversions that intentionally do not use strings. There was previously an implementation in x-pack and in the reloading of secure settings. The method from the reloading of secure settings was adopted as it handled more scenarios related to the backing byte and char buffers that were used to perform the conversions. The cleaned up class is moved into libs/core to allow it to be used by requests that will be migrated to the high level rest client. Relates #32332 --- .../org/elasticsearch/common/CharArrays.java | 150 ++++++++++++++++++ .../elasticsearch/common/CharArraysTests.java | 72 +++++++++ .../NodesReloadSecureSettingsRequest.java | 66 +------- .../action/token/CreateTokenRequest.java | 2 +- .../action/user/ChangePasswordRequest.java | 2 +- .../security/action/user/PutUserRequest.java | 2 +- .../core/security/authc/support/BCrypt.java | 9 +- .../security/authc/support/CharArrays.java | 101 ------------ .../core/security/authc/support/Hasher.java | 1 + .../authc/support/UsernamePasswordToken.java | 14 +- .../xpack/core/ssl/PemUtils.java | 2 +- .../core/watcher/crypto/CryptoService.java | 2 +- .../ldap/ActiveDirectorySessionFactory.java | 2 +- .../authc/ldap/LdapSessionFactory.java | 2 +- .../ldap/LdapUserSearchSessionFactory.java | 2 +- .../authc/ldap/PoolingSessionFactory.java | 2 +- .../esnative/ESNativeMigrateToolTests.java | 2 +- .../example/realm/CustomRealm.java | 2 +- 18 files changed, 257 insertions(+), 178 deletions(-) create mode 100644 libs/core/src/main/java/org/elasticsearch/common/CharArrays.java create mode 100644 libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/CharArrays.java diff --git a/libs/core/src/main/java/org/elasticsearch/common/CharArrays.java b/libs/core/src/main/java/org/elasticsearch/common/CharArrays.java new file mode 100644 index 000000000000..907874ca5735 --- /dev/null +++ b/libs/core/src/main/java/org/elasticsearch/common/CharArrays.java @@ -0,0 +1,150 @@ +/* + * 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; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +/** + * Helper class similar to Arrays to handle conversions for Char arrays + */ +public final class CharArrays { + + private CharArrays() {} + + /** + * Decodes the provided byte[] to a UTF-8 char[]. This is done while avoiding + * conversions to String. The provided byte[] is not modified by this method, so + * the caller needs to take care of clearing the value if it is sensitive. + */ + public static char[] utf8BytesToChars(byte[] utf8Bytes) { + final ByteBuffer byteBuffer = ByteBuffer.wrap(utf8Bytes); + final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer); + final char[] chars; + if (charBuffer.hasArray()) { + // there is no guarantee that the char buffers backing array is the right size + // so we need to make a copy + chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit()); + Arrays.fill(charBuffer.array(), (char) 0); // clear sensitive data + } else { + final int length = charBuffer.limit() - charBuffer.position(); + chars = new char[length]; + charBuffer.get(chars); + // if the buffer is not read only we can reset and fill with 0's + if (charBuffer.isReadOnly() == false) { + charBuffer.clear(); // reset + for (int i = 0; i < charBuffer.limit(); i++) { + charBuffer.put((char) 0); + } + } + } + return chars; + } + + /** + * Encodes the provided char[] to a UTF-8 byte[]. This is done while avoiding + * conversions to String. The provided char[] is not modified by this method, so + * the caller needs to take care of clearing the value if it is sensitive. + */ + public static byte[] toUtf8Bytes(char[] chars) { + final CharBuffer charBuffer = CharBuffer.wrap(chars); + final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); + final byte[] bytes; + if (byteBuffer.hasArray()) { + // there is no guarantee that the byte buffers backing array is the right size + // so we need to make a copy + bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); + Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data + } else { + final int length = byteBuffer.limit() - byteBuffer.position(); + bytes = new byte[length]; + byteBuffer.get(bytes); + // if the buffer is not read only we can reset and fill with 0's + if (byteBuffer.isReadOnly() == false) { + byteBuffer.clear(); // reset + for (int i = 0; i < byteBuffer.limit(); i++) { + byteBuffer.put((byte) 0); + } + } + } + return bytes; + } + + /** + * Tests if a char[] contains a sequence of characters that match the prefix. This is like + * {@link String#startsWith(String)} but does not require conversion of the char[] to a string. + */ + public static boolean charsBeginsWith(String prefix, char[] chars) { + if (chars == null || prefix == null) { + return false; + } + + if (prefix.length() > chars.length) { + return false; + } + + for (int i = 0; i < prefix.length(); i++) { + if (chars[i] != prefix.charAt(i)) { + return false; + } + } + + return true; + } + + /** + * Constant time equality check of char arrays to avoid potential timing attacks. + */ + public static boolean constantTimeEquals(char[] a, char[] b) { + Objects.requireNonNull(a, "char arrays must not be null for constantTimeEquals"); + Objects.requireNonNull(b, "char arrays must not be null for constantTimeEquals"); + if (a.length != b.length) { + return false; + } + + int equals = 0; + for (int i = 0; i < a.length; i++) { + equals |= a[i] ^ b[i]; + } + + return equals == 0; + } + + /** + * Constant time equality check of strings to avoid potential timing attacks. + */ + public static boolean constantTimeEquals(String a, String b) { + Objects.requireNonNull(a, "strings must not be null for constantTimeEquals"); + Objects.requireNonNull(b, "strings must not be null for constantTimeEquals"); + if (a.length() != b.length()) { + return false; + } + + int equals = 0; + for (int i = 0; i < a.length(); i++) { + equals |= a.charAt(i) ^ b.charAt(i); + } + + return equals == 0; + } +} diff --git a/libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java b/libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java new file mode 100644 index 000000000000..64b1ecd1f8a2 --- /dev/null +++ b/libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java @@ -0,0 +1,72 @@ +/* + * 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; + +import org.elasticsearch.test.ESTestCase; + +import java.nio.charset.StandardCharsets; + +public class CharArraysTests extends ESTestCase { + + public void testCharsToBytes() { + final String originalValue = randomUnicodeOfCodepointLengthBetween(0, 32); + final byte[] expectedBytes = originalValue.getBytes(StandardCharsets.UTF_8); + final char[] valueChars = originalValue.toCharArray(); + + final byte[] convertedBytes = CharArrays.toUtf8Bytes(valueChars); + assertArrayEquals(expectedBytes, convertedBytes); + } + + public void testBytesToUtf8Chars() { + final String originalValue = randomUnicodeOfCodepointLengthBetween(0, 32); + final byte[] bytes = originalValue.getBytes(StandardCharsets.UTF_8); + final char[] expectedChars = originalValue.toCharArray(); + + final char[] convertedChars = CharArrays.utf8BytesToChars(bytes); + assertArrayEquals(expectedChars, convertedChars); + } + + public void testCharsBeginsWith() { + assertFalse(CharArrays.charsBeginsWith(randomAlphaOfLength(4), null)); + assertFalse(CharArrays.charsBeginsWith(null, null)); + assertFalse(CharArrays.charsBeginsWith(null, randomAlphaOfLength(4).toCharArray())); + assertFalse(CharArrays.charsBeginsWith(randomAlphaOfLength(2), randomAlphaOfLengthBetween(3, 8).toCharArray())); + + final String prefix = randomAlphaOfLengthBetween(2, 4); + assertTrue(CharArrays.charsBeginsWith(prefix, prefix.toCharArray())); + final char[] prefixedValue = prefix.concat(randomAlphaOfLengthBetween(1, 12)).toCharArray(); + assertTrue(CharArrays.charsBeginsWith(prefix, prefixedValue)); + + final String modifiedPrefix = randomBoolean() ? prefix.substring(1) : prefix.substring(0, prefix.length() - 1); + final char[] nonMatchingValue = modifiedPrefix.concat(randomAlphaOfLengthBetween(0, 12)).toCharArray(); + assertFalse(CharArrays.charsBeginsWith(prefix, nonMatchingValue)); + assertTrue(CharArrays.charsBeginsWith(modifiedPrefix, nonMatchingValue)); + } + + public void testConstantTimeEquals() { + final String value = randomAlphaOfLengthBetween(0, 32); + assertTrue(CharArrays.constantTimeEquals(value, value)); + assertTrue(CharArrays.constantTimeEquals(value.toCharArray(), value.toCharArray())); + + final String other = randomAlphaOfLengthBetween(1, 32); + assertFalse(CharArrays.constantTimeEquals(value, other)); + assertFalse(CharArrays.constantTimeEquals(value.toCharArray(), other.toCharArray())); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java index 50df7b1bb26e..5320470d366e 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java @@ -22,14 +22,12 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.nodes.BaseNodesRequest; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.SecureString; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -83,7 +81,7 @@ public void readFrom(StreamInput in) throws IOException { super.readFrom(in); final byte[] passwordBytes = in.readByteArray(); try { - this.secureSettingsPassword = new SecureString(utf8BytesToChars(passwordBytes)); + this.secureSettingsPassword = new SecureString(CharArrays.utf8BytesToChars(passwordBytes)); } finally { Arrays.fill(passwordBytes, (byte) 0); } @@ -92,69 +90,11 @@ public void readFrom(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - final byte[] passwordBytes = charsToUtf8Bytes(this.secureSettingsPassword.getChars()); + final byte[] passwordBytes = CharArrays.toUtf8Bytes(this.secureSettingsPassword.getChars()); try { out.writeByteArray(passwordBytes); } finally { Arrays.fill(passwordBytes, (byte) 0); } } - - /** - * Encodes the provided char[] to a UTF-8 byte[]. This is done while avoiding - * conversions to String. The provided char[] is not modified by this method, so - * the caller needs to take care of clearing the value if it is sensitive. - */ - private static byte[] charsToUtf8Bytes(char[] chars) { - final CharBuffer charBuffer = CharBuffer.wrap(chars); - final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); - final byte[] bytes; - if (byteBuffer.hasArray()) { - // there is no guarantee that the byte buffers backing array is the right size - // so we need to make a copy - bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); - Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - } else { - final int length = byteBuffer.limit() - byteBuffer.position(); - bytes = new byte[length]; - byteBuffer.get(bytes); - // if the buffer is not read only we can reset and fill with 0's - if (byteBuffer.isReadOnly() == false) { - byteBuffer.clear(); // reset - for (int i = 0; i < byteBuffer.limit(); i++) { - byteBuffer.put((byte) 0); - } - } - } - return bytes; - } - - /** - * Decodes the provided byte[] to a UTF-8 char[]. This is done while avoiding - * conversions to String. The provided byte[] is not modified by this method, so - * the caller needs to take care of clearing the value if it is sensitive. - */ - public static char[] utf8BytesToChars(byte[] utf8Bytes) { - final ByteBuffer byteBuffer = ByteBuffer.wrap(utf8Bytes); - final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer); - final char[] chars; - if (charBuffer.hasArray()) { - // there is no guarantee that the char buffers backing array is the right size - // so we need to make a copy - chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit()); - Arrays.fill(charBuffer.array(), (char) 0); // clear sensitive data - } else { - final int length = charBuffer.limit() - charBuffer.position(); - chars = new char[length]; - charBuffer.get(chars); - // if the buffer is not read only we can reset and fill with 0's - if (charBuffer.isReadOnly() == false) { - charBuffer.clear(); // reset - for (int i = 0; i < charBuffer.limit(); i++) { - charBuffer.put((char) 0); - } - } - } - return chars; - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java index 5956e1a66134..fdb46711c0c5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java @@ -15,7 +15,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import java.io.IOException; import java.util.Arrays; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequest.java index f84b133d984b..b78b81c06008 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequest.java @@ -12,7 +12,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import java.io.IOException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java index f37072b9cf0f..e704259396a3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java @@ -8,12 +8,12 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; import java.io.IOException; import java.util.Map; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/BCrypt.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/BCrypt.java index ceb93dc4c853..a93476bbdc8d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/BCrypt.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/BCrypt.java @@ -14,6 +14,7 @@ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +import org.elasticsearch.common.CharArrays; import org.elasticsearch.common.settings.SecureString; import java.security.SecureRandom; @@ -54,7 +55,7 @@ * String stronger_salt = BCrypt.gensalt(12)
* *

- * The amount of work increases exponentially (2**log_rounds), so + * The amount of work increases exponentially (2**log_rounds), so * each increment is twice as much work. The default log_rounds is * 10, and the valid range is 4 to 30. * @@ -689,7 +690,11 @@ public static String hashpw(SecureString password, String salt) { // the next lines are the SecureString replacement for the above commented-out section if (minor >= 'a') { - try (SecureString secureString = new SecureString(CharArrays.concat(password.getChars(), "\000".toCharArray()))) { + final char[] suffix = "\000".toCharArray(); + final char[] result = new char[password.length() + suffix.length]; + System.arraycopy(password.getChars(), 0, result, 0, password.length()); + System.arraycopy(suffix, 0, result, password.length(), suffix.length); + try (SecureString secureString = new SecureString(result)) { passwordb = CharArrays.toUtf8Bytes(secureString.getChars()); } } else { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/CharArrays.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/CharArrays.java deleted file mode 100644 index 26df90c31a2d..000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/CharArrays.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.core.security.authc.support; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -/** - * Helper class similar to Arrays to handle conversions for Char arrays - */ -public class CharArrays { - - public static char[] utf8BytesToChars(byte[] utf8Bytes) { - ByteBuffer byteBuffer = ByteBuffer.wrap(utf8Bytes); - CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer); - char[] chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit()); - byteBuffer.clear(); - charBuffer.clear(); - return chars; - } - - /** - * Like String.indexOf for for an array of chars - */ - static int indexOf(char[] array, char ch) { - for (int i = 0; (i < array.length); i++) { - if (array[i] == ch) { - return i; - } - } - return -1; - } - - /** - * Converts the provided char[] to a UTF-8 byte[]. The provided char[] is not modified by this - * method, so the caller needs to take care of clearing the value if it is sensitive. - */ - public static byte[] toUtf8Bytes(char[] chars) { - CharBuffer charBuffer = CharBuffer.wrap(chars); - ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); - byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); - Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - return bytes; - } - - public static boolean charsBeginsWith(String prefix, char[] chars) { - if (chars == null || prefix == null) { - return false; - } - - if (prefix.length() > chars.length) { - return false; - } - - for (int i = 0; i < prefix.length(); i++) { - if (chars[i] != prefix.charAt(i)) { - return false; - } - } - - return true; - } - - public static boolean constantTimeEquals(char[] a, char[] b) { - if (a.length != b.length) { - return false; - } - - int equals = 0; - for (int i = 0; i < a.length; i++) { - equals |= a[i] ^ b[i]; - } - - return equals == 0; - } - - public static boolean constantTimeEquals(String a, String b) { - if (a.length() != b.length()) { - return false; - } - - int equals = 0; - for (int i = 0; i < a.length(); i++) { - equals |= a.charAt(i) ^ b.charAt(i); - } - - return equals == 0; - } - - public static char[] concat(char[] a, char[] b) { - final char[] result = new char[a.length + b.length]; - System.arraycopy(a, 0, result, 0, a.length); - System.arraycopy(b, 0, result, a.length, b.length); - return result; - } -} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java index d12547bd9064..492622b2c519 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.security.authc.support; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.common.hash.MessageDigests; import org.elasticsearch.common.settings.SecureString; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java index d8e58c29d237..134930360088 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UsernamePasswordToken.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.security.authc.support; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -107,7 +108,7 @@ private static UsernamePasswordToken extractToken(String headerValue) { throw authenticationError("invalid basic authentication header encoding", e); } - int i = CharArrays.indexOf(userpasswd, ':'); + int i = indexOfColon(userpasswd); if (i < 0) { throw authenticationError("invalid basic authentication header value"); } @@ -121,4 +122,15 @@ public static void putTokenHeader(ThreadContext context, UsernamePasswordToken t context.putHeader(BASIC_AUTH_HEADER, basicAuthHeaderValue(token.username, token.password)); } + /** + * Like String.indexOf for for an array of chars + */ + private static int indexOfColon(char[] array) { + for (int i = 0; (i < array.length); i++) { + if (array[i] == ':') { + return i; + } + } + return -1; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java index d959c017e0a3..a3814a76a3e6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java @@ -7,7 +7,7 @@ package org.elasticsearch.xpack.core.ssl; import org.elasticsearch.common.hash.MessageDigests; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import java.io.BufferedReader; import java.io.IOException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/crypto/CryptoService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/crypto/CryptoService.java index b1f3a32769ec..a25e79ffdf66 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/crypto/CryptoService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/crypto/CryptoService.java @@ -13,7 +13,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.core.watcher.WatcherField; import org.elasticsearch.xpack.core.security.SecurityField; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java index d175e1b22931..8107d7488188 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java @@ -32,7 +32,7 @@ import org.elasticsearch.xpack.core.security.authc.ldap.ActiveDirectorySessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapSearchScope; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactory.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactory.java index 36d14aa67c0d..70b2f0015cf7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactory.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapSessionFactory.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.SearchGroupsResolverSettings; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java index 2ec87888d8c1..a3541ec2759b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java @@ -24,7 +24,7 @@ import org.elasticsearch.xpack.core.security.authc.ldap.LdapUserSearchSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.SearchGroupsResolverSettings; import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapSearchScope; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession.GroupsResolver; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java index 367bd525036e..986fa1900e7c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java @@ -25,7 +25,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.PoolingSessionFactorySettings; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeMigrateToolTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeMigrateToolTests.java index 212ee7ea499e..6d75cf093714 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeMigrateToolTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ESNativeMigrateToolTests.java @@ -13,7 +13,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.test.NativeRealmIntegTestCase; import org.elasticsearch.test.SecuritySettingsSource; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.security.client.SecurityClient; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.BeforeClass; diff --git a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java index af3fb160e133..c6502c05d252 100644 --- a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java +++ b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java @@ -12,7 +12,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; -import org.elasticsearch.xpack.core.security.authc.support.CharArrays; +import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.protocol.xpack.security.User; From 93ada98eb54d99b9d98ed51778bd6422d18fd392 Mon Sep 17 00:00:00 2001 From: debadair Date: Wed, 15 Aug 2018 15:09:41 -0700 Subject: [PATCH 002/283] [DOCS] Fixing cross doc link to Stack Overview security topic. --- x-pack/docs/en/security/configuring-es.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/docs/en/security/configuring-es.asciidoc b/x-pack/docs/en/security/configuring-es.asciidoc index 5e8f1adbc7aa..53f36afc7348 100644 --- a/x-pack/docs/en/security/configuring-es.asciidoc +++ b/x-pack/docs/en/security/configuring-es.asciidoc @@ -9,7 +9,7 @@ password-protect your data as well as implement more advanced security measures such as encrypting communications, role-based access control, IP filtering, and auditing. For more information, see -{xpack-ref}/xpack-security.html[Securing the Elastic Stack]. +{xpack-ref}/elasticsearch-security.html[Securing the Elastic Stack]. To use {security} in {es}: From 70d80a3d093e327a05eeb7b8c49fee7c94952a8f Mon Sep 17 00:00:00 2001 From: markharwood Date: Thu, 16 Aug 2018 10:21:37 +0100 Subject: [PATCH 003/283] Docs enhancement: added reference to cluster-level setting `search.default_allow_partial_results` (#32810) Closes #32809 --- docs/reference/search/request-body.asciidoc | 3 ++- docs/reference/search/uri-request.asciidoc | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/reference/search/request-body.asciidoc b/docs/reference/search/request-body.asciidoc index 2a51d705d83e..e7c9b593af37 100644 --- a/docs/reference/search/request-body.asciidoc +++ b/docs/reference/search/request-body.asciidoc @@ -90,7 +90,8 @@ And here is a sample response: Set to `false` to return an overall failure if the request would produce partial results. Defaults to true, which will allow partial results in the case of timeouts - or partial failures. + or partial failures. This default can be controlled using the cluster-level setting + `search.default_allow_partial_results`. `terminate_after`:: diff --git a/docs/reference/search/uri-request.asciidoc b/docs/reference/search/uri-request.asciidoc index a90f32bb3cd3..279bc0c0384c 100644 --- a/docs/reference/search/uri-request.asciidoc +++ b/docs/reference/search/uri-request.asciidoc @@ -125,5 +125,6 @@ more details on the different types of search that can be performed. |`allow_partial_search_results` |Set to `false` to return an overall failure if the request would produce partial results. Defaults to true, which will allow partial results in the case of timeouts -or partial failures.. +or partial failures. This default can be controlled using the cluster-level setting +`search.default_allow_partial_results`. |======================================================================= From e6bfba1d799cbc41fce5ea88e2252f78fd9cac65 Mon Sep 17 00:00:00 2001 From: datosh Date: Thu, 16 Aug 2018 11:34:41 +0200 Subject: [PATCH 004/283] [DOCS] Clarify sentence in network-host.asciidoc (#32429) --- docs/reference/setup/important-settings/network-host.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/setup/important-settings/network-host.asciidoc b/docs/reference/setup/important-settings/network-host.asciidoc index 7e29e73123d8..1788bfebc66b 100644 --- a/docs/reference/setup/important-settings/network-host.asciidoc +++ b/docs/reference/setup/important-settings/network-host.asciidoc @@ -9,7 +9,7 @@ location on a single node. This can be useful for testing Elasticsearch's ability to form clusters, but it is not a configuration recommended for production. -In order to communicate and to form a cluster with nodes on other servers, your +In order to form a cluster with nodes on other servers, your node will need to bind to a non-loopback address. While there are many <>, usually all you need to configure is `network.host`: From 996ed73d735b1a2943cb5c7af6ba315a225b64c0 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Thu, 16 Aug 2018 11:44:20 +0200 Subject: [PATCH 005/283] Test: Fix unpredictive merges in DocumentSubsetReaderTests The merge policy that was used could lead to unpredictive merges due to the randomization of `setDeletesPctAllowed`. Closes #32457 --- .../authz/accesscontrol/DocumentSubsetReaderTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetReaderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetReaderTests.java index 38857e2170de..dca2f37f3f22 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetReaderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/DocumentSubsetReaderTests.java @@ -80,9 +80,8 @@ public void cleanDirectory() throws Exception { bitsetFilterCache.close(); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32457") public void testSearch() throws Exception { - IndexWriter iw = new IndexWriter(directory, newIndexWriterConfig()); + IndexWriter iw = new IndexWriter(directory, newIndexWriterConfig().setMergePolicy(newLogMergePolicy(random()))); Document document = new Document(); document.add(new StringField("field", "value1", Field.Store.NO)); From 7f6802cb51eda172c64fb223bf5728719f586b11 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Thu, 16 Aug 2018 10:48:56 +0100 Subject: [PATCH 006/283] [ML] Choose seconds to fix intermittent DatafeeedConfigTest failure --- .../xpack/core/ml/datafeed/DatafeedConfigTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java index ffc13655d229..3030449abd1b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java @@ -100,7 +100,7 @@ public static DatafeedConfig createRandomizedDatafeedConfig(String jobId, long b if (aggHistogramInterval == null) { builder.setFrequency(TimeValue.timeValueSeconds(randomIntBetween(1, 1_000_000))); } else { - builder.setFrequency(TimeValue.timeValueMillis(randomIntBetween(1, 5) * aggHistogramInterval)); + builder.setFrequency(TimeValue.timeValueSeconds(randomIntBetween(1, 5) * aggHistogramInterval)); } } if (randomBoolean()) { From 039babddf532c918ae3f0ac5360e85e7ba5b3052 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 16 Aug 2018 11:53:01 +0200 Subject: [PATCH 007/283] CharArraysTests: Fix test bug. --- .../test/java/org/elasticsearch/common/CharArraysTests.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java b/libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java index 64b1ecd1f8a2..9283283ab086 100644 --- a/libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java +++ b/libs/core/src/test/java/org/elasticsearch/common/CharArraysTests.java @@ -55,7 +55,10 @@ public void testCharsBeginsWith() { assertTrue(CharArrays.charsBeginsWith(prefix, prefixedValue)); final String modifiedPrefix = randomBoolean() ? prefix.substring(1) : prefix.substring(0, prefix.length() - 1); - final char[] nonMatchingValue = modifiedPrefix.concat(randomAlphaOfLengthBetween(0, 12)).toCharArray(); + char[] nonMatchingValue; + do { + nonMatchingValue = modifiedPrefix.concat(randomAlphaOfLengthBetween(0, 12)).toCharArray(); + } while (new String(nonMatchingValue).startsWith(prefix)); assertFalse(CharArrays.charsBeginsWith(prefix, nonMatchingValue)); assertTrue(CharArrays.charsBeginsWith(modifiedPrefix, nonMatchingValue)); } From d80457ee2a0a5ce8973b48a06b5d60ee2c5e64c1 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 16 Aug 2018 11:07:20 +0100 Subject: [PATCH 008/283] Mutes test in DuelScrollIT Due to https://github.com/elastic/elasticsearch/issues/32682 --- .../test/java/org/elasticsearch/search/scroll/DuelScrollIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java b/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java index 31fcfa7155cc..1ddd11e5d0f7 100644 --- a/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java +++ b/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java @@ -21,6 +21,7 @@ import com.carrotsearch.hppc.IntHashSet; import com.carrotsearch.randomizedtesting.generators.RandomPicks; + import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; @@ -256,6 +257,7 @@ private void testDuelIndexOrder(SearchType searchType, boolean trackScores, int } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32682") public void testDuelIndexOrderQueryThenFetch() throws Exception { final SearchType searchType = RandomPicks.randomFrom(random(), Arrays.asList(SearchType.QUERY_THEN_FETCH, SearchType.DFS_QUERY_THEN_FETCH)); From e35be019014aafba9c34484e5711a04ce482ffd1 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 16 Aug 2018 12:27:21 +0200 Subject: [PATCH 009/283] AwaitFix AckIT. Relates #32767 --- server/src/test/java/org/elasticsearch/cluster/ack/AckIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/cluster/ack/AckIT.java b/server/src/test/java/org/elasticsearch/cluster/ack/AckIT.java index 2cd8a2c27c71..df97854cc35b 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ack/AckIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/ack/AckIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.cluster.ack; +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteResponse; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; @@ -50,6 +51,7 @@ import static org.hamcrest.Matchers.notNullValue; @ClusterScope(minNumDataNodes = 2) +@AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/32767") public class AckIT extends ESIntegTestCase { @Override From f8c7414ee861b441f81716e13c3190f4d2d97c54 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 16 Aug 2018 07:24:05 -0400 Subject: [PATCH 010/283] Remove passphrase support from reload settings API (#32889) We do not support passphrases on the secure settings storage (the keystore). Yet, we added support for this in the API layer. This commit removes this support so that we are not limited in our future options, or have to make a breaking change. --- .../NodesReloadSecureSettingsRequest.java | 68 +------------ ...desReloadSecureSettingsRequestBuilder.java | 49 ---------- ...nsportNodesReloadSecureSettingsAction.java | 6 +- .../RestReloadSecureSettingsAction.java | 11 +-- .../action/admin/ReloadSecureSettingsIT.java | 98 ++----------------- 5 files changed, 16 insertions(+), 216 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java index 5320470d366e..fb3e6ac71adf 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java @@ -19,82 +19,22 @@ package org.elasticsearch.action.admin.cluster.node.reload; - -import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.nodes.BaseNodesRequest; -import org.elasticsearch.common.CharArrays; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.settings.SecureString; - -import java.io.IOException; -import java.util.Arrays; - -import static org.elasticsearch.action.ValidateActions.addValidationError; /** - * Request for a reload secure settings action + * Request for a reload secure settings action. */ public class NodesReloadSecureSettingsRequest extends BaseNodesRequest { - /** - * The password which is broadcasted to all nodes, but is never stored on - * persistent storage. The password is used to reread and decrypt the contents - * of the node's keystore (backing the implementation of - * {@code SecureSettings}). - */ - private SecureString secureSettingsPassword; - public NodesReloadSecureSettingsRequest() { } /** - * Reload secure settings only on certain nodes, based on the nodes ids - * specified. If none are passed, secure settings will be reloaded on all the - * nodes. + * Reload secure settings only on certain nodes, based on the nodes IDs specified. If none are passed, secure settings will be reloaded + * on all the nodes. */ - public NodesReloadSecureSettingsRequest(String... nodesIds) { + public NodesReloadSecureSettingsRequest(final String... nodesIds) { super(nodesIds); } - @Override - public ActionRequestValidationException validate() { - ActionRequestValidationException validationException = null; - if (secureSettingsPassword == null) { - validationException = addValidationError("secure settings password cannot be null (use empty string instead)", - validationException); - } - return validationException; - } - - public SecureString secureSettingsPassword() { - return secureSettingsPassword; - } - - public NodesReloadSecureSettingsRequest secureStorePassword(SecureString secureStorePassword) { - this.secureSettingsPassword = secureStorePassword; - return this; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - final byte[] passwordBytes = in.readByteArray(); - try { - this.secureSettingsPassword = new SecureString(CharArrays.utf8BytesToChars(passwordBytes)); - } finally { - Arrays.fill(passwordBytes, (byte) 0); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - final byte[] passwordBytes = CharArrays.toUtf8Bytes(this.secureSettingsPassword.getChars()); - try { - out.writeByteArray(passwordBytes); - } finally { - Arrays.fill(passwordBytes, (byte) 0); - } - } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java index b5f2f73e56f5..c8250455e6ba 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequestBuilder.java @@ -19,19 +19,8 @@ package org.elasticsearch.action.admin.cluster.node.reload; -import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.support.nodes.NodesOperationRequestBuilder; import org.elasticsearch.client.ElasticsearchClient; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; /** * Builder for the reload secure settings nodes request @@ -39,46 +28,8 @@ public class NodesReloadSecureSettingsRequestBuilder extends NodesOperationRequestBuilder { - public static final String SECURE_SETTINGS_PASSWORD_FIELD_NAME = "secure_settings_password"; - public NodesReloadSecureSettingsRequestBuilder(ElasticsearchClient client, NodesReloadSecureSettingsAction action) { super(client, action, new NodesReloadSecureSettingsRequest()); } - public NodesReloadSecureSettingsRequestBuilder setSecureStorePassword(SecureString secureStorePassword) { - request.secureStorePassword(secureStorePassword); - return this; - } - - public NodesReloadSecureSettingsRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException { - Objects.requireNonNull(xContentType); - // EMPTY is ok here because we never call namedObject - try (InputStream stream = source.streamInput(); - XContentParser parser = xContentType.xContent().createParser(NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, stream)) { - XContentParser.Token token; - token = parser.nextToken(); - if (token != XContentParser.Token.START_OBJECT) { - throw new ElasticsearchParseException("expected an object, but found token [{}]", token); - } - token = parser.nextToken(); - if (token != XContentParser.Token.FIELD_NAME || false == SECURE_SETTINGS_PASSWORD_FIELD_NAME.equals(parser.currentName())) { - throw new ElasticsearchParseException("expected a field named [{}], but found [{}]", SECURE_SETTINGS_PASSWORD_FIELD_NAME, - token); - } - token = parser.nextToken(); - if (token != XContentParser.Token.VALUE_STRING) { - throw new ElasticsearchParseException("expected field [{}] to be of type string, but found [{}] instead", - SECURE_SETTINGS_PASSWORD_FIELD_NAME, token); - } - final String password = parser.text(); - setSecureStorePassword(new SecureString(password.toCharArray())); - token = parser.nextToken(); - if (token != XContentParser.Token.END_OBJECT) { - throw new ElasticsearchParseException("expected end of object, but found token [{}]", token); - } - } - return this; - } - } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java index 0f44170fa603..b8a36bac68d6 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java @@ -31,7 +31,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.KeyStoreWrapper; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.PluginsService; @@ -82,16 +81,13 @@ protected NodesReloadSecureSettingsResponse.NodeResponse newNodeResponse() { @Override protected NodesReloadSecureSettingsResponse.NodeResponse nodeOperation(NodeRequest nodeReloadRequest) { - final NodesReloadSecureSettingsRequest request = nodeReloadRequest.request; - final SecureString secureSettingsPassword = request.secureSettingsPassword(); try (KeyStoreWrapper keystore = KeyStoreWrapper.load(environment.configFile())) { // reread keystore from config file if (keystore == null) { return new NodesReloadSecureSettingsResponse.NodeResponse(clusterService.localNode(), new IllegalStateException("Keystore is missing")); } - // decrypt the keystore using the password from the request - keystore.decrypt(secureSettingsPassword.getChars()); + keystore.decrypt(new char[0]); // add the keystore to the original node settings object final Settings settingsWithKeystore = Settings.builder() .put(environment.settings(), false) diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java index 0697871ea5d1..2251615d678f 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestReloadSecureSettingsAction.java @@ -59,7 +59,6 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client .cluster() .prepareReloadSecureSettings() .setTimeout(request.param("timeout")) - .source(request.requiredContent(), request.getXContentType()) .setNodesIds(nodesIds); final NodesReloadSecureSettingsRequest nodesRequest = nodesRequestBuilder.request(); return channel -> nodesRequestBuilder @@ -68,12 +67,12 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client public RestResponse buildResponse(NodesReloadSecureSettingsResponse response, XContentBuilder builder) throws Exception { builder.startObject(); - RestActions.buildNodesHeader(builder, channel.request(), response); - builder.field("cluster_name", response.getClusterName().value()); - response.toXContent(builder, channel.request()); + { + RestActions.buildNodesHeader(builder, channel.request(), response); + builder.field("cluster_name", response.getClusterName().value()); + response.toXContent(builder, channel.request()); + } builder.endObject(); - // clear password for the original request - nodesRequest.secureSettingsPassword().close(); return new BytesRestResponse(RestStatus.OK, builder); } }); diff --git a/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java b/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java index 795275824054..3f9e258ffec1 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java +++ b/server/src/test/java/org/elasticsearch/action/admin/ReloadSecureSettingsIT.java @@ -20,11 +20,9 @@ package org.elasticsearch.action.admin; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsResponse; import org.elasticsearch.common.settings.KeyStoreWrapper; import org.elasticsearch.common.settings.SecureSettings; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.Plugin; @@ -44,11 +42,11 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.containsString; public class ReloadSecureSettingsIT extends ESIntegTestCase { @@ -62,7 +60,7 @@ public void testMissingKeystoreFile() throws Exception { Files.deleteIfExists(KeyStoreWrapper.keystorePath(environment.configFile())); final int initialReloadCount = mockReloadablePlugin.getReloadCount(); final CountDownLatch latch = new CountDownLatch(1); - client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(new SecureString(new char[0])).execute( + client().admin().cluster().prepareReloadSecureSettings().execute( new ActionListener() { @Override public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) { @@ -96,44 +94,6 @@ public void onFailure(Exception e) { assertThat(mockReloadablePlugin.getReloadCount(), equalTo(initialReloadCount)); } - public void testNullKeystorePassword() throws Exception { - final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class); - final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class) - .stream().findFirst().get(); - final AtomicReference reloadSettingsError = new AtomicReference<>(); - final int initialReloadCount = mockReloadablePlugin.getReloadCount(); - final CountDownLatch latch = new CountDownLatch(1); - client().admin().cluster().prepareReloadSecureSettings().execute( - new ActionListener() { - @Override - public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) { - try { - reloadSettingsError.set(new AssertionError("Null keystore password should fail")); - } finally { - latch.countDown(); - } - } - - @Override - public void onFailure(Exception e) { - try { - assertThat(e, instanceOf(ActionRequestValidationException.class)); - assertThat(e.getMessage(), containsString("secure settings password cannot be null")); - } catch (final AssertionError ae) { - reloadSettingsError.set(ae); - } finally { - latch.countDown(); - } - } - }); - latch.await(); - if (reloadSettingsError.get() != null) { - throw reloadSettingsError.get(); - } - // in the null password case no reload should be triggered - assertThat(mockReloadablePlugin.getReloadCount(), equalTo(initialReloadCount)); - } - public void testInvalidKeystoreFile() throws Exception { final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class); final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class) @@ -149,7 +109,7 @@ public void testInvalidKeystoreFile() throws Exception { Files.copy(keystore, KeyStoreWrapper.keystorePath(environment.configFile()), StandardCopyOption.REPLACE_EXISTING); } final CountDownLatch latch = new CountDownLatch(1); - client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(new SecureString(new char[0])).execute( + client().admin().cluster().prepareReloadSecureSettings().execute( new ActionListener() { @Override public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) { @@ -181,52 +141,6 @@ public void onFailure(Exception e) { assertThat(mockReloadablePlugin.getReloadCount(), equalTo(initialReloadCount)); } - public void testWrongKeystorePassword() throws Exception { - final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class); - final MockReloadablePlugin mockReloadablePlugin = pluginsService.filterPlugins(MockReloadablePlugin.class) - .stream().findFirst().get(); - final Environment environment = internalCluster().getInstance(Environment.class); - final AtomicReference reloadSettingsError = new AtomicReference<>(); - final int initialReloadCount = mockReloadablePlugin.getReloadCount(); - // "some" keystore should be present in this case - writeEmptyKeystore(environment, new char[0]); - final CountDownLatch latch = new CountDownLatch(1); - client().admin() - .cluster() - .prepareReloadSecureSettings() - .setSecureStorePassword(new SecureString(new char[] { 'W', 'r', 'o', 'n', 'g' })) - .execute(new ActionListener() { - @Override - public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) { - try { - assertThat(nodesReloadResponse, notNullValue()); - final Map nodesMap = nodesReloadResponse.getNodesMap(); - assertThat(nodesMap.size(), equalTo(cluster().size())); - for (final NodesReloadSecureSettingsResponse.NodeResponse nodeResponse : nodesReloadResponse.getNodes()) { - assertThat(nodeResponse.reloadException(), notNullValue()); - assertThat(nodeResponse.reloadException(), instanceOf(SecurityException.class)); - } - } catch (final AssertionError e) { - reloadSettingsError.set(e); - } finally { - latch.countDown(); - } - } - - @Override - public void onFailure(Exception e) { - reloadSettingsError.set(new AssertionError("Nodes request failed", e)); - latch.countDown(); - } - }); - latch.await(); - if (reloadSettingsError.get() != null) { - throw reloadSettingsError.get(); - } - // in the wrong password case no reload should be triggered - assertThat(mockReloadablePlugin.getReloadCount(), equalTo(initialReloadCount)); - } - public void testMisbehavingPlugin() throws Exception { final Environment environment = internalCluster().getInstance(Environment.class); final PluginsService pluginsService = internalCluster().getInstance(PluginsService.class); @@ -247,7 +161,7 @@ public void testMisbehavingPlugin() throws Exception { .get(Settings.builder().put(environment.settings()).setSecureSettings(secureSettings).build()) .toString(); final CountDownLatch latch = new CountDownLatch(1); - client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(new SecureString(new char[0])).execute( + client().admin().cluster().prepareReloadSecureSettings().execute( new ActionListener() { @Override public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) { @@ -314,7 +228,7 @@ protected Collection> nodePlugins() { private void successfulReloadCall() throws InterruptedException { final AtomicReference reloadSettingsError = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); - client().admin().cluster().prepareReloadSecureSettings().setSecureStorePassword(new SecureString(new char[0])).execute( + client().admin().cluster().prepareReloadSecureSettings().execute( new ActionListener() { @Override public void onResponse(NodesReloadSecureSettingsResponse nodesReloadResponse) { From b87f3062b77cab7888e9037b4996f2c26db12816 Mon Sep 17 00:00:00 2001 From: Hazem Khaled Date: Thu, 16 Aug 2018 14:54:04 +0300 Subject: [PATCH 011/283] [DOCS] Update WordPress plugins links (#32194) --- docs/plugins/integrations.asciidoc | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/plugins/integrations.asciidoc b/docs/plugins/integrations.asciidoc index 90f2c685fdae..8bffe5193ed7 100644 --- a/docs/plugins/integrations.asciidoc +++ b/docs/plugins/integrations.asciidoc @@ -17,14 +17,11 @@ Integrations are not plugins, but are external tools or modules that make it eas * https://drupal.org/project/elasticsearch_connector[Drupal]: Drupal Elasticsearch integration. -* https://wordpress.org/plugins/wpsolr-search-engine/[WPSOLR]: - Elasticsearch (and Apache Solr) WordPress Plugin - -* http://searchbox-io.github.com/wp-elasticsearch/[Wp-Elasticsearch]: +* https://wordpress.org/plugins/elasticpress/[ElasticPress]: Elasticsearch WordPress Plugin -* https://github.com/wallmanderco/elasticsearch-indexer[Elasticsearch Indexer]: - Elasticsearch WordPress Plugin +* https://wordpress.org/plugins/wpsolr-search-engine/[WPSOLR]: + Elasticsearch (and Apache Solr) WordPress Plugin * https://doc.tiki.org/Elasticsearch[Tiki Wiki CMS Groupware]: Tiki has native support for Elasticsearch. This provides faster & better From aedc2c1c498688970afcebe08726480e611f117b Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 16 Aug 2018 07:18:43 -0500 Subject: [PATCH 012/283] HLRC: adding machine learning delete job (#32820) * HLRC: adding machine learning delete job * Fixing whitespace * Moving docs and tests around * Unifying ml asciidoc file naming convention --- .../client/MachineLearningClient.java | 40 ++++++++++ .../client/RequestConverters.java | 16 ++++ .../client/MachineLearningIT.java | 15 ++++ .../client/RequestConvertersTests.java | 15 ++++ .../MlClientDocumentationIT.java | 56 +++++++++++++- .../high-level/ml/delete-job.asciidoc | 49 ++++++++++++ .../high-level/supported-apis.asciidoc | 2 + .../protocol/xpack/ml/DeleteJobRequest.java | 75 +++++++++++++++++++ .../protocol/xpack/ml/DeleteJobResponse.java | 60 +++++++++++++++ .../xpack/ml/DeleteJobRequestTests.java | 45 +++++++++++ .../xpack/ml/DeleteJobResponseTests.java | 42 +++++++++++ 11 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 docs/java-rest/high-level/ml/delete-job.asciidoc create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequestTests.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponseTests.java 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 a3e5ba72b773..2e7914e64abd 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.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -80,6 +82,44 @@ public void putJobAsync(PutJobRequest request, RequestOptions options, ActionLis Collections.emptySet()); } + /** + * Deletes the given Machine Learning Job + *

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

+ * @param request the request to delete the job + * @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 DeleteJobResponse deleteJob(DeleteJobRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + RequestConverters::deleteMachineLearningJob, + options, + DeleteJobResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Deletes the given Machine Learning Job asynchronously and notifies the listener on completion + *

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

+ * @param request the request to delete the job + * @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) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + RequestConverters::deleteMachineLearningJob, + options, + DeleteJobResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Opens a Machine Learning Job. * When you open a new job, it starts with an empty model. diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 973c0ce126d3..c40b4893e014 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -112,6 +112,7 @@ import org.elasticsearch.protocol.xpack.license.GetLicenseRequest; import org.elasticsearch.protocol.xpack.license.PutLicenseRequest; import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; +import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; @@ -1211,6 +1212,21 @@ static Request putMachineLearningJob(PutJobRequest putJobRequest) throws IOExcep return request; } + static Request deleteMachineLearningJob(DeleteJobRequest deleteJobRequest) { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(deleteJobRequest.getJobId()) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + + Params params = new Params(request); + params.putParam("force", Boolean.toString(deleteJobRequest.isForce())); + + return request; + } + static Request machineLearningOpenJob(OpenJobRequest openJobRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") 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 94e73a14c188..0037460150f1 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,8 @@ import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -48,6 +50,19 @@ public void testPutJob() throws Exception { assertThat(createdJob.getJobType(), is(Job.ANOMALY_DETECTOR_JOB_TYPE)); } + public void testDeleteJob() throws Exception { + String jobId = randomValidJobId(); + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + DeleteJobResponse response = execute(new DeleteJobRequest(jobId), + machineLearningClient::deleteJob, + machineLearningClient::deleteJobAsync); + + assertTrue(response.isAcknowledged()); + } + public void testOpenJob() throws Exception { String jobId = randomValidJobId(); Job job = buildJob(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 1c9707e0e27f..786cb94f8926 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -127,6 +127,7 @@ import org.elasticsearch.index.rankeval.RestRankEvalAction; import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; +import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest; @@ -2611,6 +2612,20 @@ public void testXPackDeleteWatch() { assertThat(request.getEntity(), nullValue()); } + public void testDeleteMachineLearningJob() { + String jobId = randomAlphaOfLength(10); + DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId); + + Request request = RequestConverters.deleteMachineLearningJob(deleteJobRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId, request.getEndpoint()); + assertEquals(Boolean.toString(false), request.getParameters().get("force")); + + deleteJobRequest.setForce(true); + request = RequestConverters.deleteMachineLearningJob(deleteJobRequest); + assertEquals(Boolean.toString(true), request.getParameters().get("force")); + } + public void testPostMachineLearningOpenJob() throws Exception { String jobId = "some-job-id"; OpenJobRequest openJobRequest = new OpenJobRequest(jobId); 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 50cd244c0fa0..a77d8b43e573 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 @@ -25,6 +25,8 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -122,6 +124,56 @@ public void onFailure(Exception e) { } } + public void testDeleteJob() throws Exception { + RestHighLevelClient client = highLevelClient(); + + String jobId = "my-first-machine-learning-job"; + + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + Job secondJob = MachineLearningIT.buildJob("my-second-machine-learning-job"); + client.machineLearning().putJob(new PutJobRequest(secondJob), RequestOptions.DEFAULT); + + { + //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); + //end::x-pack-delete-ml-job-request + + //tag::x-pack-delete-ml-job-response + boolean isAcknowledged = deleteJobResponse.isAcknowledged(); //<1> + //end::x-pack-delete-ml-job-response + } + { + //tag::x-pack-delete-ml-job-request-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(DeleteJobResponse deleteJobResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::x-pack-delete-ml-job-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-job-request-async + DeleteJobRequest deleteJobRequest = new DeleteJobRequest("my-second-machine-learning-job"); + client.machineLearning().deleteJobAsync(deleteJobRequest, RequestOptions.DEFAULT, listener); // <1> + //end::x-pack-delete-ml-job-request-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testOpenJob() throws Exception { RestHighLevelClient client = highLevelClient(); @@ -143,7 +195,6 @@ public void testOpenJob() throws Exception { //end::x-pack-ml-open-job-execute } - { //tag::x-pack-ml-open-job-listener ActionListener listener = new ActionListener() { @@ -154,7 +205,7 @@ public void onResponse(OpenJobResponse openJobResponse) { @Override public void onFailure(Exception e) { - //<2> + // <2> } }; //end::x-pack-ml-open-job-listener @@ -169,6 +220,5 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } - } } diff --git a/docs/java-rest/high-level/ml/delete-job.asciidoc b/docs/java-rest/high-level/ml/delete-job.asciidoc new file mode 100644 index 000000000000..44a6a4794095 --- /dev/null +++ b/docs/java-rest/high-level/ml/delete-job.asciidoc @@ -0,0 +1,49 @@ +[[java-rest-high-x-pack-ml-delete-job]] +=== Delete Job API + +[[java-rest-high-x-pack-machine-learning-delete-job-request]] +==== Delete Job Request + +A `DeleteJobRequest` object requires a non-null `jobId` 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-job-request] +--------------------------------------------------- +<1> Use to forcefully delete an opened job; +this method is quicker than closing and deleting the job. +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: +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-job-response] +--------------------------------------------------- +<1> `isAcknowledged` was the deletion request acknowledged or not + +[[java-rest-high-x-pack-machine-learning-delete-job-async]] +==== Delete Job Asynchronously + +This request can also be made asynchronously. +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-job-request-async] +--------------------------------------------------- +<1> The `DeleteJobRequest` 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 `DeleteJobRequest` could be defined as follows: + +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-job-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/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index a2db3436317c..6bcb736243a7 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -205,9 +205,11 @@ include::licensing/delete-license.asciidoc[] The Java High Level REST Client supports the following Machine Learning APIs: * <> +* <> * <> include::ml/put-job.asciidoc[] +include::ml/delete-job.asciidoc[] include::ml/open-job.asciidoc[] == Migration APIs diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java new file mode 100644 index 000000000000..1b7450de0929 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; + +import java.util.Objects; + +public class DeleteJobRequest extends ActionRequest { + + private String jobId; + private boolean force; + + public DeleteJobRequest(String jobId) { + this.jobId = Objects.requireNonNull(jobId, "[job_id] must not be null"); + } + + public String getJobId() { + return jobId; + } + + public void setJobId(String jobId) { + this.jobId = Objects.requireNonNull(jobId, "[job_id] must not be null"); + } + + public boolean isForce() { + return force; + } + + public void setForce(boolean force) { + this.force = force; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public int hashCode() { + return Objects.hash(jobId, force); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != getClass()) { + return false; + } + + DeleteJobRequest other = (DeleteJobRequest) obj; + return Objects.equals(jobId, other.jobId) && Objects.equals(force, other.force); + } + +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java new file mode 100644 index 000000000000..0b4faa38f545 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.ml; + +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +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/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequestTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequestTests.java new file mode 100644 index 000000000000..fb8a38fa0c68 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequestTests.java @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.ml; + +import org.elasticsearch.protocol.xpack.ml.job.config.JobTests; +import org.elasticsearch.test.ESTestCase; + +public class DeleteJobRequestTests extends ESTestCase { + + private DeleteJobRequest createTestInstance() { + return new DeleteJobRequest(JobTests.randomValidJobId()); + } + + public void test_WithNullJobId() { + NullPointerException ex = expectThrows(NullPointerException.class, () -> new DeleteJobRequest(null)); + assertEquals("[job_id] must not be null", ex.getMessage()); + + ex = expectThrows(NullPointerException.class, () -> createTestInstance().setJobId(null)); + assertEquals("[job_id] must not be null", ex.getMessage()); + } + + public void test_WithForce() { + DeleteJobRequest deleteJobRequest = createTestInstance(); + assertFalse(deleteJobRequest.isForce()); + + deleteJobRequest.setForce(true); + assertTrue(deleteJobRequest.isForce()); + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponseTests.java new file mode 100644 index 000000000000..a73179a08983 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponseTests.java @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.ml; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class DeleteJobResponseTests extends AbstractXContentTestCase { + + @Override + protected DeleteJobResponse createTestInstance() { + return new DeleteJobResponse(); + } + + @Override + protected DeleteJobResponse doParseInstance(XContentParser parser) throws IOException { + return DeleteJobResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} From 3dd1677cdc8fa6b4c36a79f7b206430661469ce8 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Thu, 16 Aug 2018 15:33:17 +0200 Subject: [PATCH 013/283] [Test] Fix DuelScrollIT#testDuelIndexOrderQueryThenFetch This commit disables the automatic `refresh_interval` in order to ensure that index readers cannot differ between the normal and scroll search. This issue is related to the 7.5 Lucene upgrade which contains a change that makes single segment merge more likely to occur (max deletes percentage). Closes #32682 --- .../java/org/elasticsearch/search/scroll/DuelScrollIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java b/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java index 1ddd11e5d0f7..4005f1218a92 100644 --- a/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java +++ b/server/src/test/java/org/elasticsearch/search/scroll/DuelScrollIT.java @@ -199,6 +199,8 @@ private int createIndex(boolean singleShard) throws Exception { } // no replicas, as they might be ordered differently settings.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0); + // we need to control refreshes as they might take different merges into account + settings.put("index.refresh_interval", -1); assertAcked(prepareCreate("test").setSettings(settings.build()).get()); final int numDocs = randomIntBetween(10, 200); @@ -257,7 +259,6 @@ private void testDuelIndexOrder(SearchType searchType, boolean trackScores, int } } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32682") public void testDuelIndexOrderQueryThenFetch() throws Exception { final SearchType searchType = RandomPicks.randomFrom(random(), Arrays.asList(SearchType.QUERY_THEN_FETCH, SearchType.DFS_QUERY_THEN_FETCH)); From eaaf37a1f99309c8a7c176c9b2cc7f56ec5c69d1 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 16 Aug 2018 15:25:51 +0200 Subject: [PATCH 014/283] AwaitFix FullClusterRestartIT#testRollupIDSchemeAfterRestart. --- .../org/elasticsearch/xpack/restart/FullClusterRestartIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java index 24303b8342b7..6ead87aba610 100644 --- a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java +++ b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java @@ -325,6 +325,7 @@ public void testRollupAfterRestart() throws Exception { } } + @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/32773") public void testRollupIDSchemeAfterRestart() throws Exception { assumeTrue("Rollup can be tested with 6.3.0 and onwards", oldClusterVersion.onOrAfter(Version.V_6_3_0)); assumeTrue("Rollup ID scheme changed in 6.4", oldClusterVersion.before(Version.V_6_4_0)); From d9fd74bcdcabd5020e40ef7e5dd67c6de09ae0f0 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Thu, 16 Aug 2018 08:03:21 -0700 Subject: [PATCH 015/283] Painless: Special Case def (#32871) This removes def from the classes map in PainlessLookup and instead always special cases it. This prevents potential calls against the def type that shouldn't be made and forces all cases of def throughout Painless code to be special cased. --- .../java/org/elasticsearch/painless/ScriptClassInfo.java | 3 ++- .../org/elasticsearch/painless/lookup/PainlessLookup.java | 3 ++- .../elasticsearch/painless/lookup/PainlessLookupBuilder.java | 5 +---- .../elasticsearch/painless/lookup/PainlessLookupUtility.java | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptClassInfo.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptClassInfo.java index 345db46f8875..7de8353194dd 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptClassInfo.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/ScriptClassInfo.java @@ -21,6 +21,7 @@ import org.elasticsearch.painless.lookup.PainlessLookup; import org.elasticsearch.painless.lookup.PainlessLookupUtility; +import org.elasticsearch.painless.lookup.def; import java.lang.invoke.MethodType; import java.lang.reflect.Field; @@ -190,7 +191,7 @@ private static Class definitionTypeForClass(PainlessLookup painlessLookup, Cl componentType = componentType.getComponentType(); } - if (painlessLookup.lookupPainlessClass(componentType) == null) { + if (componentType != def.class && painlessLookup.lookupPainlessClass(componentType) == null) { throw new IllegalArgumentException(unknownErrorMessageSource.apply(componentType)); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java index 16b8ac14f14f..55855a3cb1ef 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java @@ -26,6 +26,7 @@ import java.util.Set; import java.util.function.Function; +import static org.elasticsearch.painless.lookup.PainlessLookupUtility.DEF_CLASS_NAME; import static org.elasticsearch.painless.lookup.PainlessLookupUtility.buildPainlessConstructorKey; import static org.elasticsearch.painless.lookup.PainlessLookupUtility.buildPainlessFieldKey; import static org.elasticsearch.painless.lookup.PainlessLookupUtility.buildPainlessMethodKey; @@ -47,7 +48,7 @@ public final class PainlessLookup { public boolean isValidCanonicalClassName(String canonicalClassName) { Objects.requireNonNull(canonicalClassName); - return canonicalClassNamesToClasses.containsKey(canonicalClassName); + return DEF_CLASS_NAME.equals(canonicalClassName) || canonicalClassNamesToClasses.containsKey(canonicalClassName); } public Class canonicalTypeNameToType(String canonicalTypeName) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index e644453a4c1b..c8353b54c9f4 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -211,9 +211,6 @@ public static PainlessLookup buildFromWhitelists(List whitelists) { public PainlessLookupBuilder() { canonicalClassNamesToClasses = new HashMap<>(); classesToPainlessClassBuilders = new HashMap<>(); - - canonicalClassNamesToClasses.put(DEF_CLASS_NAME, def.class); - classesToPainlessClassBuilders.put(def.class, new PainlessClassBuilder()); } private Class canonicalTypeNameToType(String canonicalTypeName) { @@ -225,7 +222,7 @@ private boolean isValidType(Class type) { type = type.getComponentType(); } - return classesToPainlessClassBuilders.containsKey(type); + return type == def.class || classesToPainlessClassBuilders.containsKey(type); } public void addPainlessClass(ClassLoader classLoader, String javaClassName, boolean importClassName) { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java index f2eb43451696..71cacab9eba9 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java @@ -82,7 +82,7 @@ public static Class canonicalTypeNameToType(String canonicalTypeName, Map type = canonicalClassNamesToClasses.get(canonicalTypeName); + Class type = DEF_CLASS_NAME.equals(canonicalTypeName) ? def.class : canonicalClassNamesToClasses.get(canonicalTypeName); if (type != null) { return type; @@ -105,7 +105,7 @@ public static Class canonicalTypeNameToType(String canonicalTypeName, Map Date: Thu, 16 Aug 2018 11:32:35 -0400 Subject: [PATCH 016/283] Fix docs for fixed filename for heap dump path (#32882) The docs here incorrectly state that it is okay for a heap dump file to exist when heap dump path is configured to a fixed filename. This is incorrect, the JVM will fail to write the heap dump if a heap dump file already exists at the specified location (see the DumpWriter constructor DumpWriter::DumpWriter(const char* path) in the JVM source). --- .../setup/important-settings/heap-dump-path.asciidoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/reference/setup/important-settings/heap-dump-path.asciidoc b/docs/reference/setup/important-settings/heap-dump-path.asciidoc index b0d301b21d0b..fb8c7ff35f0d 100644 --- a/docs/reference/setup/important-settings/heap-dump-path.asciidoc +++ b/docs/reference/setup/important-settings/heap-dump-path.asciidoc @@ -8,8 +8,8 @@ distributions, and the `data` directory under the root of the Elasticsearch installation for the <> archive distributions). If this path is not suitable for receiving heap dumps, you should modify the entry `-XX:HeapDumpPath=...` in -<>. If you specify a fixed filename instead -of a directory, the JVM will repeatedly use the same file; this is one -mechanism for preventing heap dumps from accumulating in the heap dump -path. Alternatively, you can configure a scheduled task via your OS to -remove heap dumps that are older than a configured age. +<>. If you specify a directory, the JVM +will generate a filename for the heap dump based on the PID of the running +instance. If you specify a fixed filename instead of a directory, the file must +not exist when the JVM needs to perform a heap dump on an out of memory +exception, otherwise the heap dump will fail. From d604b3e3a1aca100c4560b4d05f846148b76fef4 Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Thu, 16 Aug 2018 17:18:51 +0100 Subject: [PATCH 017/283] Temporarily disabled ML BWC tests for backporting https://github.com/elastic/elasticsearch/pull/32816 --- .../test/mixed_cluster/40_ml_datafeed_crud.yml | 6 ++++++ .../rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml | 6 ++++++ .../test/upgraded_cluster/40_ml_datafeed_crud.yml | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml index 0ec288f90973..529e9e497caf 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml @@ -1,3 +1,9 @@ +--- +setup: + - skip: + version: "all" + reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" + --- "Test old cluster datafeed": - do: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml index c1317bdf3d66..b8cfcbcda4b1 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml @@ -1,3 +1,9 @@ +--- +setup: + - skip: + version: "all" + reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" + --- "Put job and datafeed in old cluster": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml index 6b4c963dd533..13e7289457a1 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml @@ -1,4 +1,8 @@ setup: + - skip: + version: "all" + reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" + - do: cluster.health: wait_for_status: green From 62559d2b3c26c5617049c1c328d0e5522680e96f Mon Sep 17 00:00:00 2001 From: Ed Savage <32410745+edsavage@users.noreply.github.com> Date: Thu, 16 Aug 2018 18:23:26 +0100 Subject: [PATCH 018/283] Re enable ml bwc tests (#32916) [ML] Re-enabling BWC tests Re-enable BWC tests for ML now that #32816 has been backported to 6.x --- .../xpack/core/ml/job/config/AnalysisConfig.java | 10 ++++------ .../test/mixed_cluster/30_ml_jobs_crud.yml | 6 ------ .../test/mixed_cluster/40_ml_datafeed_crud.yml | 6 ------ .../rest-api-spec/test/old_cluster/30_ml_jobs_crud.yml | 6 ------ .../test/old_cluster/40_ml_datafeed_crud.yml | 6 ------ .../test/upgraded_cluster/30_ml_jobs_crud.yml | 4 ---- .../test/upgraded_cluster/40_ml_datafeed_crud.yml | 4 ---- 7 files changed, 4 insertions(+), 38 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/AnalysisConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/AnalysisConfig.java index 135ad755359e..9068ffda4de5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/AnalysisConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/AnalysisConfig.java @@ -162,9 +162,8 @@ public AnalysisConfig(StreamInput in) throws IOException { } // BWC for removed per-partition normalization - // Version check is temporarily against the latest to satisfy CI tests - // TODO change to V_6_5_0 after successful backport to 6.x - if (in.getVersion().before(Version.V_7_0_0_alpha1)) { + // TODO Remove in 7.0.0 + if (in.getVersion().before(Version.V_6_5_0)) { in.readBoolean(); } } @@ -197,9 +196,8 @@ public void writeTo(StreamOutput out) throws IOException { } // BWC for removed per-partition normalization - // Version check is temporarily against the latest to satisfy CI tests - // TODO change to V_6_5_0 after successful backport to 6.x - if (out.getVersion().before(Version.V_7_0_0_alpha1)) { + // TODO Remove in 7.0.0 + if (out.getVersion().before(Version.V_6_5_0)) { out.writeBoolean(false); } } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/30_ml_jobs_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/30_ml_jobs_crud.yml index cb036b9d13a2..ba0f4d5091e0 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/30_ml_jobs_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/30_ml_jobs_crud.yml @@ -1,9 +1,3 @@ ---- -setup: - - skip: - version: "all" - reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" - --- "Test get old cluster job": - do: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml index 529e9e497caf..0ec288f90973 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/40_ml_datafeed_crud.yml @@ -1,9 +1,3 @@ ---- -setup: - - skip: - version: "all" - reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" - --- "Test old cluster datafeed": - do: diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/30_ml_jobs_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/30_ml_jobs_crud.yml index 061a242a78d3..3a3334f6907e 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/30_ml_jobs_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/30_ml_jobs_crud.yml @@ -1,9 +1,3 @@ ---- -setup: - - skip: - version: "all" - reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" - --- "Put job on the old cluster and post some data": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml index b8cfcbcda4b1..c1317bdf3d66 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/40_ml_datafeed_crud.yml @@ -1,9 +1,3 @@ ---- -setup: - - skip: - version: "all" - reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" - --- "Put job and datafeed in old cluster": diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/30_ml_jobs_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/30_ml_jobs_crud.yml index 1da16e79cbe6..bb47524b41d8 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/30_ml_jobs_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/30_ml_jobs_crud.yml @@ -1,8 +1,4 @@ setup: - - skip: - version: "all" - reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" - - do: cluster.health: wait_for_status: green diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml index 13e7289457a1..6b4c963dd533 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/upgraded_cluster/40_ml_datafeed_crud.yml @@ -1,8 +1,4 @@ setup: - - skip: - version: "all" - reason: "Temporarily disabled while backporting https://github.com/elastic/elasticsearch/pull/32816" - - do: cluster.health: wait_for_status: green From 0876630b3069c2d37005c21e06f33980a4541422 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 16 Aug 2018 16:25:34 -0400 Subject: [PATCH 019/283] Guard against null in email admin watches (#32923) The Kibana settings docs that these watches rely on can sometimes contain no xpack settings. When this is the case, we will end up with a null pointer exception in the script. We need to guard against in these scripts so this commit does that. --- .../monitoring/watches/elasticsearch_cluster_status.json | 2 +- .../main/resources/monitoring/watches/elasticsearch_nodes.json | 2 +- .../monitoring/watches/elasticsearch_version_mismatch.json | 2 +- .../resources/monitoring/watches/kibana_version_mismatch.json | 2 +- .../resources/monitoring/watches/logstash_version_mismatch.json | 2 +- .../resources/monitoring/watches/xpack_license_expiration.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_cluster_status.json b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_cluster_status.json index c0a13ea63a64..e1f418d5a8d7 100644 --- a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_cluster_status.json +++ b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_cluster_status.json @@ -145,7 +145,7 @@ }, "transform": { "script": { - "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def state = ctx.payload.check.hits.hits[0]._source.cluster_state.status;if (ctx.vars.not_resolved){ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check == false) {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = ['timestamp': ctx.execution_time, 'metadata': ctx.metadata.xpack];}if (ctx.vars.fails_check) {ctx.payload.prefix = 'Elasticsearch cluster status is ' + state + '.';if (state == 'red') {ctx.payload.message = 'Allocate missing primary shards and replica shards.';ctx.payload.metadata.severity = 2100;} else {ctx.payload.message = 'Allocate missing replica shards.';ctx.payload.metadata.severity = 1100;}}ctx.vars.state = state.toUpperCase();ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" + "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0 && ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack != null) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def state = ctx.payload.check.hits.hits[0]._source.cluster_state.status;if (ctx.vars.not_resolved){ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check == false) {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = ['timestamp': ctx.execution_time, 'metadata': ctx.metadata.xpack];}if (ctx.vars.fails_check) {ctx.payload.prefix = 'Elasticsearch cluster status is ' + state + '.';if (state == 'red') {ctx.payload.message = 'Allocate missing primary shards and replica shards.';ctx.payload.metadata.severity = 2100;} else {ctx.payload.message = 'Allocate missing replica shards.';ctx.payload.metadata.severity = 1100;}}ctx.vars.state = state.toUpperCase();ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" } }, "actions": { diff --git a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_nodes.json b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_nodes.json index a6bf7b6145ce..5c0cb7f55b4e 100644 --- a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_nodes.json +++ b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_nodes.json @@ -151,7 +151,7 @@ }, "transform": { "script": { - "source": "void formatResults(StringBuilder message, String type, Map typeMap) {if (typeMap.empty == false) {message.append(' Node');if (typeMap.size() != 1) {message.append('s were');} else {message.append(' was');}message.append(' ').append(type).append(' [').append(typeMap.size()).append(']: ').append(typeMap.values().stream().collect(Collectors.joining(', ', '[', ']'))).append('.');}}ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;def clusterState = ctx.payload.check.hits.hits[0]._source.cluster_state;def persistentUuidToName = [:];def latestNodes = clusterState.nodes;def ephemeralUuidToPersistentUuid = [:];def payload = ['timestamp': ctx.execution_time,'updated_timestamp': ctx.execution_time,'resolved_timestamp': ctx.execution_time,'metadata': ctx.metadata.xpack,'prefix': 'Elasticsearch cluster nodes have changed!','nodes': ['hash': clusterState.nodes_hash,'added': persistentUuidToName,'removed': [:],'restarted': [:]]];for (def latestNode : latestNodes.entrySet()) {persistentUuidToName[latestNode.key] = latestNode.value.name;ephemeralUuidToPersistentUuid[latestNode.value.ephemeral_id] = latestNode.key;}def previousNodes = ctx.payload.check.hits.hits[1]._source.cluster_state.nodes;def previousPersistentUuidToName = [:];for (def previousNode : previousNodes.entrySet()){if (persistentUuidToName.containsKey(previousNode.key) == false){payload.nodes.removed[previousNode.key] = previousNode.value.name;}else{if (ephemeralUuidToPersistentUuid.containsKey(previousNode.value.ephemeral_id) == false) {payload.nodes.restarted[previousNode.key] = persistentUuidToName[previousNode.key];}persistentUuidToName.remove(previousNode.key);}}StringBuilder message = new StringBuilder();formatResults(message, 'removed', payload.nodes.removed);formatResults(message, 'added', payload.nodes.added);formatResults(message, 'restarted', payload.nodes.restarted);payload.message = message.toString().trim();return payload;" + "source": "void formatResults(StringBuilder message, String type, Map typeMap) {if (typeMap.empty == false) {message.append(' Node');if (typeMap.size() != 1) {message.append('s were');} else {message.append(' was');}message.append(' ').append(type).append(' [').append(typeMap.size()).append(']: ').append(typeMap.values().stream().collect(Collectors.joining(', ', '[', ']'))).append('.');}}ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0 && ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack != null) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;def clusterState = ctx.payload.check.hits.hits[0]._source.cluster_state;def persistentUuidToName = [:];def latestNodes = clusterState.nodes;def ephemeralUuidToPersistentUuid = [:];def payload = ['timestamp': ctx.execution_time,'updated_timestamp': ctx.execution_time,'resolved_timestamp': ctx.execution_time,'metadata': ctx.metadata.xpack,'prefix': 'Elasticsearch cluster nodes have changed!','nodes': ['hash': clusterState.nodes_hash,'added': persistentUuidToName,'removed': [:],'restarted': [:]]];for (def latestNode : latestNodes.entrySet()) {persistentUuidToName[latestNode.key] = latestNode.value.name;ephemeralUuidToPersistentUuid[latestNode.value.ephemeral_id] = latestNode.key;}def previousNodes = ctx.payload.check.hits.hits[1]._source.cluster_state.nodes;def previousPersistentUuidToName = [:];for (def previousNode : previousNodes.entrySet()){if (persistentUuidToName.containsKey(previousNode.key) == false){payload.nodes.removed[previousNode.key] = previousNode.value.name;}else{if (ephemeralUuidToPersistentUuid.containsKey(previousNode.value.ephemeral_id) == false) {payload.nodes.restarted[previousNode.key] = persistentUuidToName[previousNode.key];}persistentUuidToName.remove(previousNode.key);}}StringBuilder message = new StringBuilder();formatResults(message, 'removed', payload.nodes.removed);formatResults(message, 'added', payload.nodes.added);formatResults(message, 'restarted', payload.nodes.restarted);payload.message = message.toString().trim();return payload;" } }, "actions": { diff --git a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_version_mismatch.json b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_version_mismatch.json index 7e18c981f0f1..051a3a9d4092 100644 --- a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_version_mismatch.json +++ b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/elasticsearch_version_mismatch.json @@ -141,7 +141,7 @@ }, "transform": { "script": { - "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def versionMessage = null;if (ctx.vars.fails_check) {def versions = new ArrayList(ctx.payload.check.hits.hits[0]._source.cluster_stats.nodes.versions);Collections.sort(versions);versionMessage = 'Versions: [' + String.join(', ', versions) + '].';}if (ctx.vars.not_resolved) {ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check) {ctx.payload.message = versionMessage;} else {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster is running with multiple versions of Elasticsearch.', 'message': versionMessage, 'metadata': ctx.metadata.xpack ];}ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" + "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0 && ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack != null) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def versionMessage = null;if (ctx.vars.fails_check) {def versions = new ArrayList(ctx.payload.check.hits.hits[0]._source.cluster_stats.nodes.versions);Collections.sort(versions);versionMessage = 'Versions: [' + String.join(', ', versions) + '].';}if (ctx.vars.not_resolved) {ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check) {ctx.payload.message = versionMessage;} else {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster is running with multiple versions of Elasticsearch.', 'message': versionMessage, 'metadata': ctx.metadata.xpack ];}ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" } }, "actions": { diff --git a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/kibana_version_mismatch.json b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/kibana_version_mismatch.json index bf2da3ffb1dd..b2acba610e14 100644 --- a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/kibana_version_mismatch.json +++ b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/kibana_version_mismatch.json @@ -161,7 +161,7 @@ }, "transform": { "script": { - "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def versionMessage = null;if (ctx.vars.fails_check) {versionMessage = 'Versions: [' + String.join(', ', ctx.vars.versions) + '].';}if (ctx.vars.not_resolved) {ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check) {ctx.payload.message = versionMessage;} else {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster is running with multiple versions of Kibana.', 'message': versionMessage, 'metadata': ctx.metadata.xpack ];}ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" + "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0 && ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack != null) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def versionMessage = null;if (ctx.vars.fails_check) {versionMessage = 'Versions: [' + String.join(', ', ctx.vars.versions) + '].';}if (ctx.vars.not_resolved) {ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check) {ctx.payload.message = versionMessage;} else {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster is running with multiple versions of Kibana.', 'message': versionMessage, 'metadata': ctx.metadata.xpack ];}ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" } }, "actions": { diff --git a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/logstash_version_mismatch.json b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/logstash_version_mismatch.json index 71a0cfd46bfd..cf1fdde606c7 100644 --- a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/logstash_version_mismatch.json +++ b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/logstash_version_mismatch.json @@ -161,7 +161,7 @@ }, "transform": { "script": { - "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def versionMessage = null;if (ctx.vars.fails_check) {versionMessage = 'Versions: [' + String.join(', ', ctx.vars.versions) + '].';}if (ctx.vars.not_resolved) {ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check) {ctx.payload.message = versionMessage;} else {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster is running with multiple versions of Logstash.', 'message': versionMessage, 'metadata': ctx.metadata.xpack ];}ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" + "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0 && ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack != null) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def versionMessage = null;if (ctx.vars.fails_check) {versionMessage = 'Versions: [' + String.join(', ', ctx.vars.versions) + '].';}if (ctx.vars.not_resolved) {ctx.payload = ctx.payload.alert.hits.hits[0]._source;if (ctx.vars.fails_check) {ctx.payload.message = versionMessage;} else {ctx.payload.resolved_timestamp = ctx.execution_time;}} else {ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster is running with multiple versions of Logstash.', 'message': versionMessage, 'metadata': ctx.metadata.xpack ];}ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" } }, "actions": { diff --git a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/xpack_license_expiration.json b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/xpack_license_expiration.json index a05198a15eb9..7eb0d59167db 100644 --- a/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/xpack_license_expiration.json +++ b/x-pack/plugin/monitoring/src/main/resources/monitoring/watches/xpack_license_expiration.json @@ -134,7 +134,7 @@ }, "transform": { "script": { - "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def alertMessage = null;if (ctx.vars.fails_check) { alertMessage = 'Update your license.';} if (ctx.vars.not_resolved) { ctx.payload = ctx.payload.alert.hits.hits[0]._source;ctx.payload.metadata = ctx.metadata.xpack;if (ctx.vars.fails_check == false) { ctx.payload.resolved_timestamp = ctx.execution_time;} } else { ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster\\'s license is going to expire in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', 'message': alertMessage, 'metadata': ctx.metadata.xpack ];} if (ctx.vars.fails_check) { ctx.payload.metadata.time = ctx.vars.expiry.toString();} ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" + "source": "ctx.vars.email_recipient = (ctx.payload.kibana_settings.hits.total > 0 && ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack != null) ? ctx.payload.kibana_settings.hits.hits[0]._source.kibana_settings.xpack.default_admin_email : null;ctx.vars.is_new = ctx.vars.fails_check && !ctx.vars.not_resolved;ctx.vars.is_resolved = !ctx.vars.fails_check && ctx.vars.not_resolved;def alertMessage = null;if (ctx.vars.fails_check) { alertMessage = 'Update your license.';} if (ctx.vars.not_resolved) { ctx.payload = ctx.payload.alert.hits.hits[0]._source;ctx.payload.metadata = ctx.metadata.xpack;if (ctx.vars.fails_check == false) { ctx.payload.resolved_timestamp = ctx.execution_time;} } else { ctx.payload = [ 'timestamp': ctx.execution_time, 'prefix': 'This cluster\\'s license is going to expire in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', 'message': alertMessage, 'metadata': ctx.metadata.xpack ];} if (ctx.vars.fails_check) { ctx.payload.metadata.time = ctx.vars.expiry.toString();} ctx.payload.update_timestamp = ctx.execution_time;return ctx.payload;" } }, "actions": { From cbf160a4e6c6c13f63f2d0d2dd1adf006fc70d97 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Thu, 16 Aug 2018 17:36:58 -0700 Subject: [PATCH 020/283] For filters aggs, make sure that rewrites preserve other_bucket. (#32921) --- .../bucket/filter/FiltersAggregationBuilder.java | 5 ++++- .../search/aggregations/bucket/FiltersTests.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregationBuilder.java index e35bf376aae4..810126e85125 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregationBuilder.java @@ -209,7 +209,10 @@ protected AggregationBuilder doRewrite(QueryRewriteContext queryShardContext) th } } if (changed) { - return new FiltersAggregationBuilder(getName(), rewrittenFilters, this.keyed); + FiltersAggregationBuilder rewritten = new FiltersAggregationBuilder(getName(), rewrittenFilters, this.keyed); + rewritten.otherBucket(otherBucket); + rewritten.otherBucketKey(otherBucketKey); + return rewritten; } else { return this; } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/FiltersTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/FiltersTests.java index 327a717f05c5..bdfdd4d028f0 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/FiltersTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/FiltersTests.java @@ -178,4 +178,18 @@ public void testRewrite() throws IOException { assertSame(rewritten, rewritten.rewrite(new QueryRewriteContext(xContentRegistry(), null, null, () -> 0L))); } + + public void testRewritePreservesOtherBucket() throws IOException { + FiltersAggregationBuilder originalFilters = new FiltersAggregationBuilder("my-agg", new BoolQueryBuilder()); + originalFilters.otherBucket(randomBoolean()); + originalFilters.otherBucketKey(randomAlphaOfLength(10)); + + AggregationBuilder rewritten = originalFilters.rewrite(new QueryRewriteContext(xContentRegistry(), + null, null, () -> 0L)); + assertThat(rewritten, instanceOf(FiltersAggregationBuilder.class)); + + FiltersAggregationBuilder rewrittenFilters = (FiltersAggregationBuilder) rewritten; + assertEquals(originalFilters.otherBucket(), rewrittenFilters.otherBucket()); + assertEquals(originalFilters.otherBucketKey(), rewrittenFilters.otherBucketKey()); + } } From 1136a9583792ae20ffc2d6c58324ce4e04962976 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Thu, 16 Aug 2018 21:16:06 -0600 Subject: [PATCH 021/283] Security: remove put privilege API (#32879) This commit removes the put privilege API in favor of having a single API to create and update privileges. If we see the need to have an API like this in the future we can always add it back. --- .../PutPrivilegesRequestBuilder.java | 27 ---------- .../core/security/client/SecurityClient.java | 6 --- .../xpack/security/Security.java | 2 - .../privilege/RestPutPrivilegeAction.java | 49 ------------------- .../privilege/RestPutPrivilegesAction.java | 2 + .../PutPrivilegesRequestBuilderTests.java | 30 ------------ .../api/xpack.security.put_privilege.json | 33 ------------- .../api/xpack.security.put_privileges.json | 2 +- .../test/privileges/10_basic.yml | 42 ++++++++-------- .../authz/40_condtional_cluster_priv.yml | 40 +++++++++------ 10 files changed, 49 insertions(+), 184 deletions(-) delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java delete mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java index b8c2685d28a1..562e22a1eb92 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilder.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -34,32 +33,6 @@ public PutPrivilegesRequestBuilder(ElasticsearchClient client, PutPrivilegesActi super(client, action, new PutPrivilegesRequest()); } - /** - * Populate the put privileges request using the given source, application name and privilege name - * The source must contain a single privilege object which matches the application and privilege names. - */ - public PutPrivilegesRequestBuilder source(String applicationName, String expectedName, - BytesReference source, XContentType xContentType) - throws IOException { - Objects.requireNonNull(xContentType); - // EMPTY is ok here because we never call namedObject - try (InputStream stream = source.streamInput(); - XContentParser parser = xContentType.xContent() - .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) { - XContentParser.Token token = parser.currentToken(); - if (token == null) { - token = parser.nextToken(); - } - if (token == XContentParser.Token.START_OBJECT) { - final ApplicationPrivilegeDescriptor privilege = parsePrivilege(parser, applicationName, expectedName); - this.request.setPrivileges(Collections.singleton(privilege)); - } else { - throw new ElasticsearchParseException("expected an object but found {} instead", token); - } - } - return this; - } - ApplicationPrivilegeDescriptor parsePrivilege(XContentParser parser, String applicationName, String privilegeName) throws IOException { ApplicationPrivilegeDescriptor privilege = ApplicationPrivilegeDescriptor.parse(parser, applicationName, privilegeName, false); checkPrivilegeName(privilege, applicationName, privilegeName); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java index e1d3a2db8e95..d3cc60194f2c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java @@ -292,12 +292,6 @@ public GetPrivilegesRequestBuilder prepareGetPrivileges(String applicationName, return new GetPrivilegesRequestBuilder(client, GetPrivilegesAction.INSTANCE).application(applicationName).privileges(privileges); } - public PutPrivilegesRequestBuilder preparePutPrivilege(String applicationName, String privilegeName, - BytesReference bytesReference, XContentType xContentType) throws IOException { - return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE) - .source(applicationName, privilegeName, bytesReference, xContentType); - } - public PutPrivilegesRequestBuilder preparePutPrivileges(BytesReference bytesReference, XContentType xContentType) throws IOException { return new PutPrivilegesRequestBuilder(client, PutPrivilegesAction.INSTANCE).source(bytesReference, xContentType); } 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 d31ffae13f24..857b343b753c 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 @@ -191,7 +191,6 @@ import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; import org.elasticsearch.xpack.security.rest.action.privilege.RestDeletePrivilegesAction; import org.elasticsearch.xpack.security.rest.action.privilege.RestGetPrivilegesAction; -import org.elasticsearch.xpack.security.rest.action.privilege.RestPutPrivilegeAction; import org.elasticsearch.xpack.security.rest.action.privilege.RestPutPrivilegesAction; import org.elasticsearch.xpack.security.rest.action.realm.RestClearRealmCacheAction; import org.elasticsearch.xpack.security.rest.action.role.RestClearRolesCacheAction; @@ -762,7 +761,6 @@ public List getRestHandlers(Settings settings, RestController restC new RestSamlInvalidateSessionAction(settings, restController, getLicenseState()), new RestGetPrivilegesAction(settings, restController, getLicenseState()), new RestPutPrivilegesAction(settings, restController, getLicenseState()), - new RestPutPrivilegeAction(settings, restController, getLicenseState()), new RestDeletePrivilegesAction(settings, restController, getLicenseState()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java deleted file mode 100644 index 6c3ef8e70fab..000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegeAction.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.security.rest.action.privilege; - -import org.elasticsearch.client.node.NodeClient; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.rest.RestController; -import org.elasticsearch.rest.RestRequest; -import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequestBuilder; -import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.client.SecurityClient; -import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; - -import java.io.IOException; - -import static org.elasticsearch.rest.RestRequest.Method.POST; -import static org.elasticsearch.rest.RestRequest.Method.PUT; - -/** - * Rest endpoint to add one or more {@link ApplicationPrivilege} objects to the security index - */ -public class RestPutPrivilegeAction extends SecurityBaseRestHandler { - - public RestPutPrivilegeAction(Settings settings, RestController controller, XPackLicenseState licenseState) { - super(settings, licenseState); - controller.registerHandler(PUT, "/_xpack/security/privilege/{application}/{privilege}", this); - controller.registerHandler(POST, "/_xpack/security/privilege/{application}/{privilege}", this); - } - - @Override - public String getName() { - return "xpack_security_put_privilege_action"; - } - - @Override - public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { - final String application = request.param("application"); - final String privilege = request.param("privilege"); - PutPrivilegesRequestBuilder requestBuilder = new SecurityClient(client) - .preparePutPrivilege(application, privilege, request.requiredContent(), request.getXContentType()) - .setRefreshPolicy(request.param("refresh")); - - return RestPutPrivilegesAction.execute(requestBuilder); - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java index eb1104c9bc03..dc565e3f8733 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestPutPrivilegesAction.java @@ -29,6 +29,7 @@ import java.util.Map; import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.rest.RestRequest.Method.PUT; /** * Rest endpoint to add one or more {@link ApplicationPrivilege} objects to the security index @@ -37,6 +38,7 @@ public class RestPutPrivilegesAction extends SecurityBaseRestHandler { public RestPutPrivilegesAction(Settings settings, RestController controller, XPackLicenseState licenseState) { super(settings, licenseState); + controller.registerHandler(PUT, "/_xpack/security/privilege/", this); controller.registerHandler(POST, "/_xpack/security/privilege/", this); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java index db0548c03ef3..2ece398d3d19 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/PutPrivilegesRequestBuilderTests.java @@ -52,36 +52,6 @@ private ApplicationPrivilegeDescriptor descriptor(String app, String name, Strin return new ApplicationPrivilegeDescriptor(app, name, Sets.newHashSet(actions), Collections.emptyMap()); } - public void testBuildRequestFromJsonObject() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); - builder.source("foo", "read", new BytesArray( - "{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }" - ), XContentType.JSON); - final List privileges = builder.request().getPrivileges(); - assertThat(privileges, iterableWithSize(1)); - assertThat(privileges, contains(descriptor("foo", "read", "data:/read/*", "admin:/read/*"))); - } - - public void testPrivilegeNameValidationOfSingleElement() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); - final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> - builder.source("foo", "write", new BytesArray( - "{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }" - ), XContentType.JSON)); - assertThat(exception.getMessage(), containsString("write")); - assertThat(exception.getMessage(), containsString("read")); - } - - public void testApplicationNameValidationOfSingleElement() throws Exception { - final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); - final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> - builder.source("bar", "read", new BytesArray( - "{ \"application\":\"foo\", \"name\":\"read\", \"actions\":[ \"data:/read/*\", \"admin:/read/*\" ] }" - ), XContentType.JSON)); - assertThat(exception.getMessage(), containsString("foo")); - assertThat(exception.getMessage(), containsString("bar")); - } - public void testPrivilegeNameValidationOfMultipleElement() throws Exception { final PutPrivilegesRequestBuilder builder = new PutPrivilegesRequestBuilder(null, PutPrivilegesAction.INSTANCE); final IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json deleted file mode 100644 index 3d453682c643..000000000000 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privilege.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "xpack.security.put_privilege": { - "documentation": "TODO", - "methods": [ "POST", "PUT" ], - "url": { - "path": "/_xpack/security/privilege/{application}/{name}", - "paths": [ "/_xpack/security/privilege/{application}/{name}" ], - "parts": { - "application": { - "type" : "string", - "description" : "Application name", - "required" : true - }, - "name": { - "type" : "string", - "description" : "Privilege name", - "required" : true - } - }, - "params": { - "refresh": { - "type" : "enum", - "options": ["true", "false", "wait_for"], - "description" : "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." - } - } - }, - "body": { - "description" : "The privilege to add", - "required" : true - } - } -} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json index 07eb54171581..312db3c9a182 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_privileges.json @@ -1,7 +1,7 @@ { "xpack.security.put_privileges": { "documentation": "TODO", - "methods": [ "POST" ], + "methods": [ "PUT", "POST" ], "url": { "path": "/_xpack/security/privilege/", "paths": [ diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml index e8dddf215357..30fa3a8d0784 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/10_basic.yml @@ -30,24 +30,26 @@ teardown: ignore: 404 --- "Test put and get privileges": - # Single privilege, with names in URL + # Single privilege - do: - xpack.security.put_privilege: - application: app - name: p1 + xpack.security.put_privileges: body: > { - "application": "app", - "name": "p1", - "actions": [ "data:read/*" , "action:login" ], - "metadata": { - "key1" : "val1a", - "key2" : "val2a" + "app": { + "p1": { + "application": "app", + "name": "p1", + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key1" : "val1a", + "key2" : "val2a" + } + } } } - match: { "app.p1" : { created: true } } - # Multiple privileges, no names in URL + # Multiple privileges - do: xpack.security.put_privileges: body: > @@ -84,18 +86,18 @@ teardown: - match: { "app.p3" : { created: true } } - match: { "app2.p1" : { created: true } } - # Update existing privilege, with names in URL + # Update existing privilege - do: - xpack.security.put_privilege: - application: app - name: p1 + xpack.security.put_privileges: body: > { - "application": "app", - "name": "p1", - "actions": [ "data:read/*" , "action:login" ], - "metadata": { - "key3" : "val3" + "app": { + "p1": { + "actions": [ "data:read/*" , "action:login" ], + "metadata": { + "key3" : "val3" + } + } } } - match: { "app.p1" : { created: false } } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/40_condtional_cluster_priv.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/40_condtional_cluster_priv.yml index b3a1e2206908..a7d3fabd2a28 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/40_condtional_cluster_priv.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/40_condtional_cluster_priv.yml @@ -31,21 +31,25 @@ setup: } - do: - xpack.security.put_privilege: - application: app-allow - name: read + xpack.security.put_privileges: body: > { - "actions": [ "data:read/*" ] + "app-allow": { + "read": { + "actions": [ "data:read/*" ] + } + } } - do: - xpack.security.put_privilege: - application: app_deny - name: read + xpack.security.put_privileges: body: > { - "actions": [ "data:read/*" ] + "app-deny": { + "read": { + "actions": [ "data:read/*" ] + } + } } --- @@ -82,12 +86,14 @@ teardown: - do: headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user - xpack.security.put_privilege: - application: app - name: read + xpack.security.put_privileges: body: > { - "actions": [ "data:read/*" ] + "app": { + "read": { + "actions": [ "data:read/*" ] + } + } } - match: { "app.read" : { created: true } } @@ -112,12 +118,14 @@ teardown: "Test put application privileges when not allowed": - do: headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user - xpack.security.put_privilege: - application: app_deny - name: write + xpack.security.put_privileges: body: > { - "actions": [ "data:write/*" ] + "app_deny": { + "write": { + "actions": [ "data:write/*" ] + } + } } catch: forbidden From d16562eab5c514f2caa9092b73c53e3a82f7d66d Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 17 Aug 2018 09:41:39 +0300 Subject: [PATCH 022/283] RFC: Test that example plugins build stand-alone (#32235) Add tests for build-tools to make sure example plugins build stand-alone using it. This will catch issues such as referencing files from the buildSrc directly, breaking external uses of build-tools. --- build.gradle | 8 + buildSrc/build.gradle | 13 ++ .../elasticsearch/gradle/BuildPlugin.groovy | 3 +- .../gradle/plugin/PluginBuildPlugin.groovy | 2 - .../plugin/PluginPropertiesExtension.groovy | 30 +++- .../gradle/plugin/PluginPropertiesTask.groovy | 1 - .../gradle/test/RestIntegTestTask.groovy | 11 +- .../gradle/BuildExamplePluginsIT.java | 164 ++++++++++++++++++ plugins/examples/custom-settings/build.gradle | 3 +- .../examples/painless-whitelist/build.gradle | 5 +- plugins/examples/rescore/build.gradle | 4 +- plugins/examples/rest-handler/build.gradle | 5 +- .../script-expert-scoring/build.gradle | 4 +- 13 files changed, 233 insertions(+), 20 deletions(-) create mode 100644 buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java diff --git a/build.gradle b/build.gradle index 3674e0a540bf..0df5b97ae4a2 100644 --- a/build.gradle +++ b/build.gradle @@ -87,8 +87,15 @@ subprojects { } } } + repositories { + maven { + name = 'localTest' + url = "${rootProject.buildDir}/local-test-repo" + } + } } } + plugins.withType(BuildPlugin).whenPluginAdded { project.licenseFile = project.rootProject.file('licenses/APACHE-LICENSE-2.0.txt') project.noticeFile = project.rootProject.file('NOTICE.txt') @@ -228,6 +235,7 @@ subprojects { "org.elasticsearch.client:elasticsearch-rest-high-level-client:${version}": ':client:rest-high-level', "org.elasticsearch.client:test:${version}": ':client:test', "org.elasticsearch.client:transport:${version}": ':client:transport', + "org.elasticsearch.plugin:elasticsearch-scripting-painless-spi:${version}": ':modules:lang-painless:spi', "org.elasticsearch.test:framework:${version}": ':test:framework', "org.elasticsearch.distribution.integ-test-zip:elasticsearch:${version}": ':distribution:archives:integ-test-zip', "org.elasticsearch.distribution.zip:elasticsearch:${version}": ':distribution:archives:zip', diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 5775b2b6323f..967c2e27ee8d 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -162,11 +162,24 @@ if (project != rootProject) { // it's fine as we run them as part of :buildSrc test.enabled = false task integTest(type: Test) { + // integration test requires the local testing repo for example plugin builds + dependsOn project.rootProject.allprojects.collect { + it.tasks.matching { it.name == 'publishNebulaPublicationToLocalTestRepository'} + } exclude "**/*Tests.class" include "**/*IT.class" testClassesDirs = sourceSets.test.output.classesDirs classpath = sourceSets.test.runtimeClasspath inputs.dir(file("src/testKit")) + // tell BuildExamplePluginsIT where to find the example plugins + systemProperty ( + 'test.build-tools.plugin.examples', + files( + project(':example-plugins').subprojects.collect { it.projectDir } + ).asPath, + ) + systemProperty 'test.local-test-repo-path', "${rootProject.buildDir}/local-test-repo" + systemProperty 'test.lucene-snapshot-revision', (versions.lucene =~ /\w+-snapshot-([a-z0-9]+)/)[0][1] } check.dependsOn(integTest) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 306a2bcb58bd..f3f014f0e8aa 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -554,7 +554,7 @@ class BuildPlugin implements Plugin { project.publishing { publications { nebula(MavenPublication) { - artifact project.tasks.shadowJar + artifacts = [ project.tasks.shadowJar ] artifactId = project.archivesBaseName /* * Configure the pom to include the "shadow" as compile dependencies @@ -584,7 +584,6 @@ class BuildPlugin implements Plugin { } } } - } /** Adds compiler settings to the project */ diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy index 00f178fda9c9..6f42e41beaa1 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy @@ -25,7 +25,6 @@ import org.elasticsearch.gradle.NoticeTask import org.elasticsearch.gradle.test.RestIntegTestTask import org.elasticsearch.gradle.test.RunTask import org.gradle.api.InvalidUserDataException -import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.XmlProvider @@ -39,7 +38,6 @@ import java.nio.file.Path import java.nio.file.StandardCopyOption import java.util.regex.Matcher import java.util.regex.Pattern - /** * Encapsulates build configuration for an Elasticsearch plugin. */ diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy index 6cfe44c80683..c250d7695a83 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesExtension.groovy @@ -20,6 +20,7 @@ package org.elasticsearch.gradle.plugin import org.gradle.api.Project import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile /** * A container for plugin properties that will be written to the plugin descriptor, for easy @@ -55,18 +56,39 @@ class PluginPropertiesExtension { boolean requiresKeystore = false /** A license file that should be included in the built plugin zip. */ - @Input - File licenseFile = null + private File licenseFile = null /** * A notice file that should be included in the built plugin zip. This will be * extended with notices from the {@code licenses/} directory. */ - @Input - File noticeFile = null + private File noticeFile = null + + Project project = null PluginPropertiesExtension(Project project) { name = project.name version = project.version + this.project = project + } + + @InputFile + File getLicenseFile() { + return licenseFile + } + + void setLicenseFile(File licenseFile) { + project.ext.licenseFile = licenseFile + this.licenseFile = licenseFile + } + + @InputFile + File getNoticeFile() { + return noticeFile + } + + void setNoticeFile(File noticeFile) { + project.ext.noticeFile = noticeFile + this.noticeFile = noticeFile } } diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy index 8e913153f05a..9588f77a71db 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginPropertiesTask.groovy @@ -23,7 +23,6 @@ import org.gradle.api.InvalidUserDataException import org.gradle.api.Task import org.gradle.api.tasks.Copy import org.gradle.api.tasks.OutputFile - /** * Creates a plugin descriptor. */ diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/RestIntegTestTask.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/RestIntegTestTask.groovy index d2101c48aabd..2838849981a1 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/RestIntegTestTask.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/RestIntegTestTask.groovy @@ -31,6 +31,7 @@ import org.gradle.api.provider.Provider import org.gradle.api.tasks.Copy import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskState +import org.gradle.plugins.ide.idea.IdeaPlugin import java.nio.charset.StandardCharsets import java.nio.file.Files @@ -243,10 +244,12 @@ public class RestIntegTestTask extends DefaultTask { } } } - project.idea { - module { - if (scopes.TEST != null) { - scopes.TEST.plus.add(project.configurations.restSpec) + if (project.plugins.hasPlugin(IdeaPlugin)) { + project.idea { + module { + if (scopes.TEST != null) { + scopes.TEST.plus.add(project.configurations.restSpec) + } } } } diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java new file mode 100644 index 000000000000..9b63d6f45e06 --- /dev/null +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java @@ -0,0 +1,164 @@ +/* + * 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.gradle; + +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.apache.commons.io.FileUtils; +import org.elasticsearch.gradle.test.GradleIntegrationTestCase; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class BuildExamplePluginsIT extends GradleIntegrationTestCase { + + private static List EXAMPLE_PLUGINS = Collections.unmodifiableList( + Arrays.stream( + Objects.requireNonNull(System.getProperty("test.build-tools.plugin.examples")) + .split(File.pathSeparator) + ).map(File::new).collect(Collectors.toList()) + ); + + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); + + public final File examplePlugin; + + public BuildExamplePluginsIT(File examplePlugin) { + this.examplePlugin = examplePlugin; + } + + @BeforeClass + public static void assertProjectsExist() { + assertEquals( + EXAMPLE_PLUGINS, + EXAMPLE_PLUGINS.stream().filter(File::exists).collect(Collectors.toList()) + ); + } + + @ParametersFactory + public static Iterable parameters() { + return EXAMPLE_PLUGINS + .stream() + .map(each -> new Object[] {each}) + .collect(Collectors.toList()); + } + + public void testCurrentExamplePlugin() throws IOException { + FileUtils.copyDirectory(examplePlugin, tmpDir.getRoot()); + // just get rid of deprecation warnings + Files.write( + getTempPath("settings.gradle"), + "enableFeaturePreview('STABLE_PUBLISHING')\n".getBytes(StandardCharsets.UTF_8) + ); + + adaptBuildScriptForTest(); + + Files.write( + tmpDir.newFile("NOTICE.txt").toPath(), + "dummy test notice".getBytes(StandardCharsets.UTF_8) + ); + + GradleRunner.create() + .withProjectDir(tmpDir.getRoot()) + .withArguments("clean", "check", "-s", "-i", "--warning-mode=all", "--scan") + .withPluginClasspath() + .build(); + } + + private void adaptBuildScriptForTest() throws IOException { + // Add the local repo as a build script URL so we can pull in build-tools and apply the plugin under test + // + is ok because we have no other repo and just want to pick up latest + writeBuildScript( + "buildscript {\n" + + " repositories {\n" + + " maven {\n" + + " url = '" + getLocalTestRepoPath() + "'\n" + + " }\n" + + " }\n" + + " dependencies {\n" + + " classpath \"org.elasticsearch.gradle:build-tools:+\"\n" + + " }\n" + + "}\n" + ); + // get the original file + Files.readAllLines(getTempPath("build.gradle"), StandardCharsets.UTF_8) + .stream() + .map(line -> line + "\n") + .forEach(this::writeBuildScript); + // Add a repositories section to be able to resolve dependencies + String luceneSnapshotRepo = ""; + String luceneSnapshotRevision = System.getProperty("test.lucene-snapshot-revision"); + if (luceneSnapshotRepo != null) { + luceneSnapshotRepo = " maven {\n" + + " url \"http://s3.amazonaws.com/download.elasticsearch.org/lucenesnapshots/" + luceneSnapshotRevision + "\"\n" + + " }\n"; + } + writeBuildScript("\n" + + "repositories {\n" + + " maven {\n" + + " url \"" + getLocalTestRepoPath() + "\"\n" + + " }\n" + + luceneSnapshotRepo + + "}\n" + ); + Files.delete(getTempPath("build.gradle")); + Files.move(getTempPath("build.gradle.new"), getTempPath("build.gradle")); + System.err.print("Generated build script is:"); + Files.readAllLines(getTempPath("build.gradle")).forEach(System.err::println); + } + + private Path getTempPath(String fileName) { + return new File(tmpDir.getRoot(), fileName).toPath(); + } + + private Path writeBuildScript(String script) { + try { + Path path = getTempPath("build.gradle.new"); + return Files.write( + path, + script.getBytes(StandardCharsets.UTF_8), + Files.exists(path) ? StandardOpenOption.APPEND : StandardOpenOption.CREATE_NEW + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String getLocalTestRepoPath() { + String property = System.getProperty("test.local-test-repo-path"); + Objects.requireNonNull(property, "test.local-test-repo-path not passed to tests"); + File file = new File(property); + assertTrue("Expected " + property + " to exist, but it did not!", file.exists()); + return file.getAbsolutePath(); + } + +} diff --git a/plugins/examples/custom-settings/build.gradle b/plugins/examples/custom-settings/build.gradle index e0e728cec242..3caf29c8513b 100644 --- a/plugins/examples/custom-settings/build.gradle +++ b/plugins/examples/custom-settings/build.gradle @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - apply plugin: 'elasticsearch.esplugin' esplugin { name 'custom-settings' description 'An example plugin showing how to register custom settings' classname 'org.elasticsearch.example.customsettings.ExampleCustomSettingsPlugin' + licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') + noticeFile rootProject.file('NOTICE.txt') } integTestCluster { diff --git a/plugins/examples/painless-whitelist/build.gradle b/plugins/examples/painless-whitelist/build.gradle index ef1ca7d741e9..cb2aeb82e9d0 100644 --- a/plugins/examples/painless-whitelist/build.gradle +++ b/plugins/examples/painless-whitelist/build.gradle @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - apply plugin: 'elasticsearch.esplugin' esplugin { @@ -24,10 +23,12 @@ esplugin { description 'An example whitelisting additional classes and methods in painless' classname 'org.elasticsearch.example.painlesswhitelist.MyWhitelistPlugin' extendedPlugins = ['lang-painless'] + licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') + noticeFile rootProject.file('NOTICE.txt') } dependencies { - compileOnly project(':modules:lang-painless') + compileOnly "org.elasticsearch.plugin:elasticsearch-scripting-painless-spi:${versions.elasticsearch}" } if (System.getProperty('tests.distribution') == null) { diff --git a/plugins/examples/rescore/build.gradle b/plugins/examples/rescore/build.gradle index 4adeb0c721ba..cdecd760c81e 100644 --- a/plugins/examples/rescore/build.gradle +++ b/plugins/examples/rescore/build.gradle @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ - apply plugin: 'elasticsearch.esplugin' esplugin { name 'example-rescore' description 'An example plugin implementing rescore and verifying that plugins *can* implement rescore' classname 'org.elasticsearch.example.rescore.ExampleRescorePlugin' + licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') + noticeFile rootProject.file('NOTICE.txt') } + diff --git a/plugins/examples/rest-handler/build.gradle b/plugins/examples/rest-handler/build.gradle index cfe84e6a45a9..eff2fd1b6c6e 100644 --- a/plugins/examples/rest-handler/build.gradle +++ b/plugins/examples/rest-handler/build.gradle @@ -16,13 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - apply plugin: 'elasticsearch.esplugin' esplugin { name 'rest-handler' description 'An example plugin showing how to register a REST handler' classname 'org.elasticsearch.example.resthandler.ExampleRestHandlerPlugin' + licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') + noticeFile rootProject.file('NOTICE.txt') } // No unit tests in this example @@ -40,4 +41,4 @@ integTestCluster { } integTestRunner { systemProperty 'external.address', "${ -> exampleFixture.addressAndPort }" -} +} \ No newline at end of file diff --git a/plugins/examples/script-expert-scoring/build.gradle b/plugins/examples/script-expert-scoring/build.gradle index 7c602d9bc027..e9da62acdcff 100644 --- a/plugins/examples/script-expert-scoring/build.gradle +++ b/plugins/examples/script-expert-scoring/build.gradle @@ -16,13 +16,15 @@ * specific language governing permissions and limitations * under the License. */ - apply plugin: 'elasticsearch.esplugin' esplugin { name 'script-expert-scoring' description 'An example script engine to use low level Lucene internals for expert scoring' classname 'org.elasticsearch.example.expertscript.ExpertScriptPlugin' + licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') + noticeFile rootProject.file('NOTICE.txt') } test.enabled = false + From 148a76f0c7498ca55c9b56c605578fdb04bded30 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 17 Aug 2018 11:14:58 +0300 Subject: [PATCH 023/283] Fix failing BuildExamplePluginsIT test --- plugins/examples/custom-suggester/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/examples/custom-suggester/build.gradle b/plugins/examples/custom-suggester/build.gradle index b36d5cd218d2..977e467391d8 100644 --- a/plugins/examples/custom-suggester/build.gradle +++ b/plugins/examples/custom-suggester/build.gradle @@ -23,6 +23,8 @@ esplugin { name 'custom-suggester' description 'An example plugin showing how to write and register a custom suggester' classname 'org.elasticsearch.example.customsuggester.CustomSuggesterPlugin' + licenseFile rootProject.file('licenses/APACHE-LICENSE-2.0.txt') + noticeFile rootProject.file('NOTICE.txt') } integTestCluster { @@ -30,4 +32,4 @@ integTestCluster { } // this plugin has no unit tests, only rest tests -tasks.test.enabled = false \ No newline at end of file +tasks.test.enabled = false From efdad7d5fc32b0887a6fa2f34b259dfb566cf77b Mon Sep 17 00:00:00 2001 From: JeffSaxeVA <42005772+JeffSaxeVA@users.noreply.github.com> Date: Fri, 17 Aug 2018 04:56:06 -0400 Subject: [PATCH 024/283] [DOCS] Add "remove a tag" script logic as an example (#32556) It took me quite a while of online searching and experimenting to realize the function-call asymmetry in the Add versus Remove from a list, like the "tags" list! I realize we cannot give examples for every single thing the user wants to do in Painless, but this is such a common use case (removing a tag from a single doc, or from a set of docs with Update-By-Query) that I believe it ought to be demonstrated immediately after the "add a tag" example. We have an example of removing an entire document field, but not removing one element of a list (a multi-valued field). Also, a minor grammar fix: I have added an apostrophe to the word "its" in the accompanying text of the example just above. --- docs/reference/docs/update.asciidoc | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/reference/docs/update.asciidoc b/docs/reference/docs/update.asciidoc index 7ba7e2da6336..1cfc122bee40 100644 --- a/docs/reference/docs/update.asciidoc +++ b/docs/reference/docs/update.asciidoc @@ -47,7 +47,7 @@ POST test/_doc/1/_update // TEST[continued] We can add a tag to the list of tags (note, if the tag exists, it -will still add it, since its a list): +will still add it, since it's a list): [source,js] -------------------------------------------------- @@ -65,6 +65,28 @@ POST test/_doc/1/_update // CONSOLE // TEST[continued] +We can remove a tag from the list of tags. Note that the Painless function to +`remove` a tag takes as its parameter the array index of the element you wish +to remove, so you need a bit more logic to locate it while avoiding a runtime +error. Note that if the tag was present more than once in the list, this will +remove only one occurrence of it: + +[source,js] +-------------------------------------------------- +POST test/_doc/1/_update +{ + "script" : { + "source": "if (ctx._source.tags.contains(params.tag)) { ctx._source.tags.remove(ctx._source.tags.indexOf(params.tag)) }", + "lang": "painless", + "params" : { + "tag" : "blue" + } + } +} +-------------------------------------------------- +// CONSOLE +// TEST[continued] + In addition to `_source`, the following variables are available through the `ctx` map: `_index`, `_type`, `_id`, `_version`, `_routing` and `_now` (the current timestamp). @@ -172,7 +194,7 @@ the request was ignored. "_index": "test", "_type": "_doc", "_id": "1", - "_version": 6, + "_version": 7, "result": "noop" } -------------------------------------------------- From dd5a5aab88edc88f0f7949ecda67432efdcfc616 Mon Sep 17 00:00:00 2001 From: JB Nizet Date: Fri, 17 Aug 2018 10:59:26 +0200 Subject: [PATCH 025/283] Fix allowed value for HighlighterBuilder encoder in javadocs (#32780) Relates to #32745 --- .../search/fetch/subphase/highlight/HighlightBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilder.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilder.java index 049de439ac75..9483e76d072a 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilder.java @@ -220,7 +220,7 @@ public HighlightBuilder tagsSchema(String schemaName) { /** * Set encoder for the highlighting - * are {@code styled} and {@code default}. + * are {@code html} and {@code default}. * * @param encoder name */ From ae38cfbaec24ed4ab5a28f091e2c9173ab06ea58 Mon Sep 17 00:00:00 2001 From: markwalkom Date: Fri, 17 Aug 2018 19:03:09 +1000 Subject: [PATCH 026/283] [DOCS] Update getting-started.asciidoc (#29518) Highlighted that you can change shard counts using `_shrink` and `_split`. --- docs/reference/getting-started.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/getting-started.asciidoc b/docs/reference/getting-started.asciidoc index b89021e1cfe5..3e44b2c41f67 100755 --- a/docs/reference/getting-started.asciidoc +++ b/docs/reference/getting-started.asciidoc @@ -93,7 +93,8 @@ Replication is important for two primary reasons: To summarize, each index can be split into multiple shards. An index can also be replicated zero (meaning no replicas) or more times. Once replicated, each index will have primary shards (the original shards that were replicated from) and replica shards (the copies of the primary shards). -The number of shards and replicas can be defined per index at the time the index is created. After the index is created, you may change the number of replicas dynamically anytime but you cannot change the number of shards after-the-fact. + +The number of shards and replicas can be defined per index at the time the index is created. After the index is created, you may also change the number of replicas dynamically anytime. You can change the number of shards for an existing index using the {ref}/indices-shrink-index.html[`_shrink`] and {ref}/indices-split-index.html[`_split`] APIs, however this is not a trivial task and pre-planning for the correct number of shards is the optimal approach. By default, each index in Elasticsearch is allocated one primary shard and one replica which means that if you have at least two nodes in your cluster, your index will have one primary shard and another replica shard (one complete replica) for a total of two shards per index. From 76aba8ad7bf4a5c8bce78fbefdfbd9d987294e92 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 17 Aug 2018 10:57:00 +0100 Subject: [PATCH 027/283] HLRC: Move ML request converters into their own class (#32906) --- .../client/MLRequestConverters.java | 78 ++++++++++++++++ .../client/MachineLearningClient.java | 12 +-- .../client/RequestConverters.java | 45 +--------- .../client/MLRequestConvertersTests.java | 90 +++++++++++++++++++ .../client/RequestConvertersTests.java | 29 ------ .../xpack/ml/job/config/AnalysisConfig.java | 4 + .../xpack/ml/job/config/Detector.java | 4 + .../protocol/xpack/ml/job/config/Job.java | 6 +- 8 files changed, 188 insertions(+), 80 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java 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 new file mode 100644 index 000000000000..e26a4c629a0b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client; + +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.client.RequestConverters.EndpointBuilder; +import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; +import org.elasticsearch.protocol.xpack.ml.PutJobRequest; + +import java.io.IOException; + +import static org.elasticsearch.client.RequestConverters.REQUEST_BODY_CONTENT_TYPE; +import static org.elasticsearch.client.RequestConverters.createEntity; + +final class MLRequestConverters { + + private MLRequestConverters() {} + + static Request putJob(PutJobRequest putJobRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(putJobRequest.getJob().getId()) + .build(); + Request request = new Request(HttpPut.METHOD_NAME, endpoint); + request.setEntity(createEntity(putJobRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + + static Request openJob(OpenJobRequest openJobRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(openJobRequest.getJobId()) + .addPathPartAsIs("_open") + .build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + request.setJsonEntity(openJobRequest.toString()); + return request; + } + + static Request deleteJob(DeleteJobRequest deleteJobRequest) { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(deleteJobRequest.getJobId()) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + + RequestConverters.Params params = new RequestConverters.Params(request); + params.putParam("force", Boolean.toString(deleteJobRequest.isForce())); + + return request; + } +} 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 2e7914e64abd..32b6cd6cf2c6 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 @@ -57,7 +57,7 @@ public final class MachineLearningClient { */ public PutJobResponse putJob(PutJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - RequestConverters::putMachineLearningJob, + MLRequestConverters::putJob, options, PutJobResponse::fromXContent, Collections.emptySet()); @@ -75,7 +75,7 @@ public PutJobResponse putJob(PutJobRequest request, RequestOptions options) thro */ public void putJobAsync(PutJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - RequestConverters::putMachineLearningJob, + MLRequestConverters::putJob, options, PutJobResponse::fromXContent, listener, @@ -95,7 +95,7 @@ public void putJobAsync(PutJobRequest request, RequestOptions options, ActionLis */ public DeleteJobResponse deleteJob(DeleteJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - RequestConverters::deleteMachineLearningJob, + MLRequestConverters::deleteJob, options, DeleteJobResponse::fromXContent, Collections.emptySet()); @@ -113,7 +113,7 @@ public DeleteJobResponse deleteJob(DeleteJobRequest request, RequestOptions opti */ public void deleteJobAsync(DeleteJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - RequestConverters::deleteMachineLearningJob, + MLRequestConverters::deleteJob, options, DeleteJobResponse::fromXContent, listener, @@ -138,7 +138,7 @@ public void deleteJobAsync(DeleteJobRequest request, RequestOptions options, Act */ public OpenJobResponse openJob(OpenJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - RequestConverters::machineLearningOpenJob, + MLRequestConverters::openJob, options, OpenJobResponse::fromXContent, Collections.emptySet()); @@ -160,7 +160,7 @@ public OpenJobResponse openJob(OpenJobRequest request, RequestOptions options) t */ public void openJobAsync(OpenJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - RequestConverters::machineLearningOpenJob, + MLRequestConverters::openJob, options, OpenJobResponse::fromXContent, listener, diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index c40b4893e014..0e5fce5b2272 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -112,9 +112,6 @@ import org.elasticsearch.protocol.xpack.license.GetLicenseRequest; import org.elasticsearch.protocol.xpack.license.PutLicenseRequest; import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; -import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; -import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; -import org.elasticsearch.protocol.xpack.ml.PutJobRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest; import org.elasticsearch.rest.action.search.RestSearchAction; @@ -1200,46 +1197,6 @@ static Request deleteLicense(DeleteLicenseRequest deleteLicenseRequest) { return request; } - static Request putMachineLearningJob(PutJobRequest putJobRequest) throws IOException { - String endpoint = new EndpointBuilder() - .addPathPartAsIs("_xpack") - .addPathPartAsIs("ml") - .addPathPartAsIs("anomaly_detectors") - .addPathPart(putJobRequest.getJob().getId()) - .build(); - Request request = new Request(HttpPut.METHOD_NAME, endpoint); - request.setEntity(createEntity(putJobRequest, REQUEST_BODY_CONTENT_TYPE)); - return request; - } - - static Request deleteMachineLearningJob(DeleteJobRequest deleteJobRequest) { - String endpoint = new EndpointBuilder() - .addPathPartAsIs("_xpack") - .addPathPartAsIs("ml") - .addPathPartAsIs("anomaly_detectors") - .addPathPart(deleteJobRequest.getJobId()) - .build(); - Request request = new Request(HttpDelete.METHOD_NAME, endpoint); - - Params params = new Params(request); - params.putParam("force", Boolean.toString(deleteJobRequest.isForce())); - - return request; - } - - static Request machineLearningOpenJob(OpenJobRequest openJobRequest) throws IOException { - String endpoint = new EndpointBuilder() - .addPathPartAsIs("_xpack") - .addPathPartAsIs("ml") - .addPathPartAsIs("anomaly_detectors") - .addPathPart(openJobRequest.getJobId()) - .addPathPartAsIs("_open") - .build(); - Request request = new Request(HttpPost.METHOD_NAME, endpoint); - request.setJsonEntity(openJobRequest.toString()); - return request; - } - static Request getMigrationAssistance(IndexUpgradeInfoRequest indexUpgradeInfoRequest) { EndpointBuilder endpointBuilder = new EndpointBuilder() .addPathPartAsIs("_xpack/migration/assistance") @@ -1251,7 +1208,7 @@ static Request getMigrationAssistance(IndexUpgradeInfoRequest indexUpgradeInfoRe return request; } - private static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException { + static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException { BytesRef source = XContentHelper.toXContent(toXContent, xContentType, false).toBytesRef(); return new ByteArrayEntity(source.bytes, source.offset, source.length, createContentType(xContentType)); } 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 new file mode 100644 index 000000000000..43a41960e003 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -0,0 +1,90 @@ +/* + * 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; + +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpPost; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; +import org.elasticsearch.protocol.xpack.ml.PutJobRequest; +import org.elasticsearch.protocol.xpack.ml.job.config.AnalysisConfig; +import org.elasticsearch.protocol.xpack.ml.job.config.Detector; +import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.elasticsearch.test.ESTestCase; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; + +public class MLRequestConvertersTests extends ESTestCase { + + public void testPutJob() throws IOException { + Job job = createValidJob("foo"); + PutJobRequest putJobRequest = new PutJobRequest(job); + + Request request = MLRequestConverters.putJob(putJobRequest); + + assertThat(request.getEndpoint(), equalTo("/_xpack/ml/anomaly_detectors/foo")); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) { + Job parsedJob = Job.PARSER.apply(parser, null).build(); + assertThat(parsedJob, equalTo(job)); + } + } + + public void testOpenJob() throws Exception { + String jobId = "some-job-id"; + OpenJobRequest openJobRequest = new OpenJobRequest(jobId); + openJobRequest.setTimeout(TimeValue.timeValueMinutes(10)); + + Request request = MLRequestConverters.openJob(openJobRequest); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_open", request.getEndpoint()); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + request.getEntity().writeTo(bos); + assertEquals(bos.toString("UTF-8"), "{\"job_id\":\""+ jobId +"\",\"timeout\":\"10m\"}"); + } + + public void testDeleteJob() { + String jobId = randomAlphaOfLength(10); + DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId); + + Request request = MLRequestConverters.deleteJob(deleteJobRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId, request.getEndpoint()); + assertEquals(Boolean.toString(false), request.getParameters().get("force")); + + deleteJobRequest.setForce(true); + request = MLRequestConverters.deleteJob(deleteJobRequest); + assertEquals(Boolean.toString(true), request.getParameters().get("force")); + } + + private static Job createValidJob(String jobId) { + AnalysisConfig.Builder analysisConfig = AnalysisConfig.builder(Collections.singletonList( + Detector.builder().setFunction("count").build())); + Job.Builder jobBuilder = Job.builder(jobId); + jobBuilder.setAnalysisConfig(analysisConfig); + return jobBuilder.build(); + } +} \ No newline at end of file diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 786cb94f8926..47195f0bb2ab 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -127,8 +127,6 @@ import org.elasticsearch.index.rankeval.RestRankEvalAction; import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; -import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; -import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest; import org.elasticsearch.repositories.fs.FsRepository; @@ -2612,33 +2610,6 @@ public void testXPackDeleteWatch() { assertThat(request.getEntity(), nullValue()); } - public void testDeleteMachineLearningJob() { - String jobId = randomAlphaOfLength(10); - DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId); - - Request request = RequestConverters.deleteMachineLearningJob(deleteJobRequest); - assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); - assertEquals("/_xpack/ml/anomaly_detectors/" + jobId, request.getEndpoint()); - assertEquals(Boolean.toString(false), request.getParameters().get("force")); - - deleteJobRequest.setForce(true); - request = RequestConverters.deleteMachineLearningJob(deleteJobRequest); - assertEquals(Boolean.toString(true), request.getParameters().get("force")); - } - - public void testPostMachineLearningOpenJob() throws Exception { - String jobId = "some-job-id"; - OpenJobRequest openJobRequest = new OpenJobRequest(jobId); - openJobRequest.setTimeout(TimeValue.timeValueMinutes(10)); - - Request request = RequestConverters.machineLearningOpenJob(openJobRequest); - assertEquals(HttpPost.METHOD_NAME, request.getMethod()); - assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_open", request.getEndpoint()); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - request.getEntity().writeTo(bos); - assertEquals(bos.toString("UTF-8"), "{\"job_id\":\""+ jobId +"\",\"timeout\":\"10m\"}"); - } - /** * Randomize the {@link FetchSourceContext} request parameters. */ diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfig.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfig.java index 00fa1bdd47fe..7baaae52a8bf 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfig.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfig.java @@ -300,6 +300,10 @@ public int hashCode() { multivariateByFields); } + public static Builder builder(List detectors) { + return new Builder(detectors); + } + public static class Builder { private List detectors; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Detector.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Detector.java index 3274b03877f1..042d48b70068 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Detector.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Detector.java @@ -265,6 +265,10 @@ public int hashCode() { excludeFrequent, rules, detectorIndex); } + public static Builder builder() { + return new Builder(); + } + public static class Builder { private String detectorDescription; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Job.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Job.java index 6bc1be3b5638..59840cfec2ae 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Job.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Job.java @@ -412,6 +412,10 @@ public final String toString() { return Strings.toString(this); } + public static Builder builder(String id) { + return new Builder(id); + } + public static class Builder { private String id; @@ -435,7 +439,7 @@ public static class Builder { private String resultsIndexName; private boolean deleted; - public Builder() { + private Builder() { } public Builder(String id) { From 2fa028cfa12067d81ac4bef875b5f8fa84bac5ac Mon Sep 17 00:00:00 2001 From: Andrey Ershov Date: Fri, 17 Aug 2018 12:36:45 +0200 Subject: [PATCH 028/283] Remove assertion in testDocStats on deletedDocs counter (#32914) testDocStats test is flaky and sometimes it's failing on jenkins and failure is not reproducible locally. The reason for this failure is in timing. If the number of deleted documents is greater than 33% of inserted documents, Lucene will schedule segments to merge if TieredMergePolicy is used (it's not the case for LogMergePolicy, but ES is only using TieredMergePolicy). If this merge is performed before stats are retrieved - we will get 0 for "deleted" counter. So basically this counter could be either 0 or numOfDeletedDocs at this point, but this is the too loose assertion and we decided to remove it at all. Closes #32766 --- .../java/org/elasticsearch/index/shard/IndexShardTests.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 47be03b99178..2228e1b017fd 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -2391,8 +2391,7 @@ public void testRecoverFromLocalShard() throws IOException { closeShards(sourceShard, targetShard); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32766") - public void testDocStats() throws IOException { + public void testDocStats() throws IOException, InterruptedException { IndexShard indexShard = null; try { indexShard = newStartedShard(); @@ -2441,8 +2440,6 @@ public void testDocStats() throws IOException { assertTrue(searcher.reader().numDocs() <= docStats.getCount()); } assertThat(docStats.getCount(), equalTo(numDocs)); - // Lucene will delete a segment if all docs are deleted from it; this means that we lose the deletes when deleting all docs - assertThat(docStats.getDeleted(), equalTo(numDocsToDelete == numDocs ? 0 : numDocsToDelete)); } // merge them away From ca54aacbb5a8603ec14916c4d65da69086dee8ae Mon Sep 17 00:00:00 2001 From: Paul Sanwald Date: Fri, 17 Aug 2018 07:03:25 -0400 Subject: [PATCH 029/283] Fix InternalAutoDateHistogram reproducible failure (#32723) Update test logic to correctly bucket intervals. --- .../AutoDateHistogramAggregationBuilder.java | 2 +- .../histogram/InternalAutoDateHistogram.java | 8 +- .../InternalAutoDateHistogramTests.java | 113 ++++++++++++++---- 3 files changed, 95 insertions(+), 28 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregationBuilder.java index 50a0c85c041c..b97670ddb573 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/AutoDateHistogramAggregationBuilder.java @@ -156,7 +156,7 @@ public int getNumBuckets() { return new AutoDateHistogramAggregatorFactory(name, config, numBuckets, roundings, context, parent, subFactoriesBuilder, metaData); } - private static Rounding createRounding(DateTimeUnit interval, DateTimeZone timeZone) { + static Rounding createRounding(DateTimeUnit interval, DateTimeZone timeZone) { Rounding.Builder tzRoundingBuilder = Rounding.builder(interval); if (timeZone != null) { tzRoundingBuilder.timeZone(timeZone); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java index 6a78ca672498..7bc2b9a31783 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogram.java @@ -418,7 +418,7 @@ private BucketReduceResult addEmptyBuckets(BucketReduceResult currentResult, Red return currentResult; } int roundingIdx = getAppropriateRounding(list.get(0).key, list.get(list.size() - 1).key, currentResult.roundingIdx, - bucketInfo.roundingInfos); + bucketInfo.roundingInfos, targetBuckets); RoundingInfo roundingInfo = bucketInfo.roundingInfos[roundingIdx]; Rounding rounding = roundingInfo.rounding; // merge buckets using the new rounding @@ -447,8 +447,8 @@ private BucketReduceResult addEmptyBuckets(BucketReduceResult currentResult, Red return new BucketReduceResult(list, roundingInfo, roundingIdx); } - private int getAppropriateRounding(long minKey, long maxKey, int roundingIdx, - RoundingInfo[] roundings) { + static int getAppropriateRounding(long minKey, long maxKey, int roundingIdx, + RoundingInfo[] roundings, int targetBuckets) { if (roundingIdx == roundings.length - 1) { return roundingIdx; } @@ -480,7 +480,7 @@ private int getAppropriateRounding(long minKey, long maxKey, int roundingIdx, currentKey = currentRounding.nextRoundingValue(currentKey); } currentRoundingIdx++; - } while (requiredBuckets > (targetBuckets * roundings[roundingIdx].getMaximumInnerInterval()) + } while (requiredBuckets > (targetBuckets * roundings[currentRoundingIdx - 1].getMaximumInnerInterval()) && currentRoundingIdx < roundings.length); // The loop will increase past the correct rounding index here so we // need to subtract one to get the rounding index we need diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java index 981d263d7d63..b7c5bf03ac55 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalAutoDateHistogramTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.aggregations.bucket.histogram; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.rounding.DateTimeUnit; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation; @@ -28,7 +29,11 @@ import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.test.InternalMultiBucketAggregationTestCase; import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -39,6 +44,8 @@ import static org.elasticsearch.common.unit.TimeValue.timeValueHours; import static org.elasticsearch.common.unit.TimeValue.timeValueMinutes; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; +import static org.elasticsearch.search.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder.createRounding; +import static org.hamcrest.Matchers.equalTo; public class InternalAutoDateHistogramTests extends InternalMultiBucketAggregationTestCase { @@ -61,6 +68,7 @@ protected InternalAutoDateHistogram createTestInstance(String name, int nbBuckets = randomNumberOfBuckets(); int targetBuckets = randomIntBetween(1, nbBuckets * 2 + 1); List buckets = new ArrayList<>(nbBuckets); + long startingDate = System.currentTimeMillis(); long interval = randomIntBetween(1, 3); @@ -72,23 +80,41 @@ protected InternalAutoDateHistogram createTestInstance(String name, } InternalAggregations subAggregations = new InternalAggregations(Collections.emptyList()); BucketInfo bucketInfo = new BucketInfo(roundingInfos, randomIntBetween(0, roundingInfos.length - 1), subAggregations); + return new InternalAutoDateHistogram(name, buckets, targetBuckets, bucketInfo, format, pipelineAggregators, metaData); + } + /* + This test was added to reproduce a bug where getAppropriateRounding was only ever using the first innerIntervals + passed in, instead of using the interval associated with the loop. + */ + public void testGetAppropriateRoundingUsesCorrectIntervals() { + RoundingInfo[] roundings = new RoundingInfo[6]; + DateTimeZone timeZone = DateTimeZone.UTC; + // Since we pass 0 as the starting index to getAppropriateRounding, we'll also use + // an innerInterval that is quite large, such that targetBuckets * roundings[i].getMaximumInnerInterval() + // will be larger than the estimate. + roundings[0] = new RoundingInfo(createRounding(DateTimeUnit.SECOND_OF_MINUTE, timeZone), + 1000L, 1000); + roundings[1] = new RoundingInfo(createRounding(DateTimeUnit.MINUTES_OF_HOUR, timeZone), + 60 * 1000L, 1, 5, 10, 30); + roundings[2] = new RoundingInfo(createRounding(DateTimeUnit.HOUR_OF_DAY, timeZone), + 60 * 60 * 1000L, 1, 3, 12); - return new InternalAutoDateHistogram(name, buckets, targetBuckets, bucketInfo, format, pipelineAggregators, metaData); + OffsetDateTime timestamp = Instant.parse("2018-01-01T00:00:01.000Z").atOffset(ZoneOffset.UTC); + // We want to pass a roundingIdx of zero, because in order to reproduce this bug, we need the function + // to increment the rounding (because the bug was that the function would not use the innerIntervals + // from the new rounding. + int result = InternalAutoDateHistogram.getAppropriateRounding(timestamp.toEpochSecond()*1000, + timestamp.plusDays(1).toEpochSecond()*1000, 0, roundings, 25); + assertThat(result, equalTo(2)); } @Override protected void assertReduced(InternalAutoDateHistogram reduced, List inputs) { - int roundingIdx = 0; - for (InternalAutoDateHistogram histogram : inputs) { - if (histogram.getBucketInfo().roundingIdx > roundingIdx) { - roundingIdx = histogram.getBucketInfo().roundingIdx; - } - } - RoundingInfo roundingInfo = roundingInfos[roundingIdx]; long lowest = Long.MAX_VALUE; long highest = 0; + for (InternalAutoDateHistogram histogram : inputs) { for (Histogram.Bucket bucket : histogram.getBuckets()) { long bucketKey = ((DateTime) bucket.getKey()).getMillis(); @@ -100,35 +126,72 @@ protected void assertReduced(InternalAutoDateHistogram reduced, List= 0; j--) { + int interval = roundingInfo.innerIntervals[j]; + if (normalizedDuration / interval < reduced.getBuckets().size()) { + innerIntervalToUse = interval; + innerIntervalIndex = j; + } + } + } + + long intervalInMillis = innerIntervalToUse * roundingInfo.getRoughEstimateDurationMillis(); + int bucketCount = getBucketCount(lowest, highest, roundingInfo, intervalInMillis); + + //Next, if our bucketCount is still above what we need, we'll go back and determine the interval + // based on a size calculation. + if (bucketCount > reduced.getBuckets().size()) { + for (int i = innerIntervalIndex; i < roundingInfo.innerIntervals.length; i++) { + long newIntervalMillis = roundingInfo.innerIntervals[i] * roundingInfo.getRoughEstimateDurationMillis(); + if (getBucketCount(lowest, highest, roundingInfo, newIntervalMillis) <= reduced.getBuckets().size()) { + innerIntervalToUse = roundingInfo.innerIntervals[i]; + intervalInMillis = innerIntervalToUse * roundingInfo.getRoughEstimateDurationMillis(); + } } } + Map expectedCounts = new TreeMap<>(); - long intervalInMillis = innerIntervalToUse*roundingInfo.getRoughEstimateDurationMillis(); for (long keyForBucket = roundingInfo.rounding.round(lowest); - keyForBucket <= highest; + keyForBucket <= roundingInfo.rounding.round(highest); keyForBucket = keyForBucket + intervalInMillis) { expectedCounts.put(keyForBucket, 0L); + // Iterate through the input buckets, and for each bucket, determine if it's inside + // the range of the bucket in the outer loop. if it is, add the doc count to the total + // for that bucket. + for (InternalAutoDateHistogram histogram : inputs) { for (Histogram.Bucket bucket : histogram.getBuckets()) { - long bucketKey = ((DateTime) bucket.getKey()).getMillis(); - long roundedBucketKey = roundingInfo.rounding.round(bucketKey); + long roundedBucketKey = roundingInfo.rounding.round(((DateTime) bucket.getKey()).getMillis()); + long docCount = bucket.getDocCount(); if (roundedBucketKey >= keyForBucket && roundedBucketKey < keyForBucket + intervalInMillis) { - long count = bucket.getDocCount(); expectedCounts.compute(keyForBucket, - (key, oldValue) -> (oldValue == null ? 0 : oldValue) + count); + (key, oldValue) -> (oldValue == null ? 0 : oldValue) + docCount); } } } } + // If there is only a single bucket, and we haven't added it above, add a bucket with no documents. + // this step is necessary because of the roundedBucketKey < keyForBucket + intervalInMillis above. + if (roundingInfo.rounding.round(lowest) == roundingInfo.rounding.round(highest) && expectedCounts.isEmpty()) { + expectedCounts.put(roundingInfo.rounding.round(lowest), 0L); + } + + // pick out the actual reduced values to the make the assertion more readable Map actualCounts = new TreeMap<>(); for (Histogram.Bucket bucket : reduced.getBuckets()) { actualCounts.compute(((DateTime) bucket.getKey()).getMillis(), @@ -137,12 +200,16 @@ protected void assertReduced(InternalAutoDateHistogram reduced, List instanceReader() { return InternalAutoDateHistogram::new; From 75014a22d726cc95cdd00d9cb5a59924049d56a6 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 17 Aug 2018 14:06:24 +0300 Subject: [PATCH 030/283] Enable FIPS140LicenseBootstrapCheck (#32903) This commit ensures that xpack.security.fips_mode.enabled: true cannot be set in a node that doesn't have the appropriate license. --- .../main/java/org/elasticsearch/xpack/security/Security.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 857b343b753c..02910b5dd74f 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 @@ -301,7 +301,8 @@ public Security(Settings settings, final Path configPath) { new TLSLicenseBootstrapCheck(), new FIPS140SecureSettingsBootstrapCheck(settings, env), new FIPS140JKSKeystoreBootstrapCheck(settings), - new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings))); + new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings), + new FIPS140LicenseBootstrapCheck(XPackSettings.FIPS_MODE_ENABLED.get(settings)))); checks.addAll(InternalRealms.getBootstrapChecks(settings, env)); this.bootstrapChecks = Collections.unmodifiableList(checks); Automatons.updateMaxDeterminizedStates(settings); From 0d92f377fd3ecc9c6336f4770fc92951368a2aec Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 17 Aug 2018 14:09:01 +0200 Subject: [PATCH 031/283] Tests: Fix timezone conversion in DateTimeUnitTests This fix prevernts trying to parse unknown timezone ids by converting the joda time zone via java.util.TimeZone to a java time based ZoneId. Closes #32927 --- .../org/elasticsearch/common/rounding/DateTimeUnitTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/common/rounding/DateTimeUnitTests.java b/server/src/test/java/org/elasticsearch/common/rounding/DateTimeUnitTests.java index 33ea83c75929..f188eb4cac6f 100644 --- a/server/src/test/java/org/elasticsearch/common/rounding/DateTimeUnitTests.java +++ b/server/src/test/java/org/elasticsearch/common/rounding/DateTimeUnitTests.java @@ -69,7 +69,7 @@ public void testEnumIds() { public void testConversion() { long millis = randomLongBetween(0, Instant.now().toEpochMilli()); DateTimeZone zone = randomDateTimeZone(); - ZoneId zoneId = ZoneId.of(zone.getID()); + ZoneId zoneId = zone.toTimeZone().toZoneId(); int offsetSeconds = zoneId.getRules().getOffset(Instant.ofEpochMilli(millis)).getTotalSeconds(); long parsedMillisJavaTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), zoneId) From a08127c072915b8598bbbf4ed7302aa737cddd6c Mon Sep 17 00:00:00 2001 From: Jonathan Little Date: Fri, 17 Aug 2018 05:11:18 -0700 Subject: [PATCH 032/283] Scripted metric aggregations: add deprecation warning and system property to control legacy params (#31597) * Scripted metric aggregations: add deprecation warning and system property to control legacy params Scripted metric aggregation params._agg/_aggs are replaced by state/states context variables. By default the old params are still present, and a deprecation warning is emitted when Scripted Metric Aggregations are used. A new system property can be used to disable the legacy params. This functionality will be removed in a future revision. * Fix minor style issue and docs test failure * Disable deprecated params._agg/_aggs in tests and revise tests to use state/states instead * Add integration test covering deprecated scripted metrics aggs params._agg/_aggs access * Disable deprecated params._agg/_aggs in docs integration tests and revise stored scripts to use state/states instead * Revert unnecessary migrations doc change A relevant note should be added in the changes destined for 7.0; this PR is going to be backported to 6.x. * Replace deprecated _agg param bwc integration test with a couple of unit tests * Fix compatibility test after merge * Rename backwards compatibility system property per code review feedback * Tweak deprecation warning text per review feedback --- .../elasticsearch/gradle/BuildPlugin.groovy | 2 + docs/build.gradle | 11 +- server/build.gradle | 12 + .../script/ScriptedMetricAggContexts.java | 21 ++ .../scripted/InternalScriptedMetric.java | 4 +- .../ScriptedMetricAggregatorFactory.java | 13 +- .../metrics/ScriptedMetricIT.java | 338 +++++++----------- ...alScriptedMetricAggStateV6CompatTests.java | 109 ++++++ .../scripted/InternalScriptedMetricTests.java | 4 +- ...MetricAggregatorAggStateV6CompatTests.java | 180 ++++++++++ .../ScriptedMetricAggregatorTests.java | 74 ++-- 11 files changed, 512 insertions(+), 256 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java create mode 100644 server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index f3f014f0e8aa..bf3ffcabe2f6 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -798,6 +798,8 @@ class BuildPlugin implements Plugin { systemProperty 'tests.task', path systemProperty 'tests.security.manager', 'true' systemProperty 'jna.nosys', 'true' + // TODO: remove this deprecation compatibility setting for 7.0 + systemProperty 'es.aggregations.enable_scripted_metric_agg_param', 'false' systemProperty 'compiler.java', project.ext.compilerJavaVersion.getMajorVersion() if (project.ext.inFipsJvm) { systemProperty 'runtime.java', project.ext.runtimeJavaVersion.getMajorVersion() + "FIPS" diff --git a/docs/build.gradle b/docs/build.gradle index 029147bba2ff..8ee5c8a8e539 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -41,6 +41,9 @@ 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 deprecation compatibility setting for 7.0 + systemProperty 'es.aggregations.enable_scripted_metric_agg_param', 'false' } // remove when https://github.com/elastic/elasticsearch/issues/31305 is fixed @@ -400,25 +403,25 @@ buildRestTests.setups['stored_scripted_metric_script'] = ''' - do: put_script: id: "my_init_script" - body: { "script": { "lang": "painless", "source": "params._agg.transactions = []" } } + body: { "script": { "lang": "painless", "source": "state.transactions = []" } } - match: { acknowledged: true } - do: put_script: id: "my_map_script" - body: { "script": { "lang": "painless", "source": "params._agg.transactions.add(doc.type.value == 'sale' ? doc.amount.value : -1 * doc.amount.value)" } } + body: { "script": { "lang": "painless", "source": "state.transactions.add(doc.type.value == 'sale' ? doc.amount.value : -1 * doc.amount.value)" } } - match: { acknowledged: true } - do: put_script: id: "my_combine_script" - body: { "script": { "lang": "painless", "source": "double profit = 0;for (t in params._agg.transactions) { profit += t; } return profit" } } + body: { "script": { "lang": "painless", "source": "double profit = 0;for (t in state.transactions) { profit += t; } return profit" } } - match: { acknowledged: true } - do: put_script: id: "my_reduce_script" - body: { "script": { "lang": "painless", "source": "double profit = 0;for (a in params._aggs) { profit += a; } return profit" } } + body: { "script": { "lang": "painless", "source": "double profit = 0;for (a in states) { profit += a; } return profit" } } - match: { acknowledged: true } ''' diff --git a/server/build.gradle b/server/build.gradle index 1964eddd03e9..b22a93a702c2 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -342,3 +342,15 @@ if (isEclipse == false || project.path == ":server-tests") { integTest.mustRunAfter test } +// TODO: remove these compatibility tests in 7.0 +additionalTest('testScriptedMetricAggParamsV6Compatibility') { + include '**/ScriptedMetricAggregatorAggStateV6CompatTests.class' + include '**/InternalScriptedMetricAggStateV6CompatTests.class' + systemProperty 'es.aggregations.enable_scripted_metric_agg_param', 'true' +} + +test { + // these are tested explicitly in separate test tasks + exclude '**/ScriptedMetricAggregatorAggStateV6CompatTests.class' + exclude '**/InternalScriptedMetricAggStateV6CompatTests.class' +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java b/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java index 774dc95d3997..0c34c59b7be5 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java @@ -22,6 +22,8 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.Scorer; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.logging.DeprecationLogger; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.search.lookup.LeafSearchLookup; import org.elasticsearch.search.lookup.SearchLookup; @@ -31,6 +33,25 @@ import java.util.Map; public class ScriptedMetricAggContexts { + private static final DeprecationLogger DEPRECATION_LOGGER = + new DeprecationLogger(Loggers.getLogger(ScriptedMetricAggContexts.class)); + + // Public for access from tests + public static final String AGG_PARAM_DEPRECATION_WARNING = + "params._agg/_aggs for scripted metric aggregations are deprecated, use state/states (not in params) instead. " + + "Use -Des.aggregations.enable_scripted_metric_agg_param=false to disable."; + + public static boolean deprecatedAggParamEnabled() { + boolean enabled = Boolean.parseBoolean( + System.getProperty("es.aggregations.enable_scripted_metric_agg_param", "true")); + + if (enabled) { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("enable_scripted_metric_agg_param", AGG_PARAM_DEPRECATION_WARNING); + } + + return enabled; + } + private abstract static class ParamsAndStateBase { private final Map params; private final Object state; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java index f4281c063ff2..4124a8eeb76a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java @@ -96,7 +96,9 @@ public InternalAggregation doReduce(List aggregations, Redu } // Add _aggs to params map for backwards compatibility (redundant with a context variable on the ReduceScript created below). - params.put("_aggs", aggregationObjects); + if (ScriptedMetricAggContexts.deprecatedAggParamEnabled()) { + params.put("_aggs", aggregationObjects); + } ScriptedMetricAggContexts.ReduceScript.Factory factory = reduceContext.scriptService().compile( firstAggregation.reduceScript, ScriptedMetricAggContexts.ReduceScript.CONTEXT); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java index 9bd904a07013..076c29feceae 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java @@ -83,10 +83,17 @@ public Aggregator createInternal(Aggregator parent, boolean collectsFromSingleBu // Add _agg to params map for backwards compatibility (redundant with context variables on the scripts created below). // When this is removed, aggState (as passed to ScriptedMetricAggregator) can be changed to Map, since // it won't be possible to completely replace it with another type as is possible when it's an entry in params. - if (aggParams.containsKey("_agg") == false) { - aggParams.put("_agg", new HashMap()); + Object aggState = new HashMap(); + if (ScriptedMetricAggContexts.deprecatedAggParamEnabled()) { + if (aggParams.containsKey("_agg") == false) { + // Add _agg if it wasn't added manually + aggParams.put("_agg", aggState); + } else { + // If it was added manually, also use it for the agg context variable to reduce the likelihood of + // weird behavior due to multiple different variables. + aggState = aggParams.get("_agg"); + } } - Object aggState = aggParams.get("_agg"); final ScriptedMetricAggContexts.InitScript initScript = this.initScript.newInstance( mergeParams(aggParams, initScriptParams), aggState); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java index 13e148979599..c000b7fb2289 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java @@ -67,6 +67,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.notNullValue; @@ -90,42 +91,57 @@ public static class CustomScriptPlugin extends MockScriptPlugin { protected Map, Object>> pluginScripts() { Map, Object>> scripts = new HashMap<>(); - scripts.put("_agg['count'] = 1", vars -> - aggScript(vars, agg -> ((Map) agg).put("count", 1))); + scripts.put("state['count'] = 1", vars -> + aggScript(vars, state -> state.put("count", 1))); - scripts.put("_agg.add(1)", vars -> - aggScript(vars, agg -> ((List) agg).add(1))); + scripts.put("state.list.add(1)", vars -> + aggScript(vars, state -> { + // Lazily populate state.list for tests without an init script + if (state.containsKey("list") == false) { + state.put("list", new ArrayList()); + } + + ((List) state.get("list")).add(1); + })); - scripts.put("_agg[param1] = param2", vars -> - aggScript(vars, agg -> ((Map) agg).put(XContentMapValues.extractValue("params.param1", vars), + scripts.put("state[param1] = param2", vars -> + aggScript(vars, state -> state.put((String) XContentMapValues.extractValue("params.param1", vars), XContentMapValues.extractValue("params.param2", vars)))); scripts.put("vars.multiplier = 3", vars -> ((Map) vars.get("vars")).put("multiplier", 3)); - scripts.put("_agg.add(vars.multiplier)", vars -> - aggScript(vars, agg -> ((List) agg).add(XContentMapValues.extractValue("vars.multiplier", vars)))); + scripts.put("state.list.add(vars.multiplier)", vars -> + aggScript(vars, state -> { + // Lazily populate state.list for tests without an init script + if (state.containsKey("list") == false) { + state.put("list", new ArrayList()); + } + + ((List) state.get("list")).add(XContentMapValues.extractValue("vars.multiplier", vars)); + })); // Equivalent to: // // newaggregation = []; // sum = 0; // - // for (a in _agg) { - // sum += a + // for (s in state.list) { + // sum += s // }; // // newaggregation.add(sum); // return newaggregation" // - scripts.put("sum agg values as a new aggregation", vars -> { + scripts.put("sum state values as a new aggregation", vars -> { List newAggregation = new ArrayList(); - List agg = (List) vars.get("_agg"); + Map state = (Map) vars.get("state"); + List list = (List) state.get("list"); - if (agg != null) { + if (list != null) { Integer sum = 0; - for (Object a : (List) agg) { - sum += ((Number) a).intValue(); + for (Object s : list) { + sum += ((Number) s).intValue(); } newAggregation.add(sum); } @@ -137,24 +153,41 @@ protected Map, Object>> pluginScripts() { // newaggregation = []; // sum = 0; // - // for (aggregation in _aggs) { - // for (a in aggregation) { - // sum += a + // for (state in states) { + // for (s in state) { + // sum += s // } // }; // // newaggregation.add(sum); // return newaggregation" // - scripts.put("sum aggs of agg values as a new aggregation", vars -> { + scripts.put("sum all states (lists) values as a new aggregation", vars -> { List newAggregation = new ArrayList(); Integer sum = 0; - List aggs = (List) vars.get("_aggs"); - for (Object aggregation : (List) aggs) { - if (aggregation != null) { - for (Object a : (List) aggregation) { - sum += ((Number) a).intValue(); + List> states = (List>) vars.get("states"); + for (List list : states) { + if (list != null) { + for (Object s : list) { + sum += ((Number) s).intValue(); + } + } + } + newAggregation.add(sum); + return newAggregation; + }); + + scripts.put("sum all states' state.list values as a new aggregation", vars -> { + List newAggregation = new ArrayList(); + Integer sum = 0; + + List> states = (List>) vars.get("states"); + for (Map state : states) { + List list = (List) state.get("list"); + if (list != null) { + for (Object s : list) { + sum += ((Number) s).intValue(); } } } @@ -167,25 +200,25 @@ protected Map, Object>> pluginScripts() { // newaggregation = []; // sum = 0; // - // for (aggregation in _aggs) { - // for (a in aggregation) { - // sum += a + // for (state in states) { + // for (s in state) { + // sum += s // } // }; // // newaggregation.add(sum * multiplier); // return newaggregation" // - scripts.put("multiplied sum aggs of agg values as a new aggregation", vars -> { + scripts.put("multiplied sum all states (lists) values as a new aggregation", vars -> { Integer multiplier = (Integer) vars.get("multiplier"); List newAggregation = new ArrayList(); Integer sum = 0; - List aggs = (List) vars.get("_aggs"); - for (Object aggregation : (List) aggs) { - if (aggregation != null) { - for (Object a : (List) aggregation) { - sum += ((Number) a).intValue(); + List> states = (List>) vars.get("states"); + for (List list : states) { + if (list != null) { + for (Object s : list) { + sum += ((Number) s).intValue(); } } } @@ -193,53 +226,12 @@ protected Map, Object>> pluginScripts() { return newAggregation; }); - scripts.put("state.items = new ArrayList()", vars -> - aggContextScript(vars, state -> ((HashMap) state).put("items", new ArrayList()))); - - scripts.put("state.items.add(1)", vars -> - aggContextScript(vars, state -> { - HashMap stateMap = (HashMap) state; - List items = (List) stateMap.get("items"); - items.add(1); - })); - - scripts.put("sum context state values", vars -> { - int sum = 0; - HashMap state = (HashMap) vars.get("state"); - List items = (List) state.get("items"); - - for (Object x : items) { - sum += (Integer)x; - } - - return sum; - }); - - scripts.put("sum context states", vars -> { - Integer sum = 0; - - List states = (List) vars.get("states"); - for (Object state : states) { - sum += ((Number) state).intValue(); - } - - return sum; - }); - return scripts; } - static Object aggScript(Map vars, Consumer fn) { - return aggScript(vars, fn, "_agg"); - } - - static Object aggContextScript(Map vars, Consumer fn) { - return aggScript(vars, fn, "state"); - } - @SuppressWarnings("unchecked") - private static Object aggScript(Map vars, Consumer fn, String stateVarName) { - T aggState = (T) vars.get(stateVarName); + static Map aggScript(Map vars, Consumer> fn) { + Map aggState = (Map) vars.get("state"); fn.accept(aggState); return aggState; } @@ -285,17 +277,17 @@ public void setupSuiteScopeCluster() throws Exception { assertAcked(client().admin().cluster().preparePutStoredScript() .setId("mapScript_stored") .setContent(new BytesArray("{\"script\": {\"lang\": \"" + MockScriptPlugin.NAME + "\"," + - " \"source\": \"_agg.add(vars.multiplier)\"} }"), XContentType.JSON)); + " \"source\": \"state.list.add(vars.multiplier)\"} }"), XContentType.JSON)); assertAcked(client().admin().cluster().preparePutStoredScript() .setId("combineScript_stored") .setContent(new BytesArray("{\"script\": {\"lang\": \"" + MockScriptPlugin.NAME + "\"," + - " \"source\": \"sum agg values as a new aggregation\"} }"), XContentType.JSON)); + " \"source\": \"sum state values as a new aggregation\"} }"), XContentType.JSON)); assertAcked(client().admin().cluster().preparePutStoredScript() .setId("reduceScript_stored") .setContent(new BytesArray("{\"script\": {\"lang\": \"" + MockScriptPlugin.NAME + "\"," + - " \"source\": \"sum aggs of agg values as a new aggregation\"} }"), XContentType.JSON)); + " \"source\": \"sum all states (lists) values as a new aggregation\"} }"), XContentType.JSON)); indexRandom(true, builders); ensureSearchable(); @@ -315,9 +307,10 @@ public void setUp() throws Exception { // the name of the file script is used in test method while the source of the file script // must match a predefined script from CustomScriptPlugin.pluginScripts() method Files.write(scripts.resolve("init_script.mockscript"), "vars.multiplier = 3".getBytes("UTF-8")); - Files.write(scripts.resolve("map_script.mockscript"), "_agg.add(vars.multiplier)".getBytes("UTF-8")); - Files.write(scripts.resolve("combine_script.mockscript"), "sum agg values as a new aggregation".getBytes("UTF-8")); - Files.write(scripts.resolve("reduce_script.mockscript"), "sum aggs of agg values as a new aggregation".getBytes("UTF-8")); + Files.write(scripts.resolve("map_script.mockscript"), "state.list.add(vars.multiplier)".getBytes("UTF-8")); + Files.write(scripts.resolve("combine_script.mockscript"), "sum state values as a new aggregation".getBytes("UTF-8")); + Files.write(scripts.resolve("reduce_script.mockscript"), + "sum all states (lists) values as a new aggregation".getBytes("UTF-8")); } catch (IOException e) { throw new RuntimeException("failed to create scripts"); } @@ -329,7 +322,7 @@ protected Path nodeConfigPath(int nodeOrdinal) { } public void testMap() { - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg['count'] = 1", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state['count'] = 1", Collections.emptyMap()); SearchResponse response = client().prepareSearch("idx") .setQuery(matchAllQuery()) @@ -365,52 +358,12 @@ public void testMap() { assertThat(numShardsRun, greaterThan(0)); } - public void testExplicitAggParam() { - Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); - - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(1)", Collections.emptyMap()); - - SearchResponse response = client().prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation(scriptedMetric("scripted").params(params).mapScript(mapScript)) - .get(); - assertSearchResponse(response); - assertThat(response.getHits().getTotalHits(), equalTo(numDocs)); - - Aggregation aggregation = response.getAggregations().get("scripted"); - assertThat(aggregation, notNullValue()); - assertThat(aggregation, instanceOf(ScriptedMetric.class)); - ScriptedMetric scriptedMetricAggregation = (ScriptedMetric) aggregation; - assertThat(scriptedMetricAggregation.getName(), equalTo("scripted")); - assertThat(scriptedMetricAggregation.aggregation(), notNullValue()); - assertThat(scriptedMetricAggregation.aggregation(), instanceOf(ArrayList.class)); - List aggregationList = (List) scriptedMetricAggregation.aggregation(); - assertThat(aggregationList.size(), equalTo(getNumShards("idx").numPrimaries)); - long totalCount = 0; - for (Object object : aggregationList) { - assertThat(object, notNullValue()); - assertThat(object, instanceOf(List.class)); - List list = (List) object; - for (Object o : list) { - assertThat(o, notNullValue()); - assertThat(o, instanceOf(Number.class)); - Number numberValue = (Number) o; - assertThat(numberValue, equalTo((Number) 1)); - totalCount += numberValue.longValue(); - } - } - assertThat(totalCount, equalTo(numDocs)); - } - - public void testMapWithParamsAndImplicitAggMap() { + public void testMapWithParams() { // Split the params up between the script and the aggregation. - // Don't put any _agg map in params. Map scriptParams = Collections.singletonMap("param1", "12"); Map aggregationParams = Collections.singletonMap("param2", 1); - // The _agg hashmap will be available even if not declared in the params map - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg[param1] = param2", scriptParams); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state[param1] = param2", scriptParams); SearchResponse response = client().prepareSearch("idx") .setQuery(matchAllQuery()) @@ -454,7 +407,6 @@ public void testInitMapWithParams() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); SearchResponse response = client() @@ -466,7 +418,7 @@ public void testInitMapWithParams() { .initScript( new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap())) .mapScript(new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, - "_agg.add(vars.multiplier)", Collections.emptyMap()))) + "state.list.add(vars.multiplier)", Collections.emptyMap()))) .get(); assertSearchResponse(response); assertThat(response.getHits().getTotalHits(), equalTo(numDocs)); @@ -483,8 +435,11 @@ public void testInitMapWithParams() { long totalCount = 0; for (Object object : aggregationList) { assertThat(object, notNullValue()); - assertThat(object, instanceOf(List.class)); - List list = (List) object; + assertThat(object, instanceOf(HashMap.class)); + Map map = (Map) object; + assertThat(map, hasKey("list")); + assertThat(map.get("list"), instanceOf(List.class)); + List list = (List) map.get("list"); for (Object o : list) { assertThat(o, notNullValue()); assertThat(o, instanceOf(Number.class)); @@ -501,12 +456,11 @@ public void testMapCombineWithParams() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(1)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(1)", Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); SearchResponse response = client() .prepareSearch("idx") @@ -553,13 +507,13 @@ public void testInitMapCombineWithParams() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); SearchResponse response = client() .prepareSearch("idx") @@ -607,15 +561,15 @@ public void testInitMapCombineReduceWithParams() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum aggs of agg values as a new aggregation", Collections.emptyMap()); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "sum all states (lists) values as a new aggregation", Collections.emptyMap()); SearchResponse response = client() .prepareSearch("idx") @@ -652,15 +606,15 @@ public void testInitMapCombineReduceGetProperty() throws Exception { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum aggs of agg values as a new aggregation", Collections.emptyMap()); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "sum all states (lists) values as a new aggregation", Collections.emptyMap()); SearchResponse searchResponse = client() .prepareSearch("idx") @@ -707,14 +661,14 @@ public void testMapCombineReduceWithParams() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum aggs of agg values as a new aggregation", Collections.emptyMap()); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "sum all states (lists) values as a new aggregation", Collections.emptyMap()); SearchResponse response = client() .prepareSearch("idx") @@ -749,13 +703,13 @@ public void testInitMapReduceWithParams() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum aggs of agg values as a new aggregation", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "sum all states' state.list values as a new aggregation", Collections.emptyMap()); SearchResponse response = client() .prepareSearch("idx") @@ -789,12 +743,12 @@ public void testMapReduceWithParams() { Map varsMap = new HashMap<>(); varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum aggs of agg values as a new aggregation", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "sum all states' state.list values as a new aggregation", Collections.emptyMap()); SearchResponse response = client() .prepareSearch("idx") @@ -828,18 +782,18 @@ public void testInitMapCombineReduceWithParamsAndReduceParams() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); Map reduceParams = new HashMap<>(); reduceParams.put("multiplier", 4); Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "multiplied sum aggs of agg values as a new aggregation", reduceParams); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "multiplied sum all states (lists) values as a new aggregation", reduceParams); SearchResponse response = client() .prepareSearch("idx") @@ -875,7 +829,6 @@ public void testInitMapCombineReduceWithParamsStored() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); SearchResponse response = client() @@ -916,15 +869,15 @@ public void testInitMapCombineReduceWithParamsAsSubAgg() { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum aggs of agg values as a new aggregation", Collections.emptyMap()); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "sum all states (lists) values as a new aggregation", Collections.emptyMap()); SearchResponse response = client() .prepareSearch("idx") @@ -977,15 +930,15 @@ public void testEmptyAggregation() throws Exception { varsMap.put("multiplier", 1); Map params = new HashMap<>(); - params.put("_agg", new ArrayList<>()); params.put("vars", varsMap); Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "vars.multiplier = 3", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(vars.multiplier)", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(vars.multiplier)", + Collections.emptyMap()); Script combineScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum agg values as a new aggregation", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum aggs of agg values as a new aggregation", Collections.emptyMap()); + new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum state values as a new aggregation", Collections.emptyMap()); + Script reduceScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, + "sum all states (lists) values as a new aggregation", Collections.emptyMap()); SearchResponse searchResponse = client().prepareSearch("empty_bucket_idx") .setQuery(matchAllQuery()) @@ -1021,7 +974,7 @@ public void testEmptyAggregation() throws Exception { * not using a script does get cached. */ public void testDontCacheScripts() throws Exception { - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg['count'] = 1", Collections.emptyMap()); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state['count'] = 1", Collections.emptyMap()); assertAcked(prepareCreate("cache_test_idx").addMapping("type", "d", "type=long") .setSettings(Settings.builder().put("requests.cache.enable", true).put("number_of_shards", 1).put("number_of_replicas", 1)) .get()); @@ -1047,7 +1000,7 @@ public void testDontCacheScripts() throws Exception { public void testConflictingAggAndScriptParams() { Map params = Collections.singletonMap("param1", "12"); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "_agg.add(1)", params); + Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.list.add(1)", params); SearchRequestBuilder builder = client().prepareSearch("idx") .setQuery(matchAllQuery()) @@ -1056,37 +1009,4 @@ public void testConflictingAggAndScriptParams() { SearchPhaseExecutionException ex = expectThrows(SearchPhaseExecutionException.class, builder::get); assertThat(ex.getCause().getMessage(), containsString("Parameter name \"param1\" used in both aggregation and script parameters")); } - - public void testAggFromContext() { - Script initScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.items = new ArrayList()", Collections.emptyMap()); - Script mapScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "state.items.add(1)", Collections.emptyMap()); - Script combineScript = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum context state values", Collections.emptyMap()); - Script reduceScript = - new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "sum context states", - Collections.emptyMap()); - - SearchResponse response = client() - .prepareSearch("idx") - .setQuery(matchAllQuery()) - .addAggregation( - scriptedMetric("scripted") - .initScript(initScript) - .mapScript(mapScript) - .combineScript(combineScript) - .reduceScript(reduceScript)) - .get(); - - Aggregation aggregation = response.getAggregations().get("scripted"); - assertThat(aggregation, notNullValue()); - assertThat(aggregation, instanceOf(ScriptedMetric.class)); - - ScriptedMetric scriptedMetricAggregation = (ScriptedMetric) aggregation; - assertThat(scriptedMetricAggregation.getName(), equalTo("scripted")); - assertThat(scriptedMetricAggregation.aggregation(), notNullValue()); - - assertThat(scriptedMetricAggregation.aggregation(), instanceOf(Integer.class)); - Integer aggResult = (Integer) scriptedMetricAggregation.aggregation(); - long totalAgg = aggResult.longValue(); - assertThat(totalAgg, equalTo(numDocs)); - } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java new file mode 100644 index 000000000000..4abf68a960b1 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java @@ -0,0 +1,109 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.metrics.scripted; + +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.script.MockScriptEngine; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptedMetricAggContexts; +import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.aggregations.Aggregation.CommonFields; +import org.elasticsearch.search.aggregations.ParsedAggregation; +import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; +import org.elasticsearch.test.InternalAggregationTestCase; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.sameInstance; + +/** + * This test verifies that the _aggs param is added correctly when the system property + * "es.aggregations.enable_scripted_metric_agg_param" is set to true. + */ +public class InternalScriptedMetricAggStateV6CompatTests extends InternalAggregationTestCase { + + private static final String REDUCE_SCRIPT_NAME = "reduceScript"; + + @Override + protected InternalScriptedMetric createTestInstance(String name, List pipelineAggregators, + Map metaData) { + Script reduceScript = new Script(ScriptType.INLINE, MockScriptEngine.NAME, REDUCE_SCRIPT_NAME, Collections.emptyMap()); + return new InternalScriptedMetric(name, "agg value", reduceScript, pipelineAggregators, metaData); + } + + /** + * Mock of the script service. The script that is run looks at the + * "_aggs" parameter to verify that it was put in place by InternalScriptedMetric. + */ + @Override + protected ScriptService mockScriptService() { + Function, Object> script = params -> { + Object aggs = params.get("_aggs"); + Object states = params.get("states"); + assertThat(aggs, instanceOf(List.class)); + assertThat(aggs, sameInstance(states)); + return aggs; + }; + + @SuppressWarnings("unchecked") + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, + Collections.singletonMap(REDUCE_SCRIPT_NAME, script)); + Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); + return new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); + } + + @Override + protected void assertReduced(InternalScriptedMetric reduced, List inputs) { + assertWarnings(ScriptedMetricAggContexts.AGG_PARAM_DEPRECATION_WARNING); + } + + @Override + protected Reader instanceReader() { + return InternalScriptedMetric::new; + } + + @Override + protected void assertFromXContent(InternalScriptedMetric aggregation, ParsedAggregation parsedAggregation) {} + + @Override + protected Predicate excludePathsFromXContentInsertion() { + return path -> path.contains(CommonFields.VALUE.getPreferredName()); + } + + @Override + protected InternalScriptedMetric mutateInstance(InternalScriptedMetric instance) { + String name = instance.getName(); + Object value = instance.aggregation(); + Script reduceScript = instance.reduceScript; + List pipelineAggregators = instance.pipelineAggregators(); + Map metaData = instance.getMetaData(); + return new InternalScriptedMetric(name + randomAlphaOfLength(5), value, reduceScript, pipelineAggregators, + metaData); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricTests.java index 584208af4177..70ddacf5698b 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricTests.java @@ -107,7 +107,7 @@ private static Object randomValue(Supplier[] valueTypes, int level) { /** * Mock of the script service. The script that is run looks at the - * "_aggs" parameter visible when executing the script and simply returns the count. + * "states" context variable visible when executing the script and simply returns the count. * This should be equal to the number of input InternalScriptedMetrics that are reduced * in total. */ @@ -116,7 +116,7 @@ protected ScriptService mockScriptService() { // mock script always retuns the size of the input aggs list as result @SuppressWarnings("unchecked") MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, - Collections.singletonMap(REDUCE_SCRIPT_NAME, script -> ((List) script.get("_aggs")).size())); + Collections.singletonMap(REDUCE_SCRIPT_NAME, script -> ((List) script.get("states")).size())); Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); return new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java new file mode 100644 index 000000000000..bf78cae711b9 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java @@ -0,0 +1,180 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.aggregations.metrics.scripted; + +import org.apache.lucene.document.SortedNumericDocValuesField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.store.Directory; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.script.MockScriptEngine; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptedMetricAggContexts; +import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.aggregations.AggregatorTestCase; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static java.util.Collections.singleton; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.sameInstance; + +/** + * This test verifies that the _agg param is added correctly when the system property + * "es.aggregations.enable_scripted_metric_agg_param" is set to true. + */ +public class ScriptedMetricAggregatorAggStateV6CompatTests extends AggregatorTestCase { + + private static final String AGG_NAME = "scriptedMetric"; + private static final Script INIT_SCRIPT = new Script(ScriptType.INLINE, MockScriptEngine.NAME, "initScript", Collections.emptyMap()); + private static final Script MAP_SCRIPT = new Script(ScriptType.INLINE, MockScriptEngine.NAME, "mapScript", Collections.emptyMap()); + private static final Script COMBINE_SCRIPT = new Script(ScriptType.INLINE, MockScriptEngine.NAME, "combineScript", + Collections.emptyMap()); + + private static final Script INIT_SCRIPT_EXPLICIT_AGG = new Script(ScriptType.INLINE, MockScriptEngine.NAME, + "initScriptExplicitAgg", Collections.emptyMap()); + private static final Script MAP_SCRIPT_EXPLICIT_AGG = new Script(ScriptType.INLINE, MockScriptEngine.NAME, + "mapScriptExplicitAgg", Collections.emptyMap()); + private static final Script COMBINE_SCRIPT_EXPLICIT_AGG = new Script(ScriptType.INLINE, MockScriptEngine.NAME, + "combineScriptExplicitAgg", Collections.emptyMap()); + private static final String EXPLICIT_AGG_OBJECT = "Explicit agg object"; + + private static final Map, Object>> SCRIPTS = new HashMap<>(); + + @BeforeClass + @SuppressWarnings("unchecked") + public static void initMockScripts() { + // If _agg is provided implicitly, it should be the same objects as "state" from the context. + SCRIPTS.put("initScript", params -> { + Object agg = params.get("_agg"); + Object state = params.get("state"); + assertThat(agg, instanceOf(Map.class)); + assertThat(agg, sameInstance(state)); + return agg; + }); + SCRIPTS.put("mapScript", params -> { + Object agg = params.get("_agg"); + Object state = params.get("state"); + assertThat(agg, instanceOf(Map.class)); + assertThat(agg, sameInstance(state)); + return agg; + }); + SCRIPTS.put("combineScript", params -> { + Object agg = params.get("_agg"); + Object state = params.get("state"); + assertThat(agg, instanceOf(Map.class)); + assertThat(agg, sameInstance(state)); + return agg; + }); + + SCRIPTS.put("initScriptExplicitAgg", params -> { + Object agg = params.get("_agg"); + assertThat(agg, equalTo(EXPLICIT_AGG_OBJECT)); + return agg; + }); + SCRIPTS.put("mapScriptExplicitAgg", params -> { + Object agg = params.get("_agg"); + assertThat(agg, equalTo(EXPLICIT_AGG_OBJECT)); + return agg; + }); + SCRIPTS.put("combineScriptExplicitAgg", params -> { + Object agg = params.get("_agg"); + assertThat(agg, equalTo(EXPLICIT_AGG_OBJECT)); + return agg; + }); + } + + /** + * Test that the _agg param is implicitly added + */ + public void testWithImplicitAggParam() throws IOException { + try (Directory directory = newDirectory()) { + Integer numDocs = 10; + try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) { + for (int i = 0; i < numDocs; i++) { + indexWriter.addDocument(singleton(new SortedNumericDocValuesField("number", i))); + } + } + try (IndexReader indexReader = DirectoryReader.open(directory)) { + ScriptedMetricAggregationBuilder aggregationBuilder = new ScriptedMetricAggregationBuilder(AGG_NAME); + aggregationBuilder.initScript(INIT_SCRIPT).mapScript(MAP_SCRIPT).combineScript(COMBINE_SCRIPT); + search(newSearcher(indexReader, true, true), new MatchAllDocsQuery(), aggregationBuilder); + } + } + + assertWarnings(ScriptedMetricAggContexts.AGG_PARAM_DEPRECATION_WARNING); + } + + /** + * Test that an explicitly added _agg param is honored + */ + public void testWithExplicitAggParam() throws IOException { + try (Directory directory = newDirectory()) { + Integer numDocs = 10; + try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) { + for (int i = 0; i < numDocs; i++) { + indexWriter.addDocument(singleton(new SortedNumericDocValuesField("number", i))); + } + } + + Map aggParams = new HashMap<>(); + aggParams.put("_agg", EXPLICIT_AGG_OBJECT); + + try (IndexReader indexReader = DirectoryReader.open(directory)) { + ScriptedMetricAggregationBuilder aggregationBuilder = new ScriptedMetricAggregationBuilder(AGG_NAME); + aggregationBuilder + .params(aggParams) + .initScript(INIT_SCRIPT_EXPLICIT_AGG) + .mapScript(MAP_SCRIPT_EXPLICIT_AGG) + .combineScript(COMBINE_SCRIPT_EXPLICIT_AGG); + search(newSearcher(indexReader, true, true), new MatchAllDocsQuery(), aggregationBuilder); + } + } + + assertWarnings(ScriptedMetricAggContexts.AGG_PARAM_DEPRECATION_WARNING); + } + + /** + * We cannot use Mockito for mocking QueryShardContext in this case because + * script-related methods (e.g. QueryShardContext#getLazyExecutableScript) + * is final and cannot be mocked + */ + @Override + protected QueryShardContext queryShardContextMock(MapperService mapperService) { + MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, SCRIPTS); + Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); + ScriptService scriptService = new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); + return new QueryShardContext(0, mapperService.getIndexSettings(), null, null, mapperService, null, scriptService, + xContentRegistry(), writableRegistry(), null, null, System::currentTimeMillis, null); + } +} diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorTests.java index 5124503fc036..65e42556461a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorTests.java @@ -83,72 +83,72 @@ public class ScriptedMetricAggregatorTests extends AggregatorTestCase { @SuppressWarnings("unchecked") public static void initMockScripts() { SCRIPTS.put("initScript", params -> { - Map agg = (Map) params.get("_agg"); - agg.put("collector", new ArrayList()); - return agg; - }); + Map state = (Map) params.get("state"); + state.put("collector", new ArrayList()); + return state; + }); SCRIPTS.put("mapScript", params -> { - Map agg = (Map) params.get("_agg"); - ((List) agg.get("collector")).add(1); // just add 1 for each doc the script is run on - return agg; + Map state = (Map) params.get("state"); + ((List) state.get("collector")).add(1); // just add 1 for each doc the script is run on + return state; }); SCRIPTS.put("combineScript", params -> { - Map agg = (Map) params.get("_agg"); - return ((List) agg.get("collector")).stream().mapToInt(Integer::intValue).sum(); + Map state = (Map) params.get("state"); + return ((List) state.get("collector")).stream().mapToInt(Integer::intValue).sum(); }); SCRIPTS.put("initScriptScore", params -> { - Map agg = (Map) params.get("_agg"); - agg.put("collector", new ArrayList()); - return agg; - }); + Map state = (Map) params.get("state"); + state.put("collector", new ArrayList()); + return state; + }); SCRIPTS.put("mapScriptScore", params -> { - Map agg = (Map) params.get("_agg"); - ((List) agg.get("collector")).add(((Number) params.get("_score")).doubleValue()); - return agg; + Map state = (Map) params.get("state"); + ((List) state.get("collector")).add(((Number) params.get("_score")).doubleValue()); + return state; }); SCRIPTS.put("combineScriptScore", params -> { - Map agg = (Map) params.get("_agg"); - return ((List) agg.get("collector")).stream().mapToDouble(Double::doubleValue).sum(); + Map state = (Map) params.get("state"); + return ((List) state.get("collector")).stream().mapToDouble(Double::doubleValue).sum(); }); SCRIPTS.put("initScriptParams", params -> { - Map agg = (Map) params.get("_agg"); + Map state = (Map) params.get("state"); Integer initialValue = (Integer)params.get("initialValue"); ArrayList collector = new ArrayList<>(); collector.add(initialValue); - agg.put("collector", collector); - return agg; + state.put("collector", collector); + return state; }); SCRIPTS.put("mapScriptParams", params -> { - Map agg = (Map) params.get("_agg"); + Map state = (Map) params.get("state"); Integer itemValue = (Integer) params.get("itemValue"); - ((List) agg.get("collector")).add(itemValue); - return agg; + ((List) state.get("collector")).add(itemValue); + return state; }); SCRIPTS.put("combineScriptParams", params -> { - Map agg = (Map) params.get("_agg"); + Map state = (Map) params.get("state"); int divisor = ((Integer) params.get("divisor")); - return ((List) agg.get("collector")).stream().mapToInt(Integer::intValue).map(i -> i / divisor).sum(); + return ((List) state.get("collector")).stream().mapToInt(Integer::intValue).map(i -> i / divisor).sum(); }); SCRIPTS.put("initScriptSelfRef", params -> { - Map agg = (Map) params.get("_agg"); - agg.put("collector", new ArrayList()); - agg.put("selfRef", agg); - return agg; + Map state = (Map) params.get("state"); + state.put("collector", new ArrayList()); + state.put("selfRef", state); + return state; }); SCRIPTS.put("mapScriptSelfRef", params -> { - Map agg = (Map) params.get("_agg"); - agg.put("selfRef", agg); - return agg; + Map state = (Map) params.get("state"); + state.put("selfRef", state); + return state; }); SCRIPTS.put("combineScriptSelfRef", params -> { - Map agg = (Map) params.get("_agg"); - agg.put("selfRef", agg); - return agg; + Map state = (Map) params.get("state"); + state.put("selfRef", state); + return state; }); } @@ -170,7 +170,7 @@ public void testNoDocs() throws IOException { } /** - * without combine script, the "_aggs" map should contain a list of the size of the number of documents matched + * without combine script, the "states" map should contain a list of the size of the number of documents matched */ public void testScriptedMetricWithoutCombine() throws IOException { try (Directory directory = newDirectory()) { From 9cec4aa14b7ea2a4283348df9d5a45cf1a5ab137 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 17 Aug 2018 07:21:17 -0500 Subject: [PATCH 033/283] [ML] fix updating opened jobs scheduled events (#31651) (#32881) * ML: fix updating opened jobs scheduled events (#31651) * Adding UpdateParamsTests license header * Adding integration test and addressing PR comments * addressing test and job names --- .../xpack/core/ml/job/config/JobUpdate.java | 2 +- .../core/ml/job/config/JobUpdateTests.java | 2 + .../job/process/autodetect/UpdateParams.java | 1 + .../process/autodetect/UpdateParamsTests.java | 45 ++++++++++ .../ml/integration/ScheduledEventsIT.java | 83 ++++++++++++++++++- 5 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParamsTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdate.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdate.java index 380f540a3178..cdfd9bad7f1d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdate.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdate.java @@ -258,7 +258,7 @@ public Version getJobVersion() { } public boolean isAutodetectProcessUpdate() { - return modelPlotConfig != null || detectorUpdates != null; + return modelPlotConfig != null || detectorUpdates != null || groups != null; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdateTests.java index c1f25bead224..9aedf61859d3 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobUpdateTests.java @@ -274,6 +274,8 @@ public void testIsAutodetectProcessUpdate() { assertTrue(update.isAutodetectProcessUpdate()); update = new JobUpdate.Builder("foo").setDetectorUpdates(Collections.singletonList(mock(JobUpdate.DetectorUpdate.class))).build(); assertTrue(update.isAutodetectProcessUpdate()); + update = new JobUpdate.Builder("foo").setGroups(Arrays.asList("bar")).build(); + assertTrue(update.isAutodetectProcessUpdate()); } public void testUpdateAnalysisLimitWithValueGreaterThanMax() { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParams.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParams.java index 127fb18e5fff..2d338890f9fa 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParams.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParams.java @@ -66,6 +66,7 @@ public static UpdateParams fromJobUpdate(JobUpdate jobUpdate) { return new Builder(jobUpdate.getJobId()) .modelPlotConfig(jobUpdate.getModelPlotConfig()) .detectorUpdates(jobUpdate.getDetectorUpdates()) + .updateScheduledEvents(jobUpdate.getGroups() != null) .build(); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParamsTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParamsTests.java new file mode 100644 index 000000000000..2683c1131f5b --- /dev/null +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/UpdateParamsTests.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.job.process.autodetect; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.ml.job.config.DetectionRule; +import org.elasticsearch.xpack.core.ml.job.config.JobUpdate; +import org.elasticsearch.xpack.core.ml.job.config.ModelPlotConfig; +import org.elasticsearch.xpack.core.ml.job.config.Operator; +import org.elasticsearch.xpack.core.ml.job.config.RuleCondition; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + + +public class UpdateParamsTests extends ESTestCase { + + public void testFromJobUpdate() { + String jobId = "foo"; + DetectionRule rule = new DetectionRule.Builder(Arrays.asList( + new RuleCondition(RuleCondition.AppliesTo.ACTUAL, + Operator.GT, 1.0))).build(); + List rules = Arrays.asList(rule); + List detectorUpdates = Collections.singletonList( + new JobUpdate.DetectorUpdate(2, null, rules)); + JobUpdate.Builder updateBuilder = new JobUpdate.Builder(jobId) + .setModelPlotConfig(new ModelPlotConfig()) + .setDetectorUpdates(detectorUpdates); + + UpdateParams params = UpdateParams.fromJobUpdate(updateBuilder.build()); + + assertFalse(params.isUpdateScheduledEvents()); + assertEquals(params.getDetectorUpdates(), updateBuilder.build().getDetectorUpdates()); + assertEquals(params.getModelPlotConfig(), updateBuilder.build().getModelPlotConfig()); + + params = UpdateParams.fromJobUpdate(updateBuilder.setGroups(Arrays.asList("bar")).build()); + + assertTrue(params.isUpdateScheduledEvents()); + } + +} diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java b/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java index 6703e4ef2365..fb261908e2c1 100644 --- a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java +++ b/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java @@ -12,11 +12,13 @@ import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.xpack.core.ml.action.GetBucketsAction; import org.elasticsearch.xpack.core.ml.action.GetRecordsAction; +import org.elasticsearch.xpack.core.ml.action.UpdateJobAction; import org.elasticsearch.xpack.core.ml.calendars.ScheduledEvent; import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig; import org.elasticsearch.xpack.core.ml.job.config.DataDescription; import org.elasticsearch.xpack.core.ml.job.config.Detector; import org.elasticsearch.xpack.core.ml.job.config.Job; +import org.elasticsearch.xpack.core.ml.job.config.JobUpdate; import org.elasticsearch.xpack.core.ml.job.results.AnomalyRecord; import org.elasticsearch.xpack.core.ml.job.results.Bucket; import org.junit.After; @@ -193,9 +195,9 @@ public void testScheduledEventWithInterimResults() throws IOException { /** * Test an open job picks up changes to scheduled events/calendars */ - public void testOnlineUpdate() throws Exception { + public void testAddEventsToOpenJob() throws Exception { TimeValue bucketSpan = TimeValue.timeValueMinutes(30); - Job.Builder job = createJob("scheduled-events-online-update", bucketSpan); + Job.Builder job = createJob("scheduled-events-add-events-to-open-job", bucketSpan); long startTime = 1514764800000L; final int bucketCount = 5; @@ -209,7 +211,7 @@ public void testOnlineUpdate() throws Exception { // Now create a calendar and events for the job while it is open String calendarId = "test-calendar-online-update"; - putCalendar(calendarId, Collections.singletonList(job.getId()), "testOnlineUpdate calendar"); + putCalendar(calendarId, Collections.singletonList(job.getId()), "testAddEventsToOpenJob calendar"); List events = new ArrayList<>(); long eventStartTime = startTime + (bucketCount + 1) * bucketSpan.millis(); @@ -257,6 +259,81 @@ public void testOnlineUpdate() throws Exception { assertEquals(0, buckets.get(8).getScheduledEvents().size()); } + /** + * An open job that later gets added to a calendar, should take the scheduled events into account + */ + public void testAddOpenedJobToGroupWithCalendar() throws Exception { + TimeValue bucketSpan = TimeValue.timeValueMinutes(30); + String groupName = "opened-calendar-job-group"; + Job.Builder job = createJob("scheduled-events-add-opened-job-to-group-with-calendar", bucketSpan); + + long startTime = 1514764800000L; + final int bucketCount = 5; + + // Open the job + openJob(job.getId()); + + // write some buckets of data + postData(job.getId(), generateData(startTime, bucketSpan, bucketCount, bucketIndex -> randomIntBetween(100, 200)) + .stream().collect(Collectors.joining())); + + String calendarId = "test-calendar-open-job-update"; + + // Create a new calendar referencing groupName + putCalendar(calendarId, Collections.singletonList(groupName), "testAddOpenedJobToGroupWithCalendar calendar"); + + // Put events in the calendar + List events = new ArrayList<>(); + long eventStartTime = startTime + (bucketCount + 1) * bucketSpan.millis(); + long eventEndTime = eventStartTime + (long)(1.5 * bucketSpan.millis()); + events.add(new ScheduledEvent.Builder().description("Some Event") + .startTime(ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventStartTime), ZoneOffset.UTC)) + .endTime(ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventEndTime), ZoneOffset.UTC)) + .calendarId(calendarId).build()); + + postScheduledEvents(calendarId, events); + + // Update the job to be a member of the group + UpdateJobAction.Request jobUpdateRequest = new UpdateJobAction.Request(job.getId(), + new JobUpdate.Builder(job.getId()).setGroups(Collections.singletonList(groupName)).build()); + client().execute(UpdateJobAction.INSTANCE, jobUpdateRequest).actionGet(); + + // Wait until the notification that the job was updated is indexed + assertBusy(() -> { + SearchResponse searchResponse = client().prepareSearch(".ml-notifications") + .setSize(1) + .addSort("timestamp", SortOrder.DESC) + .setQuery(QueryBuilders.boolQuery() + .filter(QueryBuilders.termQuery("job_id", job.getId())) + .filter(QueryBuilders.termQuery("level", "info")) + ).get(); + SearchHit[] hits = searchResponse.getHits().getHits(); + assertThat(hits.length, equalTo(1)); + assertThat(hits[0].getSourceAsMap().get("message"), equalTo("Job updated: [groups]")); + }); + + // write some more buckets of data that cover the scheduled event period + postData(job.getId(), generateData(startTime + bucketCount * bucketSpan.millis(), bucketSpan, 5, + bucketIndex -> randomIntBetween(100, 200)) + .stream().collect(Collectors.joining())); + // and close + closeJob(job.getId()); + + GetBucketsAction.Request getBucketsRequest = new GetBucketsAction.Request(job.getId()); + List buckets = getBuckets(getBucketsRequest); + + // the first 6 buckets have no events + for (int i=0; i<=bucketCount; i++) { + assertEquals(0, buckets.get(i).getScheduledEvents().size()); + } + // 7th and 8th buckets have the event but the last one does not + assertEquals(1, buckets.get(6).getScheduledEvents().size()); + assertEquals("Some Event", buckets.get(6).getScheduledEvents().get(0)); + assertEquals(1, buckets.get(7).getScheduledEvents().size()); + assertEquals("Some Event", buckets.get(7).getScheduledEvents().get(0)); + assertEquals(0, buckets.get(8).getScheduledEvents().size()); + } + private Job.Builder createJob(String jobId, TimeValue bucketSpan) { Detector.Builder detector = new Detector.Builder("count", null); AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(Collections.singletonList(detector.build())); From da6b61e8ef912fcb89c03e69f322ee517d879f06 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 17 Aug 2018 08:13:16 -0700 Subject: [PATCH 034/283] Make Geo Context Mapping Parsing More Strict (#32821) Currently, if geo context is represented by something other than geo_point or an object with lat and lon fields, the parsing of it as a geo context can result in ignoring the context altogether, returning confusing errors such as number_format_exception or trying to parse the number specifying as long-encoded hash code. It would also fail if the geo_point was stored. This commit makes the mapping parsing more strict and will fail during mapping update or index creation if the geo context doesn't point to a geo_point field. Supersedes #32412 Closes #32202 --- .../migration/migrate_7_0/search.asciidoc | 3 + .../index/mapper/MapperService.java | 3 + .../completion/context/ContextMapping.java | 29 +++++++++ .../completion/context/ContextMappings.java | 8 ++- .../completion/context/GeoContextMapping.java | 40 +++++++++++- .../ContextCompletionSuggestSearchIT.java | 20 +++++- .../completion/GeoContextMappingTests.java | 65 +++++++++++++++++++ 7 files changed, 161 insertions(+), 7 deletions(-) diff --git a/docs/reference/migration/migrate_7_0/search.asciidoc b/docs/reference/migration/migrate_7_0/search.asciidoc index 11f465091272..094294d85304 100644 --- a/docs/reference/migration/migrate_7_0/search.asciidoc +++ b/docs/reference/migration/migrate_7_0/search.asciidoc @@ -92,6 +92,9 @@ deprecated in 6.x, has been removed. Context enabled suggestion queries without contexts have to visit every suggestion, which degrades the search performance considerably. +For geo context the value of the `path` parameter is now validated against the mapping, +and the context is only accepted if `path` points to a field with `geo_point` type. + ==== Semantics changed for `max_concurrent_shard_requests` `max_concurrent_shard_requests` used to limit the total number of concurrent shard diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 921e472c94ff..9cd8ef1f6ac6 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -52,6 +52,7 @@ import org.elasticsearch.index.similarity.SimilarityService; import org.elasticsearch.indices.InvalidTypeNameException; import org.elasticsearch.indices.mapper.MapperRegistry; +import org.elasticsearch.search.suggest.completion.context.ContextMapping; import java.io.Closeable; import java.io.IOException; @@ -421,6 +422,8 @@ private synchronized Map internalMerge(@Nullable Documen MapperMergeValidator.validateFieldReferences(fieldMappers, fieldAliasMappers, fullPathObjectMappers, fieldTypes); + ContextMapping.validateContextPaths(indexSettings.getIndexVersionCreated(), fieldMappers, fieldTypes::get); + if (reason == MergeReason.MAPPING_UPDATE) { // this check will only be performed on the master node when there is // a call to the update mapping API. For all other cases like diff --git a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java index 1aa82eeb2190..0d0c7e945891 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMapping.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.suggest.completion.context; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.Version; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentFragment; @@ -28,6 +29,8 @@ import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.mapper.CompletionFieldMapper; +import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.ParseContext; import java.io.IOException; @@ -35,6 +38,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Function; /** * A {@link ContextMapping} defines criteria that can be used to @@ -131,6 +135,31 @@ public final List parseQueryContext(XContentParser parser) */ protected abstract XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException; + /** + * Checks if the current context is consistent with the rest of the fields. For example, the GeoContext + * should check that the field that it points to has the correct type. + */ + protected void validateReferences(Version indexVersionCreated, Function fieldResolver) { + // No validation is required by default + } + + /** + * Verifies that all field paths specified in contexts point to the fields with correct mappings + */ + public static void validateContextPaths(Version indexVersionCreated, List fieldMappers, + Function fieldResolver) { + for (FieldMapper fieldMapper : fieldMappers) { + if (CompletionFieldMapper.CONTENT_TYPE.equals(fieldMapper.typeName())) { + CompletionFieldMapper.CompletionFieldType fieldType = ((CompletionFieldMapper) fieldMapper).fieldType(); + if (fieldType.hasContextMappings()) { + for (ContextMapping context : fieldType.getContextMappings()) { + context.validateReferences(indexVersionCreated, fieldResolver); + } + } + } + } + } + @Override public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.field(FIELD_NAME, name); diff --git a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java index 3c0f0e80cebd..b4c3276b946b 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/ContextMappings.java @@ -37,6 +37,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -50,7 +51,7 @@ * and creates context queries for defined {@link ContextMapping}s * for a {@link CompletionFieldMapper} */ -public class ContextMappings implements ToXContent { +public class ContextMappings implements ToXContent, Iterable> { private final List> contextMappings; private final Map> contextNameMap; @@ -97,6 +98,11 @@ public void addField(ParseContext.Document document, String name, String input, document.add(new TypedContextField(name, input, weight, contexts, document)); } + @Override + public Iterator> iterator() { + return contextMappings.iterator(); + } + /** * Field prepends context values with a suggestion * Context values are associated with a type, denoted by diff --git a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java index 48aaf705099d..938c4963620e 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/completion/context/GeoContextMapping.java @@ -19,12 +19,17 @@ package org.elasticsearch.search.suggest.completion.context; +import org.apache.logging.log4j.LogManager; +import org.apache.lucene.document.LatLonDocValuesField; +import org.apache.lucene.document.LatLonPoint; import org.apache.lucene.document.StringField; import org.apache.lucene.index.DocValuesType; import org.apache.lucene.index.IndexableField; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.Version; import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.geo.GeoUtils; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -42,6 +47,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import static org.elasticsearch.common.geo.GeoHashUtils.addNeighbors; @@ -69,6 +75,8 @@ public class GeoContextMapping extends ContextMapping { static final String CONTEXT_PRECISION = "precision"; static final String CONTEXT_NEIGHBOURS = "neighbours"; + private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(LogManager.getLogger(GeoContextMapping.class)); + private final int precision; private final String fieldName; @@ -205,11 +213,11 @@ public Set parseContext(Document document) { for (IndexableField field : fields) { if (field instanceof StringField) { spare.resetFromString(field.stringValue()); - } else { - // todo return this to .stringValue() once LatLonPoint implements it + geohashes.add(spare.geohash()); + } else if (field instanceof LatLonPoint || field instanceof LatLonDocValuesField) { spare.resetFromIndexableField(field); + geohashes.add(spare.geohash()); } - geohashes.add(spare.geohash()); } } } @@ -279,6 +287,32 @@ public List toInternalQueryContexts(List return internalQueryContextList; } + @Override + protected void validateReferences(Version indexVersionCreated, Function fieldResolver) { + if (fieldName != null) { + MappedFieldType mappedFieldType = fieldResolver.apply(fieldName); + if (mappedFieldType == null) { + if (indexVersionCreated.before(Version.V_7_0_0_alpha1)) { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("geo_context_mapping", + "field [{}] referenced in context [{}] is not defined in the mapping", fieldName, name); + } else { + throw new ElasticsearchParseException( + "field [{}] referenced in context [{}] is not defined in the mapping", fieldName, name); + } + } else if (GeoPointFieldMapper.CONTENT_TYPE.equals(mappedFieldType.typeName()) == false) { + if (indexVersionCreated.before(Version.V_7_0_0_alpha1)) { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("geo_context_mapping", + "field [{}] referenced in context [{}] must be mapped to geo_point, found [{}]", + fieldName, name, mappedFieldType.typeName()); + } else { + throw new ElasticsearchParseException( + "field [{}] referenced in context [{}] must be mapped to geo_point, found [{}]", + fieldName, name, mappedFieldType.typeName()); + } + } + } + } + @Override public boolean equals(Object o) { if (this == o) return true; 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 d95db778a6a3..44c49ace5de8 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/ContextCompletionSuggestSearchIT.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/ContextCompletionSuggestSearchIT.java @@ -493,15 +493,24 @@ public void testGeoNeighbours() throws Exception { } public void testGeoField() throws Exception { -// Version version = VersionUtils.randomVersionBetween(random(), Version.V_2_0_0, Version.V_5_0_0_alpha5); -// Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder mapping = jsonBuilder(); mapping.startObject(); mapping.startObject(TYPE); mapping.startObject("properties"); + mapping.startObject("location"); + mapping.startObject("properties"); mapping.startObject("pin"); mapping.field("type", "geo_point"); + // Enable store and disable indexing sometimes + if (randomBoolean()) { + mapping.field("store", "true"); + } + if (randomBoolean()) { + mapping.field("index", "false"); + } + mapping.endObject(); // pin mapping.endObject(); + mapping.endObject(); // location mapping.startObject(FIELD); mapping.field("type", "completion"); mapping.field("analyzer", "simple"); @@ -510,7 +519,7 @@ public void testGeoField() throws Exception { mapping.startObject(); mapping.field("name", "st"); mapping.field("type", "geo"); - mapping.field("path", "pin"); + mapping.field("path", "location.pin"); mapping.field("precision", 5); mapping.endObject(); mapping.endArray(); @@ -524,7 +533,9 @@ public void testGeoField() throws Exception { XContentBuilder source1 = jsonBuilder() .startObject() + .startObject("location") .latlon("pin", 52.529172, 13.407333) + .endObject() .startObject(FIELD) .array("input", "Hotel Amsterdam in Berlin") .endObject() @@ -533,7 +544,9 @@ public void testGeoField() throws Exception { XContentBuilder source2 = jsonBuilder() .startObject() + .startObject("location") .latlon("pin", 52.363389, 4.888695) + .endObject() .startObject(FIELD) .array("input", "Hotel Berlin in Amsterdam") .endObject() @@ -600,6 +613,7 @@ public void assertSuggestions(String suggestionName, SuggestionBuilder suggestBu private void createIndexAndMapping(CompletionMappingBuilder completionMappingBuilder) throws IOException { createIndexAndMappingAndSettings(Settings.EMPTY, completionMappingBuilder); } + private void createIndexAndMappingAndSettings(Settings settings, CompletionMappingBuilder completionMappingBuilder) throws IOException { XContentBuilder mapping = jsonBuilder().startObject() .startObject(TYPE).startObject("properties") diff --git a/server/src/test/java/org/elasticsearch/search/suggest/completion/GeoContextMappingTests.java b/server/src/test/java/org/elasticsearch/search/suggest/completion/GeoContextMappingTests.java index 56ff157ec718..a745384eb3ed 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/completion/GeoContextMappingTests.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/completion/GeoContextMappingTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.search.suggest.completion; import org.apache.lucene.index.IndexableField; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -200,6 +201,70 @@ public void testIndexingWithMultipleContexts() throws Exception { assertContextSuggestFields(fields, 3); } + public void testMalformedGeoField() throws Exception { + XContentBuilder mapping = jsonBuilder(); + mapping.startObject(); + mapping.startObject("type1"); + mapping.startObject("properties"); + mapping.startObject("pin"); + String type = randomFrom("text", "keyword", "long"); + mapping.field("type", type); + mapping.endObject(); + mapping.startObject("suggestion"); + mapping.field("type", "completion"); + mapping.field("analyzer", "simple"); + + mapping.startArray("contexts"); + mapping.startObject(); + mapping.field("name", "st"); + mapping.field("type", "geo"); + mapping.field("path", "pin"); + mapping.field("precision", 5); + mapping.endObject(); + mapping.endArray(); + + mapping.endObject(); + + mapping.endObject(); + mapping.endObject(); + mapping.endObject(); + + ElasticsearchParseException ex = expectThrows(ElasticsearchParseException.class, + () -> createIndex("test", Settings.EMPTY, "type1", mapping)); + + assertThat(ex.getMessage(), equalTo("field [pin] referenced in context [st] must be mapped to geo_point, found [" + type + "]")); + } + + public void testMissingGeoField() throws Exception { + XContentBuilder mapping = jsonBuilder(); + mapping.startObject(); + mapping.startObject("type1"); + mapping.startObject("properties"); + mapping.startObject("suggestion"); + mapping.field("type", "completion"); + mapping.field("analyzer", "simple"); + + mapping.startArray("contexts"); + mapping.startObject(); + mapping.field("name", "st"); + mapping.field("type", "geo"); + mapping.field("path", "pin"); + mapping.field("precision", 5); + mapping.endObject(); + mapping.endArray(); + + mapping.endObject(); + + mapping.endObject(); + mapping.endObject(); + mapping.endObject(); + + ElasticsearchParseException ex = expectThrows(ElasticsearchParseException.class, + () -> createIndex("test", Settings.EMPTY, "type1", mapping)); + + assertThat(ex.getMessage(), equalTo("field [pin] referenced in context [st] is not defined in the mapping")); + } + public void testParsingQueryContextBasic() throws Exception { XContentBuilder builder = jsonBuilder().value("ezs42e44yx96"); XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(builder)); From e3aa68b0a9f72db7da5ec1fbba48da87edad6c7d Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 17 Aug 2018 18:23:13 +0300 Subject: [PATCH 035/283] [TEST] Run pre 6.4 nodes in non-FIPS JVMs (#32901) Elasticsearch versions earlier than 6.4.0 cannot properly run in a FIPS 140 JVM. This commit ensures that we use a non-FIPS JVM for nodes that we spin up in BWC tests even when we're testing FIPS. --- .../groovy/org/elasticsearch/gradle/test/NodeInfo.groovy | 6 ++++++ .../qa/full-cluster-restart/with-system-key/build.gradle | 8 -------- x-pack/qa/rolling-upgrade/with-system-key/build.gradle | 9 --------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/NodeInfo.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/NodeInfo.groovy index 0dd56b863324..aaf4e468182a 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/NodeInfo.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/NodeInfo.groovy @@ -177,6 +177,12 @@ class NodeInfo { javaVersion = 8 } else if (nodeVersion.onOrAfter("6.2.0") && nodeVersion.before("6.3.0")) { javaVersion = 9 + } else if (project.inFipsJvm && nodeVersion.onOrAfter("6.3.0") && nodeVersion.before("6.4.0")) { + /* + * Elasticsearch versions before 6.4.0 cannot be run in a FIPS-140 JVM. If we're running + * bwc tests in a FIPS-140 JVM, ensure that the pre v6.4.0 nodes use a Java 10 JVM instead. + */ + javaVersion = 10 } args.addAll("-E", "node.portsfile=true") diff --git a/x-pack/qa/full-cluster-restart/with-system-key/build.gradle b/x-pack/qa/full-cluster-restart/with-system-key/build.gradle index 928280b6584b..e69de29bb2d1 100644 --- a/x-pack/qa/full-cluster-restart/with-system-key/build.gradle +++ b/x-pack/qa/full-cluster-restart/with-system-key/build.gradle @@ -1,8 +0,0 @@ -import org.elasticsearch.gradle.test.RestIntegTestTask - -// Skip test on FIPS FIXME https://github.com/elastic/elasticsearch/issues/32737 -if (project.inFipsJvm) { - tasks.withType(RestIntegTestTask) { - enabled = false - } -} diff --git a/x-pack/qa/rolling-upgrade/with-system-key/build.gradle b/x-pack/qa/rolling-upgrade/with-system-key/build.gradle index 5aaa1ed1eff9..03505e01dedd 100644 --- a/x-pack/qa/rolling-upgrade/with-system-key/build.gradle +++ b/x-pack/qa/rolling-upgrade/with-system-key/build.gradle @@ -1,10 +1 @@ -import org.elasticsearch.gradle.test.RestIntegTestTask - -// Skip test on FIPS FIXME https://github.com/elastic/elasticsearch/issues/32737 -if (project.inFipsJvm) { - tasks.withType(RestIntegTestTask) { - enabled = false - } -} - group = "${group}.x-pack.qa.rolling-upgrade.with-system-key" From c5de9ec79d4e60714761765d4581de863ed97f64 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 17 Aug 2018 09:18:08 -0700 Subject: [PATCH 036/283] [DOCS] Splits the roles API documentation into multiple pages (#32794) --- x-pack/docs/build.gradle | 14 ++ x-pack/docs/en/rest-api/security.asciidoc | 10 +- .../security/clear-roles-cache.asciidoc | 39 ++++ .../rest-api/security/create-roles.asciidoc | 102 +++++++++ .../rest-api/security/delete-roles.asciidoc | 53 +++++ .../en/rest-api/security/get-roles.asciidoc | 85 +++++++ .../docs/en/rest-api/security/roles.asciidoc | 208 +----------------- .../xpack.security.clear_cached_roles.json | 2 +- .../api/xpack.security.delete_role.json | 2 +- .../api/xpack.security.get_role.json | 2 +- .../api/xpack.security.put_role.json | 2 +- 11 files changed, 311 insertions(+), 208 deletions(-) create mode 100644 x-pack/docs/en/rest-api/security/clear-roles-cache.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/create-roles.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/delete-roles.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/get-roles.asciidoc diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index 70f6061985a6..48ac4ba565e5 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -722,3 +722,17 @@ setups['sensor_prefab_data'] = ''' {"node.terms.value":"c","temperature.sum.value":202.0,"temperature.max.value":202.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":202.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":4.0,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516294800000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} ''' +setups['admin_role'] = ''' + - do: + xpack.security.put_role: + name: "my_admin_role" + body: > + { + "cluster": ["all"], + "indices": [ + {"names": ["index1", "index2" ], "privileges": ["all"], "field_security" : {"grant" : [ "title", "body" ]}} + ], + "run_as": [ "other_user" ], + "metadata" : {"version": 1} + } +''' diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index 227e343192a5..4d59cb22daa0 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -2,20 +2,26 @@ [[security-api]] == Security APIs +You can use the following APIs to perform {security} activities. + * <> * <> * <> -* <> * <> * <> * <> * <> +include::security/roles.asciidoc[] + include::security/authenticate.asciidoc[] include::security/change-password.asciidoc[] include::security/clear-cache.asciidoc[] +include::security/clear-roles-cache.asciidoc[] +include::security/create-roles.asciidoc[] +include::security/delete-roles.asciidoc[] +include::security/get-roles.asciidoc[] include::security/privileges.asciidoc[] -include::security/roles.asciidoc[] include::security/role-mapping.asciidoc[] include::security/ssl.asciidoc[] include::security/tokens.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/clear-roles-cache.asciidoc b/x-pack/docs/en/rest-api/security/clear-roles-cache.asciidoc new file mode 100644 index 000000000000..591d7eb2d11e --- /dev/null +++ b/x-pack/docs/en/rest-api/security/clear-roles-cache.asciidoc @@ -0,0 +1,39 @@ +[role="xpack"] +[[security-api-clear-role-cache]] +=== Clear roles cache API + +Evicts roles from the native role cache. + +==== Request + +`POST /_xpack/security/role//_clear_cache` + +==== Description + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + +==== Path Parameters + +`name`:: + (string) The name of the role. + + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster +privilege. + + +==== Examples + +The clear roles cache API evicts roles from the native role cache. For example, +to clear the cache for `my_admin_role`: + +[source,js] +-------------------------------------------------- +POST /_xpack/security/role/my_admin_role/_clear_cache +-------------------------------------------------- +// CONSOLE diff --git a/x-pack/docs/en/rest-api/security/create-roles.asciidoc b/x-pack/docs/en/rest-api/security/create-roles.asciidoc new file mode 100644 index 000000000000..749676b4e836 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/create-roles.asciidoc @@ -0,0 +1,102 @@ +[role="xpack"] +[[security-api-put-role]] +=== Create roles API + +Adds roles in the native realm. + +==== Request + +`POST /_xpack/security/role/` + + +`PUT /_xpack/security/role/` + + +==== Description + +The role API is generally the preferred way to manage roles, rather than using +file-based role management. For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + + +==== Path Parameters + +`name`:: + (string) The name of the role. + + +==== Request Body + +The following parameters can be specified in the body of a PUT or POST request +and pertain to adding a role: + +`cluster`:: (list) A list of cluster privileges. These privileges define the +cluster level actions that users with this role are able to execute. + +`indices`:: (list) A list of indices permissions entries. +`field_security`::: (list) The document fields that the owners of the role have +read access to. For more information, see +{stack-ov}/field-and-document-access-control.html[Setting up field and document level security]. +`names` (required)::: (list) A list of indices (or index name patterns) to which the +permissions in this entry apply. +`privileges`(required)::: (list) The index level privileges that the owners of the role +have on the specified indices. +`query`::: A search query that defines the documents the owners of the role have +read access to. A document within the specified indices must match this query in +order for it to be accessible by the owners of the role. + +`metadata`:: (object) Optional meta-data. Within the `metadata` object, keys +that begin with `_` are reserved for system usage. + +`run_as`:: (list) A list of users that the owners of this role can impersonate. +For more information, see +{stack-ov}/run-as-privilege.html[Submitting requests on behalf of other users]. + +For more information, see {stack-ov}/defining-roles.html[Defining roles]. + + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster +privilege. + + +==== Examples + +The following example adds a role called `my_admin_role`: + +[source,js] +-------------------------------------------------- +POST /_xpack/security/role/my_admin_role +{ + "cluster": ["all"], + "indices": [ + { + "names": [ "index1", "index2" ], + "privileges": ["all"], + "field_security" : { // optional + "grant" : [ "title", "body" ] + }, + "query": "{\"match\": {\"title\": \"foo\"}}" // optional + } + ], + "run_as": [ "other_user" ], // optional + "metadata" : { // optional + "version" : 1 + } +} +-------------------------------------------------- +// CONSOLE + +A successful call returns a JSON structure that shows whether the role has been +created or updated. + +[source,js] +-------------------------------------------------- +{ + "role": { + "created": true <1> + } +} +-------------------------------------------------- +// TESTRESPONSE +<1> When an existing role is updated, `created` is set to false. diff --git a/x-pack/docs/en/rest-api/security/delete-roles.asciidoc b/x-pack/docs/en/rest-api/security/delete-roles.asciidoc new file mode 100644 index 000000000000..db42493ca0fb --- /dev/null +++ b/x-pack/docs/en/rest-api/security/delete-roles.asciidoc @@ -0,0 +1,53 @@ +[role="xpack"] +[[security-api-delete-role]] +=== Delete roles API + +Removes roles in the native realm. + +==== Request + +`DELETE /_xpack/security/role/` + + +==== Description + +The Roles API is generally the preferred way to manage roles, rather than using +file-based role management. For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + + +==== Path Parameters + +`name`:: + (string) The name of the role. + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster +privilege. + + +==== Examples + +The following example deletes a `my_admin_role` role: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/security/role/my_admin_role +-------------------------------------------------- +// CONSOLE +// TEST[setup:admin_role] + +If the role is successfully deleted, the request returns `{"found": true}`. +Otherwise, `found` is set to false. + +[source,js] +-------------------------------------------------- +{ + "found" : true +} +-------------------------------------------------- +// TESTRESPONSE + diff --git a/x-pack/docs/en/rest-api/security/get-roles.asciidoc b/x-pack/docs/en/rest-api/security/get-roles.asciidoc new file mode 100644 index 000000000000..fa6e91b519b6 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/get-roles.asciidoc @@ -0,0 +1,85 @@ +[role="xpack"] +[[security-api-get-role]] +=== Get roles API + +Retrieves roles in the native realm. + +==== Request + +`GET /_xpack/security/role` + + +`GET /_xpack/security/role/` + + +==== Description + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + +==== Path Parameters + +`name`:: + (string) The name of the role. You can specify multiple roles as a + comma-separated list. If you do not specify this parameter, the API + returns information about all roles. + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster +privilege. + + +==== Examples + +The following example retrieves information about the `my_admin_role` role in +the native realm: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/role/my_admin_role +-------------------------------------------------- +// CONSOLE +// TEST[setup:admin_role] + +A successful call returns an array of roles with the JSON representation of the +role. If the role is not defined in the native realm, the request returns 404. + +[source,js] +-------------------------------------------------- +{ + "my_admin_role": { + "cluster" : [ "all" ], + "indices" : [ + { + "names" : [ "index1", "index2" ], + "privileges" : [ "all" ], + "field_security" : { + "grant" : [ "title", "body" ]} + } + ], + "applications" : [ ], + "run_as" : [ "other_user" ], + "metadata" : { + "version" : 1 + }, + "transient_metadata": { + "enabled": true + } + } +} +-------------------------------------------------- +// TESTRESPONSE + +To retrieve all roles, omit the role name: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/role +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +NOTE: If single role is requested, that role is returned as the response. When +requesting multiple roles, an object is returned holding the found roles, each +keyed by the relevant role name. diff --git a/x-pack/docs/en/rest-api/security/roles.asciidoc b/x-pack/docs/en/rest-api/security/roles.asciidoc index 28c09c560ec3..3853963081e3 100644 --- a/x-pack/docs/en/rest-api/security/roles.asciidoc +++ b/x-pack/docs/en/rest-api/security/roles.asciidoc @@ -1,205 +1,9 @@ -[role="xpack"] +[float] [[security-api-roles]] -=== Role Management APIs +=== Roles -The Roles API enables you to add, remove, and retrieve roles in the `native` -realm. +You can use the following APIs to add, remove, and retrieve roles in the native realm: -==== Request - -`GET /_xpack/security/role` + - -`GET /_xpack/security/role/` + - -`DELETE /_xpack/security/role/` + - -`POST /_xpack/security/role//_clear_cache` + - -`POST /_xpack/security/role/` + - -`PUT /_xpack/security/role/` - - -==== Description - -The Roles API is generally the preferred way to manage roles, rather than using -file-based role management. For more information, see -{xpack-ref}/authorization.html[Configuring Role-based Access Control]. - - -==== Path Parameters - -`name`:: - (string) The name of the role. If you do not specify this parameter, the - Get Roles API returns information about all roles. - - -==== Request Body - -The following parameters can be specified in the body of a PUT or POST request -and pertain to adding a role: - -`cluster`:: (list) A list of cluster privileges. These privileges define the -cluster level actions that users with this role are able to execute. - -`indices`:: (list) A list of indices permissions entries. -`field_security`::: (list) The document fields that the owners of the role have -read access to. For more information, see -{xpack-ref}/field-and-document-access-control.html[Setting Up Field and Document Level Security]. -`names` (required)::: (list) A list of indices (or index name patterns) to which the -permissions in this entry apply. -`privileges`(required)::: (list) The index level privileges that the owners of the role -have on the specified indices. -`query`::: A search query that defines the documents the owners of the role have -read access to. A document within the specified indices must match this query in -order for it to be accessible by the owners of the role. - -`metadata`:: (object) Optional meta-data. Within the `metadata` object, keys -that begin with `_` are reserved for system usage. - -`run_as`:: (list) A list of users that the owners of this role can impersonate. -For more information, see -{xpack-ref}/run-as-privilege.html[Submitting Requests on Behalf of Other Users]. - -For more information, see {xpack-ref}/defining-roles.html[Defining Roles]. - - -==== Authorization - -To use this API, you must have at least the `manage_security` cluster -privilege. - - -==== Examples - -[[security-api-put-role]] -To add a role, submit a PUT or POST request to the `/_xpack/security/role/` -endpoint: - -[source,js] --------------------------------------------------- -POST /_xpack/security/role/my_admin_role -{ - "cluster": ["all"], - "indices": [ - { - "names": [ "index1", "index2" ], - "privileges": ["all"], - "field_security" : { // optional - "grant" : [ "title", "body" ] - }, - "query": "{\"match\": {\"title\": \"foo\"}}" // optional - } - ], - "run_as": [ "other_user" ], // optional - "metadata" : { // optional - "version" : 1 - } -} --------------------------------------------------- -// CONSOLE - -A successful call returns a JSON structure that shows whether the role has been -created or updated. - -[source,js] --------------------------------------------------- -{ - "role": { - "created": true <1> - } -} --------------------------------------------------- -// TESTRESPONSE -<1> When an existing role is updated, `created` is set to false. - -[[security-api-get-role]] -To retrieve a role from the `native` Security realm, issue a GET request to the -`/_xpack/security/role/` endpoint: - -[source,js] --------------------------------------------------- -GET /_xpack/security/role/my_admin_role --------------------------------------------------- -// CONSOLE -// TEST[continued] - -A successful call returns an array of roles with the JSON representation of the -role. If the role is not defined in the `native` realm, the request 404s. - -[source,js] --------------------------------------------------- -{ - "my_admin_role": { - "cluster" : [ "all" ], - "indices" : [ { - "names" : [ "index1", "index2" ], - "privileges" : [ "all" ], - "field_security" : { - "grant" : [ "title", "body" ] - }, - "query" : "{\"match\": {\"title\": \"foo\"}}" - } ], - "applications" : [ ], - "run_as" : [ "other_user" ], - "metadata" : { - "version" : 1 - }, - "transient_metadata": { - "enabled": true - } - } -} --------------------------------------------------- -// TESTRESPONSE - -You can specify multiple roles as a comma-separated list. To retrieve all roles, -omit the role name. - -[source,js] --------------------------------------------------- -# Retrieve roles "r1", "r2", and "my_admin_role" -GET /_xpack/security/role/r1,r2,my_admin_role - -# Retrieve all roles -GET /_xpack/security/role --------------------------------------------------- -// CONSOLE -// TEST[continued] - -NOTE: If single role is requested, that role is returned as the response. When -requesting multiple roles, an object is returned holding the found roles, each -keyed by the relevant role name. - -[[security-api-delete-role]] -To delete a role, submit a DELETE request to the `/_xpack/security/role/` -endpoint: - -[source,js] --------------------------------------------------- -DELETE /_xpack/security/role/my_admin_role --------------------------------------------------- -// CONSOLE -// TEST[continued] - -If the role is successfully deleted, the request returns `{"found": true}`. -Otherwise, `found` is set to false. - -[source,js] --------------------------------------------------- -{ - "found" : true -} --------------------------------------------------- -// TESTRESPONSE - -[[security-api-clear-role-cache]] -The Clear Roles Cache API evicts roles from the native role cache. To clear the -cache for a role, submit a POST request `/_xpack/security/role//_clear_cache` -endpoint: - -[source,js] --------------------------------------------------- -POST /_xpack/security/role/my_admin_role/_clear_cache --------------------------------------------------- -// CONSOLE +* <>, <> +* <> +* <> \ No newline at end of file diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.clear_cached_roles.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.clear_cached_roles.json index c94333325b12..d945ebe3247e 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.clear_cached_roles.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.clear_cached_roles.json @@ -1,6 +1,6 @@ { "xpack.security.clear_cached_roles": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-clear-role-cache", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-role-cache.html", "methods": [ "POST" ], "url": { "path": "/_xpack/security/role/{name}/_clear_cache", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json index 4351b1bc847a..881105d60b8b 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role.json @@ -1,6 +1,6 @@ { "xpack.security.delete_role": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-delete-role", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-role.html", "methods": [ "DELETE" ], "url": { "path": "/_xpack/security/role/{name}", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role.json index 3479c911ccdc..67bdbb8a911a 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role.json @@ -1,6 +1,6 @@ { "xpack.security.get_role": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-get-role", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-role.html", "methods": [ "GET" ], "url": { "path": "/_xpack/security/role/{name}", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role.json index 4152975189e2..63ef5ee37867 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role.json @@ -1,6 +1,6 @@ { "xpack.security.put_role": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-roles.html#security-api-put-role", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role.html", "methods": [ "PUT", "POST" ], "url": { "path": "/_xpack/security/role/{name}", From 46c35db1dfb7fa59f47a420545ab76f342ad701d Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 17 Aug 2018 17:52:29 +0100 Subject: [PATCH 037/283] [ML][TEST] Fix BasicRenormalizationIT after adding multibucket feature As the multibucket feature was merged in, this test hit a side effect which means buckets trailing an anomaly could become anomalous. This commit fixes the problem by filtering low score records when we request them. --- .../xpack/ml/integration/BasicRenormalizationIT.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java b/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java index 80afdeff82ad..cc5a9f4f1b46 100644 --- a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java +++ b/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java @@ -7,6 +7,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.core.ml.action.GetJobsStatsAction; +import org.elasticsearch.xpack.core.ml.action.GetRecordsAction; import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig; import org.elasticsearch.xpack.core.ml.job.config.DataDescription; import org.elasticsearch.xpack.core.ml.job.config.Detector; @@ -36,7 +37,11 @@ public void testDefaultRenormalization() throws Exception { String jobId = "basic-renormalization-it-test-default-renormalization-job"; createAndRunJob(jobId, null); - List records = getRecords(jobId); + GetRecordsAction.Request getRecordsRequest = new GetRecordsAction.Request(jobId); + // Setting the record score to 10.0, to avoid the low score records due to multibucket trailing effect + getRecordsRequest.setRecordScore(10.0); + + List records = getRecords(getRecordsRequest); assertThat(records.size(), equalTo(2)); AnomalyRecord laterRecord = records.get(0); assertThat(laterRecord.getActual().get(0), equalTo(100.0)); From a608205510519c3ec7a8554c1f8f16744a6ccfce Mon Sep 17 00:00:00 2001 From: lcawl Date: Fri, 17 Aug 2018 10:20:28 -0700 Subject: [PATCH 038/283] [DOCS] Fixes links to role management APIs --- x-pack/docs/en/security/authorization/managing-roles.asciidoc | 2 +- x-pack/docs/en/security/authorization/mapping-roles.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/docs/en/security/authorization/managing-roles.asciidoc b/x-pack/docs/en/security/authorization/managing-roles.asciidoc index b8a01aa4519c..aa872a88ce0a 100644 --- a/x-pack/docs/en/security/authorization/managing-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/managing-roles.asciidoc @@ -130,7 +130,7 @@ manage roles, log in to {kib} and go to *Management / Elasticsearch / Roles*. The _Role Management APIs_ enable you to add, update, remove and retrieve roles dynamically. When you use the APIs to manage roles in the `native` realm, the roles are stored in an internal {es} index. For more information and examples, -see {ref}/security-api-roles.html[Role Management APIs]. +see {ref}/security-api.html#security-api-roles.html[role management APIs]. [float] [[roles-management-file]] diff --git a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc index cf8373a65f33..9626571dbd5b 100644 --- a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc @@ -18,7 +18,7 @@ the API, and other roles that are mapped through files. When you use role-mappings, you assign existing roles to users. The available roles should either be added using the -{ref}/security-api-roles.html[Role Management APIs] or defined in the +{ref}/security-api.html#security-api-roles.html[role management APIs] or defined in the <>. Either role-mapping method can use either role management method. For example, when you use the role mapping API, you are able to map users to both API-managed roles and file-managed roles From 899e94a29bc8a4e7a9497caf12e5ec153d8f8870 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Fri, 17 Aug 2018 13:33:12 -0400 Subject: [PATCH 039/283] [Docs] Tweaks and fixes to rollup docs - Missing links to new IndexCaps API - Incorrect security permissions on IndexCaps API - GetJobs API must supply a job (or `_all`), omitting throws error - Link to search/agg limitations from RollupSearch API - Tweak URLs in quick reference - Formatting of overview page --- x-pack/docs/en/rest-api/rollup-api.asciidoc | 2 ++ .../docs/en/rest-api/rollup/rollup-caps.asciidoc | 4 ++-- .../en/rest-api/rollup/rollup-index-caps.asciidoc | 4 +--- .../en/rest-api/rollup/rollup-job-config.asciidoc | 6 ++++++ .../en/rest-api/rollup/rollup-search.asciidoc | 2 +- x-pack/docs/en/rollup/api-quickref.asciidoc | 15 ++++++++------- x-pack/docs/en/rollup/overview.asciidoc | 4 ++++ 7 files changed, 24 insertions(+), 13 deletions(-) diff --git a/x-pack/docs/en/rest-api/rollup-api.asciidoc b/x-pack/docs/en/rest-api/rollup-api.asciidoc index f1cd7c285a73..9a8ec00d77a0 100644 --- a/x-pack/docs/en/rest-api/rollup-api.asciidoc +++ b/x-pack/docs/en/rest-api/rollup-api.asciidoc @@ -16,6 +16,7 @@ === Data * <> +* <> [float] [[rollup-search-endpoint]] @@ -31,5 +32,6 @@ include::rollup/put-job.asciidoc[] include::rollup/start-job.asciidoc[] include::rollup/stop-job.asciidoc[] include::rollup/rollup-caps.asciidoc[] +include::rollup/rollup-index-caps.asciidoc[] include::rollup/rollup-search.asciidoc[] include::rollup/rollup-job-config.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/rollup/rollup-caps.asciidoc b/x-pack/docs/en/rest-api/rollup/rollup-caps.asciidoc index f770adf1f0d1..1f233f195a09 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-caps.asciidoc +++ b/x-pack/docs/en/rest-api/rollup/rollup-caps.asciidoc @@ -27,8 +27,8 @@ live? ==== Path Parameters `index`:: - (string) Index, indices or index-pattern to return rollup capabilities for. If omitted (or `_all` is used) all available - rollup job capabilities will be returned + (string) Index, indices or index-pattern to return rollup capabilities for. `_all` may be used to fetch + rollup capabilities from all jobs ==== Request Body diff --git a/x-pack/docs/en/rest-api/rollup/rollup-index-caps.asciidoc b/x-pack/docs/en/rest-api/rollup/rollup-index-caps.asciidoc index 4636d9775e9d..e5ca70cd59cd 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-index-caps.asciidoc +++ b/x-pack/docs/en/rest-api/rollup/rollup-index-caps.asciidoc @@ -26,15 +26,13 @@ This API will allow you to determine: `index`:: (string) Index or index-pattern of concrete rollup indices to check for capabilities. - - ==== Request Body There is no request body for the Get Jobs API. ==== Authorization -You must have `monitor`, `monitor_rollup`, `manage` or `manage_rollup` cluster privileges to use this API. +You must have the `read` index privilege on the index that stores the rollup results. For more information, see {xpack-ref}/security-privileges.html[Security Privileges]. diff --git a/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc b/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc index ef0ea6f00f7c..2ba92b6b59ea 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc +++ b/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc @@ -82,6 +82,12 @@ In the above example, there are several pieces of logistical configuration for t will tend to execute faster, but will require more memory during processing. This has no effect on how the data is rolled up, it is merely used for tweaking the speed/memory cost of the indexer. +[NOTE] +The `index_pattern` cannot be a pattern that would also match the destination `rollup_index`. E.g. the pattern +`"foo-*"` would match the rollup index `"foo-rollup"`. This causes problems because the rollup job would attempt +to rollup it's own data at runtime. If you attempt to configure a pattern that matches the `rollup_index`, an exception +will be thrown to prevent this behavior. + [[rollup-groups-config]] ==== Grouping Config diff --git a/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc b/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc index 470cbc4eaf57..f595d52ec10a 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc +++ b/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc @@ -34,7 +34,7 @@ or using `_all`, is not permitted The request body supports a subset of features from the regular Search API. It supports: -- `query` param for specifying an DSL query, subject to some limitations +- `query` param for specifying an DSL query, subject to some limitations (see <> and <> - `aggregations` param for specifying aggregations Functionality that is not available: diff --git a/x-pack/docs/en/rollup/api-quickref.asciidoc b/x-pack/docs/en/rollup/api-quickref.asciidoc index 937c6a84e5e1..5e99f1c69841 100644 --- a/x-pack/docs/en/rollup/api-quickref.asciidoc +++ b/x-pack/docs/en/rollup/api-quickref.asciidoc @@ -15,18 +15,19 @@ Most {rollup} endpoints have the following base: [[rollup-api-jobs]] === /job/ -* {ref}/rollup-put-job.html[PUT /job/+++]: Create a job -* {ref}/rollup-get-job.html[GET /job]: List jobs -* {ref}/rollup-get-job.html[GET /job/+++]: Get job details -* {ref}/rollup-start-job.html[POST /job//_start]: Start a job -* {ref}/rollup-stop-job.html[POST /job//_stop]: Stop a job -* {ref}/rollup-delete-job.html[DELETE /job/+++]: Delete a job +* {ref}/rollup-put-job.html[PUT /_xpack/rollup/job/+++]: Create a job +* {ref}/rollup-get-job.html[GET /_xpack/rollup/job]: List jobs +* {ref}/rollup-get-job.html[GET /_xpack/rollup/job/+++]: Get job details +* {ref}/rollup-start-job.html[POST /_xpack/rollup/job//_start]: Start a job +* {ref}/rollup-stop-job.html[POST /_xpack/rollup/job//_stop]: Stop a job +* {ref}/rollup-delete-job.html[DELETE /_xpack/rollup/job/+++]: Delete a job [float] [[rollup-api-data]] === /data/ -* {ref}/rollup-get-rollup-caps.html[GET /data//_rollup_caps+++]: Get Rollup Capabilities +* {ref}/rollup-get-rollup-caps.html[GET /_xpack/rollup/data//_rollup_caps+++]: Get Rollup Capabilities +* {ref}/rollup-get-rollup-index-caps.html[GET //_rollup/data/+++]: Get Rollup Index Capabilities [float] [[rollup-api-index]] diff --git a/x-pack/docs/en/rollup/overview.asciidoc b/x-pack/docs/en/rollup/overview.asciidoc index a3f29f23bd10..a9a983fbecc1 100644 --- a/x-pack/docs/en/rollup/overview.asciidoc +++ b/x-pack/docs/en/rollup/overview.asciidoc @@ -20,6 +20,7 @@ So while the cost of storing a millisecond of sensor data from ten years ago is reading often diminishes with time. It's not useless -- it could easily contribute to a useful analysis -- but it's reduced value often leads to deletion rather than paying the fixed storage cost. +[float] === Rollup store historical data at reduced granularity That's where Rollup comes into play. The Rollup functionality summarizes old, high-granularity data into a reduced @@ -35,6 +36,7 @@ automates this process of summarizing historical data. Details about setting up and configuring Rollup are covered in <> +[float] === Rollup uses standard query DSL The Rollup feature exposes a new search endpoint (`/_rollup_search` vs the standard `/_search`) which knows how to search @@ -48,6 +50,7 @@ are covered more in <>. But if your queries, aggregations and dashboards only use the available functionality, redirecting them to historical data is trivial. +[float] === Rollup merges "live" and "rolled" data A useful feature of Rollup is the ability to query both "live", realtime data in addition to historical "rolled" data @@ -61,6 +64,7 @@ would only see data older than a month. The RollupSearch endpoint, however, sup It will take the results from both data sources and merge them together. If there is overlap between the "live" and "rolled" data, live data is preferred to increase accuracy. +[float] === Rollup is multi-interval aware Finally, Rollup is capable of intelligently utilizing the best interval available. If you've worked with summarizing From 967b1785fab89fc735791e64ec23cbd993a5e989 Mon Sep 17 00:00:00 2001 From: lcawl Date: Fri, 17 Aug 2018 10:41:06 -0700 Subject: [PATCH 040/283] [DOCS] Fixes more broken links to role management APIs --- x-pack/docs/en/security/authorization/managing-roles.asciidoc | 2 +- x-pack/docs/en/security/authorization/mapping-roles.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/docs/en/security/authorization/managing-roles.asciidoc b/x-pack/docs/en/security/authorization/managing-roles.asciidoc index aa872a88ce0a..a7887ee66487 100644 --- a/x-pack/docs/en/security/authorization/managing-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/managing-roles.asciidoc @@ -130,7 +130,7 @@ manage roles, log in to {kib} and go to *Management / Elasticsearch / Roles*. The _Role Management APIs_ enable you to add, update, remove and retrieve roles dynamically. When you use the APIs to manage roles in the `native` realm, the roles are stored in an internal {es} index. For more information and examples, -see {ref}/security-api.html#security-api-roles.html[role management APIs]. +see {ref}/security-api.html#security-api-roles[role management APIs]. [float] [[roles-management-file]] diff --git a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc index 9626571dbd5b..1c6482510f0f 100644 --- a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc @@ -18,7 +18,7 @@ the API, and other roles that are mapped through files. When you use role-mappings, you assign existing roles to users. The available roles should either be added using the -{ref}/security-api.html#security-api-roles.html[role management APIs] or defined in the +{ref}/security-api.html#security-api-roles[role management APIs] or defined in the <>. Either role-mapping method can use either role management method. For example, when you use the role mapping API, you are able to map users to both API-managed roles and file-managed roles From 86ffce4bbc93eadb8b3b505ef48b8bf0897cb69b Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 17 Aug 2018 13:55:42 -0400 Subject: [PATCH 041/283] TEST: Mute testRetentionPolicyChangeDuringRecovery Tracked at #32089 --- .../java/org/elasticsearch/indices/recovery/RecoveryTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java index 7ca9dcc82f70..0f663eca75d7 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java @@ -43,7 +43,6 @@ import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.translog.SnapshotMatchers; import org.elasticsearch.index.translog.Translog; -import org.elasticsearch.test.junit.annotations.TestLogging; import java.util.HashMap; import java.util.List; @@ -74,7 +73,7 @@ public void testTranslogHistoryTransferred() throws Exception { } } - @TestLogging("_root:TRACE") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32089") public void testRetentionPolicyChangeDuringRecovery() throws Exception { try (ReplicationGroup shards = createGroup(0)) { shards.startPrimary(); From 647705e00a118bbafb3258d451beeee9a26c90bc Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 17 Aug 2018 15:30:31 -0500 Subject: [PATCH 042/283] Bypassing failing test PainlessDomainSplitIT#testHRDSplit (#32966) --- .../elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/qa/ml-single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java b/x-pack/qa/ml-single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java index 79e9a81831fc..0751d7307ae9 100644 --- a/x-pack/qa/ml-single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java +++ b/x-pack/qa/ml-single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java @@ -240,6 +240,7 @@ public void testIsolated() throws Exception { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32966") public void testHRDSplit() throws Exception { // Create job From 1efee66d164d1d6996bae95ab7c9f4cca9c98d5e Mon Sep 17 00:00:00 2001 From: lcawl Date: Fri, 17 Aug 2018 21:39:21 -0700 Subject: [PATCH 043/283] [DOCS] Creates redirects for role management APIs page --- docs/reference/redirects.asciidoc | 9 +++++++++ x-pack/docs/en/rest-api/security.asciidoc | 10 +++++++++- x-pack/docs/en/rest-api/security/role-mapping.asciidoc | 2 +- x-pack/docs/en/rest-api/security/roles.asciidoc | 9 --------- .../en/security/authorization/managing-roles.asciidoc | 2 +- .../en/security/authorization/mapping-roles.asciidoc | 2 +- 6 files changed, 21 insertions(+), 13 deletions(-) delete mode 100644 x-pack/docs/en/rest-api/security/roles.asciidoc diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index a5a8e4d008ac..2d11d2108905 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -503,3 +503,12 @@ guide to the {painless}/index.html[Painless Scripting Language]. See the {painless}/painless-api-reference.html[Painless API Reference] in the guide to the {painless}/index.html[Painless Scripting Language]. + +[role="exclude", id="security-api-roles"] +=== Role management APIs + +You can use the following APIs to add, remove, and retrieve roles in the native realm: + +* <>, <> +* <> +* <> \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index 4d59cb22daa0..476c9b95bfda 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -12,7 +12,15 @@ You can use the following APIs to perform {security} activities. * <> * <> -include::security/roles.asciidoc[] +[float] +[[security-role-apis]] +=== Roles + +You can use the following APIs to add, remove, and retrieve roles in the native realm: + +* <>, <> +* <> +* <> include::security/authenticate.asciidoc[] include::security/change-password.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/role-mapping.asciidoc b/x-pack/docs/en/rest-api/security/role-mapping.asciidoc index 3844e30c62dc..c8006346d4e8 100644 --- a/x-pack/docs/en/rest-api/security/role-mapping.asciidoc +++ b/x-pack/docs/en/rest-api/security/role-mapping.asciidoc @@ -22,7 +22,7 @@ Role mappings have _rules_ that identify users and a list of _roles_ that are granted to those users. NOTE: This API does not create roles. Rather, it maps users to existing roles. -Roles can be created by using <> or +Roles can be created by using <> or {xpack-ref}/defining-roles.html#roles-management-file[roles files]. The role mapping rule is a logical condition that is expressed using a JSON DSL. diff --git a/x-pack/docs/en/rest-api/security/roles.asciidoc b/x-pack/docs/en/rest-api/security/roles.asciidoc deleted file mode 100644 index 3853963081e3..000000000000 --- a/x-pack/docs/en/rest-api/security/roles.asciidoc +++ /dev/null @@ -1,9 +0,0 @@ -[float] -[[security-api-roles]] -=== Roles - -You can use the following APIs to add, remove, and retrieve roles in the native realm: - -* <>, <> -* <> -* <> \ No newline at end of file diff --git a/x-pack/docs/en/security/authorization/managing-roles.asciidoc b/x-pack/docs/en/security/authorization/managing-roles.asciidoc index a7887ee66487..f550c900edce 100644 --- a/x-pack/docs/en/security/authorization/managing-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/managing-roles.asciidoc @@ -130,7 +130,7 @@ manage roles, log in to {kib} and go to *Management / Elasticsearch / Roles*. The _Role Management APIs_ enable you to add, update, remove and retrieve roles dynamically. When you use the APIs to manage roles in the `native` realm, the roles are stored in an internal {es} index. For more information and examples, -see {ref}/security-api.html#security-api-roles[role management APIs]. +see {ref}/security-api.html#security-role-apis[role management APIs]. [float] [[roles-management-file]] diff --git a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc index 1c6482510f0f..36f3a1f27f34 100644 --- a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc @@ -18,7 +18,7 @@ the API, and other roles that are mapped through files. When you use role-mappings, you assign existing roles to users. The available roles should either be added using the -{ref}/security-api.html#security-api-roles[role management APIs] or defined in the +{ref}/security-api.html#security-role-apis[role management APIs] or defined in the <>. Either role-mapping method can use either role management method. For example, when you use the role mapping API, you are able to map users to both API-managed roles and file-managed roles From fb1c3990d72aa1b2272db9e37c06dfc7a422da77 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 17 Aug 2018 22:22:09 -0700 Subject: [PATCH 044/283] [DOCS] Splits the token APIs into separate pages (#32865) --- docs/reference/redirects.asciidoc | 10 ++- x-pack/docs/en/rest-api/security.asciidoc | 13 ++- .../rest-api/security/delete-tokens.asciidoc | 54 ++++++++++++ .../{tokens.asciidoc => get-tokens.asciidoc} | 86 ++++++------------- .../api/xpack.security.get_token.json | 2 +- .../api/xpack.security.invalidate_token.json | 2 +- 6 files changed, 104 insertions(+), 63 deletions(-) create mode 100644 x-pack/docs/en/rest-api/security/delete-tokens.asciidoc rename x-pack/docs/en/rest-api/security/{tokens.asciidoc => get-tokens.asciidoc} (62%) diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 2d11d2108905..948652c37e69 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -511,4 +511,12 @@ You can use the following APIs to add, remove, and retrieve roles in the native * <>, <> * <> -* <> \ No newline at end of file +* <> + +[role="exclude",id="security-api-tokens"] +=== Token management APIs + +You can use the following APIs to create and invalidate bearer tokens for access +without requiring basic authentication: + +* <>, <> diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index 476c9b95bfda..27e54df38b31 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -9,7 +9,6 @@ You can use the following APIs to perform {security} activities. * <> * <> * <> -* <> * <> [float] @@ -22,15 +21,25 @@ You can use the following APIs to add, remove, and retrieve roles in the native * <> * <> +[float] +[[security-token-apis]] +=== Tokens + +You can use the following APIs to create and invalidate bearer tokens for access +without requiring basic authentication: + +* <>, <> + include::security/authenticate.asciidoc[] include::security/change-password.asciidoc[] include::security/clear-cache.asciidoc[] include::security/clear-roles-cache.asciidoc[] include::security/create-roles.asciidoc[] include::security/delete-roles.asciidoc[] +include::security/delete-tokens.asciidoc[] include::security/get-roles.asciidoc[] +include::security/get-tokens.asciidoc[] include::security/privileges.asciidoc[] include::security/role-mapping.asciidoc[] include::security/ssl.asciidoc[] -include::security/tokens.asciidoc[] include::security/users.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/delete-tokens.asciidoc b/x-pack/docs/en/rest-api/security/delete-tokens.asciidoc new file mode 100644 index 000000000000..7d6bae2a4c40 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/delete-tokens.asciidoc @@ -0,0 +1,54 @@ +[role="xpack"] +[[security-api-invalidate-token]] +=== Delete token API + +Invalidates a bearer token for access without requiring basic authentication. + +==== Request + +`DELETE /_xpack/security/oauth2/token` + +==== Description + +The tokens returned by the <> have a +finite period of time for which they are valid and after that time period, they +can no longer be used. That time period is defined by the +`xpack.security.authc.token.timeout` setting. For more information, see +<>. + +If you want to invalidate a token immediately, use this delete token API. + + +==== Request Body + +The following parameters can be specified in the body of a DELETE request and +pertain to deleting a token: + +`token` (required):: +(string) An access token. + +==== Examples + +The following example invalidates the specified token immediately: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/security/oauth2/token +{ + "token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==" +} +-------------------------------------------------- +// NOTCONSOLE + +A successful call returns a JSON structure that indicates whether the token +has already been invalidated. + +[source,js] +-------------------------------------------------- +{ + "created" : true <1> +} +-------------------------------------------------- +// NOTCONSOLE + +<1> When a token has already been invalidated, `created` is set to false. diff --git a/x-pack/docs/en/rest-api/security/tokens.asciidoc b/x-pack/docs/en/rest-api/security/get-tokens.asciidoc similarity index 62% rename from x-pack/docs/en/rest-api/security/tokens.asciidoc rename to x-pack/docs/en/rest-api/security/get-tokens.asciidoc index f991a5c0cb83..a2c4e6d7a37e 100644 --- a/x-pack/docs/en/rest-api/security/tokens.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-tokens.asciidoc @@ -1,15 +1,12 @@ [role="xpack"] -[[security-api-tokens]] -=== Token Management APIs +[[security-api-get-token]] +=== Get token API -The `token` API enables you to create and invalidate bearer tokens for access -without requiring basic authentication. +Creates a bearer token for access without requiring basic authentication. ==== Request -`POST /_xpack/security/oauth2/token` + - -`DELETE /_xpack/security/oauth2/token` +`POST /_xpack/security/oauth2/token` ==== Description @@ -19,20 +16,20 @@ you can explicitly enable the `xpack.security.authc.token.enabled` setting. When you are running in production mode, a bootstrap check prevents you from enabling the token service unless you also enable TLS on the HTTP interface. -The Get Token API takes the same parameters as a typical OAuth 2.0 token API +The get token API takes the same parameters as a typical OAuth 2.0 token API except for the use of a JSON request body. -A successful Get Token API call returns a JSON structure that contains the access +A successful get token API call returns a JSON structure that contains the access token, the amount of time (seconds) that the token expires in, the type, and the scope if available. -The tokens returned by the Get Token API have a finite period of time for which +The tokens returned by the get token API have a finite period of time for which they are valid and after that time period, they can no longer be used. That time period is defined by the `xpack.security.authc.token.timeout` setting. For more information, see <>. -If you want to invalidate a token immediately, you can do so by using the Delete -Token API. +If you want to invalidate a token immediately, you can do so by using the +<>. ==== Request Body @@ -41,28 +38,28 @@ The following parameters can be specified in the body of a POST request and pertain to creating a token: `grant_type`:: -(string) The type of grant. Currently only the `password` grant type is supported. +(string) The type of grant. Valid grant types are: `password` and `refresh_token`. -`password` (required):: -(string) The user's password. +`password`:: +(string) The user's password. If you specify the `password` grant type, this +parameter is required. + +`refresh_token`:: +(string) If you specify the `refresh_token` grant type, this parameter is +required. It contains the string that was returned when you created the token +and enables you to extend its life. `scope`:: (string) The scope of the token. Currently tokens are only issued for a scope of `FULL` regardless of the value sent with the request. -`username` (required):: -(string) The username that identifies the user. - -The following parameters can be specified in the body of a DELETE request and -pertain to deleting a token: - -`token`:: -(string) An access token. +`username`:: +(string) The username that identifies the user. If you specify the `password` +grant type, this parameter is required. ==== Examples -[[security-api-get-token]] -To obtain a token, submit a POST request to the `/_xpack/security/oauth2/token` -endpoint. + +The following example obtains a token for the `test_admin` user: [source,js] -------------------------------------------------- @@ -101,8 +98,8 @@ curl -H "Authorization: Bearer dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb // NOTCONSOLE [[security-api-refresh-token]] -To extend the life of an existing token, the token api may be called again with the refresh -token within 24 hours of the token's creation. +To extend the life of an existing token, you can call the API again with the +refresh token within 24 hours of the token's creation. For example: [source,js] -------------------------------------------------- @@ -116,7 +113,8 @@ POST /_xpack/security/oauth2/token // TEST[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/] // TEST[continued] -The API will return a new token and refresh token. Each refresh token may only be used one time. +The API will return a new token and refresh token. Each refresh token may only +be used one time. [source,js] -------------------------------------------------- @@ -128,32 +126,4 @@ The API will return a new token and refresh token. Each refresh token may only b } -------------------------------------------------- // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/] -// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/] - -[[security-api-invalidate-token]] -If a token must be invalidated immediately, you can do so by submitting a DELETE -request to `/_xpack/security/oauth2/token`. For example: - -[source,js] --------------------------------------------------- -DELETE /_xpack/security/oauth2/token -{ - "token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==" -} --------------------------------------------------- -// CONSOLE -// TEST[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/] -// TEST[continued] - -A successful call returns a JSON structure that indicates whether the token -has already been invalidated. - -[source,js] --------------------------------------------------- -{ - "created" : true <1> -} --------------------------------------------------- -// TESTRESPONSE - -<1> When a token has already been invalidated, `created` is set to false. +// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/] \ No newline at end of file diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_token.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_token.json index 8020d1ecd6d9..0b6f141d10e6 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_token.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_token.json @@ -1,6 +1,6 @@ { "xpack.security.get_token": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html#security-api-get-token", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-token.html", "methods": [ "POST" ], "url": { "path": "/_xpack/security/oauth2/token", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.invalidate_token.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.invalidate_token.json index be032c2ffd02..27dd10309142 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.invalidate_token.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.invalidate_token.json @@ -1,6 +1,6 @@ { "xpack.security.invalidate_token": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-tokens.html#security-api-invalidate-token", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-token.html", "methods": [ "DELETE" ], "url": { "path": "/_xpack/security/oauth2/token", From 532d552ffd61751f18778596e32c3e0d9ca1bd80 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 17 Aug 2018 23:17:33 -0700 Subject: [PATCH 045/283] [DOCS] Splits the users API documentation into multiple pages (#32825) --- docs/reference/redirects.asciidoc | 11 + x-pack/docs/build.gradle | 13 + x-pack/docs/en/rest-api/security.asciidoc | 19 +- .../security/change-password.asciidoc | 21 +- .../rest-api/security/create-users.asciidoc | 107 +++++++++ .../rest-api/security/delete-users.asciidoc | 48 ++++ .../rest-api/security/disable-users.asciidoc | 43 ++++ .../rest-api/security/enable-users.asciidoc | 42 ++++ .../en/rest-api/security/get-users.asciidoc | 74 ++++++ .../docs/en/rest-api/security/users.asciidoc | 226 ------------------ .../api/xpack.security.delete_user.json | 2 +- .../api/xpack.security.disable_user.json | 2 +- .../api/xpack.security.enable_user.json | 2 +- .../api/xpack.security.get_user.json | 2 +- .../api/xpack.security.put_user.json | 2 +- 15 files changed, 375 insertions(+), 239 deletions(-) create mode 100644 x-pack/docs/en/rest-api/security/create-users.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/delete-users.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/disable-users.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/enable-users.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/get-users.asciidoc delete mode 100644 x-pack/docs/en/rest-api/security/users.asciidoc diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 948652c37e69..d67d8a733ac0 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -520,3 +520,14 @@ You can use the following APIs to create and invalidate bearer tokens for access without requiring basic authentication: * <>, <> + +[role="exclude",id="security-api-users"] +=== User Management APIs + +You can use the following APIs to create, read, update, and delete users from the +native realm: + +* <>, <> +* <>, <> +* <> +* <> diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index 48ac4ba565e5..aab843555819 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -736,3 +736,16 @@ setups['admin_role'] = ''' "metadata" : {"version": 1} } ''' +setups['jacknich_user'] = ''' + - do: + xpack.security.put_user: + username: "jacknich" + body: > + { + "password" : "test-password", + "roles" : [ "admin", "other_role1" ], + "full_name" : "Jack Nicholson", + "email" : "jacknich@example.com", + "metadata" : { "intelligence" : 7 } + } +''' diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index 27e54df38b31..f5b0c8eef667 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -9,7 +9,6 @@ You can use the following APIs to perform {security} activities. * <> * <> * <> -* <> [float] [[security-role-apis]] @@ -30,16 +29,32 @@ without requiring basic authentication: * <>, <> +[float] +[[security-user-apis]] +=== Users + +You can use the following APIs to create, read, update, and delete users from the +native realm: + +* <>, <> +* <>, <> +* <> +* <> + include::security/authenticate.asciidoc[] include::security/change-password.asciidoc[] include::security/clear-cache.asciidoc[] include::security/clear-roles-cache.asciidoc[] include::security/create-roles.asciidoc[] +include::security/create-users.asciidoc[] include::security/delete-roles.asciidoc[] include::security/delete-tokens.asciidoc[] +include::security/delete-users.asciidoc[] +include::security/disable-users.asciidoc[] +include::security/enable-users.asciidoc[] include::security/get-roles.asciidoc[] include::security/get-tokens.asciidoc[] +include::security/get-users.asciidoc[] include::security/privileges.asciidoc[] include::security/role-mapping.asciidoc[] include::security/ssl.asciidoc[] -include::security/users.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/change-password.asciidoc b/x-pack/docs/en/rest-api/security/change-password.asciidoc index 7dee98480e72..6e6e8cf7375e 100644 --- a/x-pack/docs/en/rest-api/security/change-password.asciidoc +++ b/x-pack/docs/en/rest-api/security/change-password.asciidoc @@ -1,9 +1,8 @@ [role="xpack"] [[security-api-change-password]] -=== Change Password API +=== Change passwords API -The Change Password API enables you to submit a request to change the password -of a user. +Changes the passwords of users in the native realm. ==== Request @@ -12,6 +11,15 @@ of a user. `POST _xpack/security/user//_password` +==== Description + +You can use the <> to update everything +but a user's `username` and `password`. This API changes a user's password. + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + + ==== Path Parameters `username`:: @@ -33,16 +41,17 @@ privilege can change passwords of other users. ==== Examples -The following example updates the password for the `elastic` user: +The following example updates the password for the `jacknich` user: [source,js] -------------------------------------------------- -POST _xpack/security/user/elastic/_password +POST /_xpack/security/user/jacknich/_password { - "password": "x-pack-test-password" + "password" : "s3cr3t" } -------------------------------------------------- // CONSOLE +// TEST[setup:jacknich_user] A successful call returns an empty JSON structure. diff --git a/x-pack/docs/en/rest-api/security/create-users.asciidoc b/x-pack/docs/en/rest-api/security/create-users.asciidoc new file mode 100644 index 000000000000..5015d0401c22 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/create-users.asciidoc @@ -0,0 +1,107 @@ +[role="xpack"] +[[security-api-put-user]] +=== Create users API + +Creates and updates users in the native realm. These users are commonly referred +to as _native users_. + + +==== Request + +`POST /_xpack/security/user/` + + +`PUT /_xpack/security/user/` + + +==== Description + +When updating a user, you can update everything but its `username` and `password`. +To change a user's password, use the +<>. + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + +==== Path Parameters + +`username` (required):: + (string) An identifier for the user. ++ +-- +[[username-validation]] +NOTE: Usernames must be at least 1 and no more than 1024 characters. They can +contain alphanumeric characters (`a-z`, `A-Z`, `0-9`), spaces, punctuation, and +printable symbols in the https://en.wikipedia.org/wiki/Basic_Latin_(Unicode_block)[Basic Latin (ASCII) block]. Leading or trailing whitespace is not allowed. + +-- + + +==== Request Body + +The following parameters can be specified in the body of a POST or PUT request: + +`enabled`:: +(boolean) Specifies whether the user is enabled. The default value is `true`. + +`email`:: +(string) The email of the user. + +`full_name`:: +(string) The full name of the user. + +`metadata`:: +(object) Arbitrary metadata that you want to associate with the user. + +`password` (required):: +(string) The user's password. Passwords must be at least 6 characters long. + +`roles` (required):: +(list) A set of roles the user has. The roles determine the user's access +permissions. To create a user without any roles, specify an empty list: `[]`. + + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +The following example creates a user `jacknich`: + +[source,js] +-------------------------------------------------- +POST /_xpack/security/user/jacknich +{ + "password" : "j@rV1s", + "roles" : [ "admin", "other_role1" ], + "full_name" : "Jack Nicholson", + "email" : "jacknich@example.com", + "metadata" : { + "intelligence" : 7 + } +} +-------------------------------------------------- +// CONSOLE + +A successful call returns a JSON structure that shows whether the user has been +created or updated. + +[source,js] +-------------------------------------------------- +{ + "user": { + "created" : true <1> + } +} +-------------------------------------------------- +// TESTRESPONSE +<1> When an existing user is updated, `created` is set to false. + +After you add a user, requests from that user can be authenticated. For example: + +[source,shell] +-------------------------------------------------- +curl -u jacknich:j@rV1s http://localhost:9200/_cluster/health +-------------------------------------------------- +// NOTCONSOLE diff --git a/x-pack/docs/en/rest-api/security/delete-users.asciidoc b/x-pack/docs/en/rest-api/security/delete-users.asciidoc new file mode 100644 index 000000000000..63a66795617b --- /dev/null +++ b/x-pack/docs/en/rest-api/security/delete-users.asciidoc @@ -0,0 +1,48 @@ +[role="xpack"] +[[security-api-delete-user]] +=== Delete users API + +Deletes users from the native realm. + +==== Request + +`DELETE /_xpack/security/user/` + +==== Description + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + +==== Path Parameters + +`username` (required):: + (string) An identifier for the user. + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +The following example deletes the user `jacknich`: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/security/user/jacknich +-------------------------------------------------- +// CONSOLE +// TEST[setup:jacknich_user] + +If the user is successfully deleted, the request returns `{"found": true}`. +Otherwise, `found` is set to false. + +[source,js] +-------------------------------------------------- +{ + "found" : true +} +-------------------------------------------------- +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/security/disable-users.asciidoc b/x-pack/docs/en/rest-api/security/disable-users.asciidoc new file mode 100644 index 000000000000..f5a6bc7e9a13 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/disable-users.asciidoc @@ -0,0 +1,43 @@ +[role="xpack"] +[[security-api-disable-user]] +=== Disable users API + +Disables users in the native realm. + + +==== Request + +`PUT /_xpack/security/user//_disable` + + +==== Description + +By default, when you create users, they are enabled. You can use this API to +revoke a user's access to {es}. To re-enable a user, there is an +<>. + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + +==== Path Parameters + +`username` (required):: + (string) An identifier for the user. + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +The following example disables the user `jacknich`: + +[source,js] +-------------------------------------------------- +PUT /_xpack/security/user/jacknich/_disable +-------------------------------------------------- +// CONSOLE +// TEST[setup:jacknich_user] diff --git a/x-pack/docs/en/rest-api/security/enable-users.asciidoc b/x-pack/docs/en/rest-api/security/enable-users.asciidoc new file mode 100644 index 000000000000..cebaaffa7b28 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/enable-users.asciidoc @@ -0,0 +1,42 @@ +[role="xpack"] +[[security-api-enable-user]] +=== Enable users API + +Enables users in the native realm. + + +==== Request + +`PUT /_xpack/security/user//_enable` + + +==== Description + +By default, when you create users, they are enabled. You can use this enable +users API and the <> to change that attribute. + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + +==== Path Parameters + +`username` (required):: + (string) An identifier for the user. + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +The following example enables the user `jacknich`: + +[source,js] +-------------------------------------------------- +PUT /_xpack/security/user/jacknich/_enable +-------------------------------------------------- +// CONSOLE +// TEST[setup:jacknich_user] diff --git a/x-pack/docs/en/rest-api/security/get-users.asciidoc b/x-pack/docs/en/rest-api/security/get-users.asciidoc new file mode 100644 index 000000000000..2a20baacb0f5 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/get-users.asciidoc @@ -0,0 +1,74 @@ +[role="xpack"] +[[security-api-get-user]] +=== Get users API + +Retrieves information about users in the native realm. + + +==== Request + +`GET /_xpack/security/user` + + +`GET /_xpack/security/user/` + +==== Description + +For more information about the native realm, see +{stack-ov}/realms.html[Realms] and <>. + +==== Path Parameters + +`username`:: + (string) An identifier for the user. You can specify multiple usernames as a comma-separated list. If you omit this parameter, the API retrieves + information about all users. + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +To retrieve a native user, submit a GET request to the `/_xpack/security/user/` +endpoint: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/user/jacknich +-------------------------------------------------- +// CONSOLE +// TEST[setup:jacknich_user] + +A successful call returns an array of users with the JSON representation of the +user. Note that user passwords are not included. + +[source,js] +-------------------------------------------------- +{ + "jacknich": { + "username": "jacknich", + "roles": [ + "admin", "other_role1" + ], + "full_name": "Jack Nicholson", + "email": "jacknich@example.com", + "metadata": { "intelligence" : 7 }, + "enabled": true + } +} +-------------------------------------------------- +// CONSOLE +// TESTRESPONSE + +If the user is not defined in the `native` realm, the request 404s. + +Omit the username to retrieve all users: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/user +-------------------------------------------------- +// CONSOLE +// TEST[continued] diff --git a/x-pack/docs/en/rest-api/security/users.asciidoc b/x-pack/docs/en/rest-api/security/users.asciidoc deleted file mode 100644 index c84da5c7d75f..000000000000 --- a/x-pack/docs/en/rest-api/security/users.asciidoc +++ /dev/null @@ -1,226 +0,0 @@ -[role="xpack"] -[[security-api-users]] -=== User Management APIs - -The `user` API enables you to create, read, update, and delete users from the -`native` realm. These users are commonly referred to as *native users*. - - -==== Request - -`GET /_xpack/security/user` + - -`GET /_xpack/security/user/` + - -`DELETE /_xpack/security/user/` + - -`POST /_xpack/security/user/` + - -`PUT /_xpack/security/user/` + - -`PUT /_xpack/security/user//_disable` + - -`PUT /_xpack/security/user//_enable` + - -`PUT /_xpack/security/user//_password` - - -==== Description - -You can use the PUT user API to create or update users. When updating a user, -you can update everything but its `username` and `password`. To change a user's -password, use the <>. - -[[username-validation]] -NOTE: Usernames must be at least 1 and no more than 1024 characters. They can -contain alphanumeric characters (`a-z`, `A-Z`, `0-9`), spaces, punctuation, and -printable symbols in the https://en.wikipedia.org/wiki/Basic_Latin_(Unicode_block)[Basic Latin (ASCII) block]. -Leading or trailing whitespace is not allowed. - -==== Path Parameters - -`username`:: - (string) An identifier for the user. If you omit this parameter from a Get - User API request, it retrieves information about all users. - - -==== Request Body - -The following parameters can be specified in the body of a POST or PUT request -and pertain to creating a user: - -`enabled`:: -(boolean) Specifies whether the user is enabled. The default value is `true`. - -`email`:: -(string) The email of the user. - -`full_name`:: -(string) The full name of the user. - -`metadata`:: -(object) Arbitrary metadata that you want to associate with the user. - -`password` (required):: -(string) The user's password. Passwords must be at least 6 characters long. - -`roles` (required):: -(list) A set of roles the user has. The roles determine the user's access -permissions. To create a user without any roles, specify an empty list: `[]`. - -==== Authorization - -To use this API, you must have at least the `manage_security` cluster privilege. - - -==== Examples - -[[security-api-put-user]] -To add a user, submit a PUT or POST request to the `/_xpack/security/user/` -endpoint. - -[source,js] --------------------------------------------------- -POST /_xpack/security/user/jacknich -{ - "password" : "j@rV1s", - "roles" : [ "admin", "other_role1" ], - "full_name" : "Jack Nicholson", - "email" : "jacknich@example.com", - "metadata" : { - "intelligence" : 7 - } -} --------------------------------------------------- -// CONSOLE - -A successful call returns a JSON structure that shows whether the user has been -created or updated. - -[source,js] --------------------------------------------------- -{ - "user": { - "created" : true <1> - } -} --------------------------------------------------- -// TESTRESPONSE -<1> When an existing user is updated, `created` is set to false. - -After you add a user through the Users API, requests from that user can be -authenticated. For example: - -[source,shell] --------------------------------------------------- -curl -u jacknich:j@rV1s http://localhost:9200/_cluster/health --------------------------------------------------- -// NOTCONSOLE - -[[security-api-get-user]] -To retrieve a native user, submit a GET request to the `/_xpack/security/user/` -endpoint: - -[source,js] --------------------------------------------------- -GET /_xpack/security/user/jacknich --------------------------------------------------- -// CONSOLE -// TEST[continued] - -A successful call returns an array of users with the JSON representation of the -user. Note that user passwords are not included. - -[source,js] --------------------------------------------------- -{ - "jacknich": { <1> - "username" : "jacknich", - "roles" : [ "admin", "other_role1" ], - "full_name" : "Jack Nicholson", - "email" : "jacknich@example.com", - "enabled": true, - "metadata" : { - "intelligence" : 7 - } - } -} --------------------------------------------------- -// TESTRESPONSE -<1> If the user is not defined in the `native` realm, the request 404s. - -You can specify multiple usernames as a comma-separated list: - -[source,js] --------------------------------------------------- -GET /_xpack/security/user/jacknich,rdinero --------------------------------------------------- -// CONSOLE -// TEST[continued] - -Omit the username to retrieve all users: - -[source,js] --------------------------------------------------- -GET /_xpack/security/user --------------------------------------------------- -// CONSOLE -// TEST[continued] - -[[security-api-reset-user-password]] -To reset the password for a user, submit a PUT request to the -`/_xpack/security/user//_password` endpoint: - -[source,js] --------------------------------------------------- -PUT /_xpack/security/user/jacknich/_password -{ - "password" : "s3cr3t" -} --------------------------------------------------- -// CONSOLE -// TEST[continued] - -[[security-api-disable-user]] -To disable a user, submit a PUT request to the -`/_xpack/security/user//_disable` endpoint: - -[source,js] --------------------------------------------------- -PUT /_xpack/security/user/jacknich/_disable --------------------------------------------------- -// CONSOLE -// TEST[continued] - -[[security-api-enable-user]] -To enable a user, submit a PUT request to the -`/_xpack/security/user//_enable` endpoint: - -[source,js] --------------------------------------------------- -PUT /_xpack/security/user/jacknich/_enable --------------------------------------------------- -// CONSOLE -// TEST[continued] - -[[security-api-delete-user]] -To delete a user, submit a DELETE request to the `/_xpack/security/user/` -endpoint: - -[source,js] --------------------------------------------------- -DELETE /_xpack/security/user/jacknich --------------------------------------------------- -// CONSOLE -// TEST[continued] - -If the user is successfully deleted, the request returns `{"found": true}`. -Otherwise, `found` is set to false. - -[source,js] --------------------------------------------------- -{ - "found" : true -} --------------------------------------------------- -// TESTRESPONSE diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json index d72c854a69dc..fa1deb3e1ec1 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_user.json @@ -1,6 +1,6 @@ { "xpack.security.delete_user": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-delete-user", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-user.html", "methods": [ "DELETE" ], "url": { "path": "/_xpack/security/user/{username}", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json index 3a72b3141911..0e55e82ead62 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.disable_user.json @@ -1,6 +1,6 @@ { "xpack.security.disable_user": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-disable-user", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-disable-user.html", "methods": [ "PUT", "POST" ], "url": { "path": "/_xpack/security/user/{username}/_disable", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json index c68144957f07..da2f67adbea3 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.enable_user.json @@ -1,6 +1,6 @@ { "xpack.security.enable_user": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-enable-user", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-enable-user.html", "methods": [ "PUT", "POST" ], "url": { "path": "/_xpack/security/user/{username}/_enable", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user.json index 910fb7d06458..94dcbca81e18 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_user.json @@ -1,6 +1,6 @@ { "xpack.security.get_user": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-get-user", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-user.html", "methods": [ "GET" ], "url": { "path": "/_xpack/security/user/{username}", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_user.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_user.json index de07498a4095..1b51783a05ef 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_user.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_user.json @@ -1,6 +1,6 @@ { "xpack.security.put_user": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-users.html#security-api-put-user", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html", "methods": [ "PUT", "POST" ], "url": { "path": "/_xpack/security/user/{username}", From f82bb64feba3d3420dd790d02cb7dd5ee612df4d Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Sat, 18 Aug 2018 08:46:44 +0200 Subject: [PATCH 046/283] NETWORKING: Make RemoteClusterConn. Lazy Resolve DNS (#32764) * Lazy resolve DNS (i.e. `String` to `DiscoveryNode`) to not run into indefinitely caching lookup issues (provided the JVM dns cache is configured correctly as explained in https://www.elastic.co/guide/en/elasticsearch/reference/6.3/networkaddress-cache-ttl.html) * Changed `InetAddress` type to `String` for that higher up the stack * Passed down `Supplier` instead of outright `DiscoveryNode` from `RemoteClusterAware#buildRemoteClustersSeeds` on to lazy resolve DNS when the `DiscoveryNode` is actually used (could've also passed down the value of `clusterName = REMOTE_CLUSTERS_SEEDS.getNamespace(concreteSetting)` together with the `List` of hosts, but this route seemed to introduce less duplication and resulted in a significantly smaller changeset). * Closes #28858 --- .../transport/RemoteClusterAware.java | 66 +++++++---- .../transport/RemoteClusterConnection.java | 17 +-- .../transport/RemoteClusterService.java | 18 +-- .../RemoteClusterConnectionTests.java | 104 ++++++++++++------ .../transport/RemoteClusterServiceTests.java | 47 ++++---- .../authz/IndicesAndAliasesResolver.java | 3 +- 6 files changed, 153 insertions(+), 102 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java index 107a4b32d89f..a12f27c93e3c 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.transport; +import java.util.function.Supplier; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -48,9 +49,20 @@ public abstract class RemoteClusterAware extends AbstractComponent { /** * A list of initial seed nodes to discover eligible nodes from the remote cluster */ - public static final Setting.AffixSetting> REMOTE_CLUSTERS_SEEDS = Setting.affixKeySetting("search.remote.", - "seeds", (key) -> Setting.listSetting(key, Collections.emptyList(), RemoteClusterAware::parseSeedAddress, - Setting.Property.NodeScope, Setting.Property.Dynamic)); + public static final Setting.AffixSetting> REMOTE_CLUSTERS_SEEDS = Setting.affixKeySetting( + "search.remote.", + "seeds", + key -> Setting.listSetting( + key, Collections.emptyList(), + s -> { + // validate seed address + parsePort(s); + return s; + }, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ) + ); public static final char REMOTE_CLUSTER_INDEX_SEPARATOR = ':'; public static final String LOCAL_CLUSTER_GROUP_KEY = ""; @@ -65,18 +77,20 @@ protected RemoteClusterAware(Settings settings) { this.clusterNameResolver = new ClusterNameExpressionResolver(settings); } - protected static Map> buildRemoteClustersSeeds(Settings settings) { - Stream>> allConcreteSettings = REMOTE_CLUSTERS_SEEDS.getAllConcreteSettings(settings); + protected static Map>> buildRemoteClustersSeeds(Settings settings) { + Stream>> allConcreteSettings = REMOTE_CLUSTERS_SEEDS.getAllConcreteSettings(settings); return allConcreteSettings.collect( Collectors.toMap(REMOTE_CLUSTERS_SEEDS::getNamespace, concreteSetting -> { String clusterName = REMOTE_CLUSTERS_SEEDS.getNamespace(concreteSetting); - List nodes = new ArrayList<>(); - for (InetSocketAddress address : concreteSetting.get(settings)) { - TransportAddress transportAddress = new TransportAddress(address); - DiscoveryNode node = new DiscoveryNode(clusterName + "#" + transportAddress.toString(), - transportAddress, - Version.CURRENT.minimumCompatibilityVersion()); - nodes.add(node); + List addresses = concreteSetting.get(settings); + List> nodes = new ArrayList<>(addresses.size()); + for (String address : addresses) { + nodes.add(() -> { + TransportAddress transportAddress = new TransportAddress(RemoteClusterAware.parseSeedAddress(address)); + return new DiscoveryNode(clusterName + "#" + transportAddress.toString(), + transportAddress, + Version.CURRENT.minimumCompatibilityVersion()); + }); } return nodes; })); @@ -128,7 +142,7 @@ public Map> groupClusterIndices(String[] requestIndices, Pr * Subclasses must implement this to receive information about updated cluster aliases. If the given address list is * empty the cluster alias is unregistered and should be removed. */ - protected abstract void updateRemoteCluster(String clusterAlias, List addresses); + protected abstract void updateRemoteCluster(String clusterAlias, List addresses); /** * Registers this instance to listen to updates on the cluster settings. @@ -138,27 +152,35 @@ public void listenForUpdates(ClusterSettings clusterSettings) { (namespace, value) -> {}); } - private static InetSocketAddress parseSeedAddress(String remoteHost) { - int portSeparator = remoteHost.lastIndexOf(':'); // in case we have a IPv6 address ie. [::1]:9300 - if (portSeparator == -1 || portSeparator == remoteHost.length()) { - throw new IllegalArgumentException("remote hosts need to be configured as [host:port], found [" + remoteHost + "] instead"); - } - String host = remoteHost.substring(0, portSeparator); + protected static InetSocketAddress parseSeedAddress(String remoteHost) { + String host = remoteHost.substring(0, indexOfPortSeparator(remoteHost)); InetAddress hostAddress; try { hostAddress = InetAddress.getByName(host); } catch (UnknownHostException e) { throw new IllegalArgumentException("unknown host [" + host + "]", e); } + return new InetSocketAddress(hostAddress, parsePort(remoteHost)); + } + + private static int parsePort(String remoteHost) { try { - int port = Integer.valueOf(remoteHost.substring(portSeparator + 1)); + int port = Integer.valueOf(remoteHost.substring(indexOfPortSeparator(remoteHost) + 1)); if (port <= 0) { throw new IllegalArgumentException("port number must be > 0 but was: [" + port + "]"); } - return new InetSocketAddress(hostAddress, port); + return port; } catch (NumberFormatException e) { - throw new IllegalArgumentException("port must be a number", e); + throw new IllegalArgumentException("failed to parse port", e); + } + } + + private static int indexOfPortSeparator(String remoteHost) { + int portSeparator = remoteHost.lastIndexOf(':'); // in case we have a IPv6 address ie. [::1]:9300 + if (portSeparator == -1 || portSeparator == remoteHost.length()) { + throw new IllegalArgumentException("remote hosts need to be configured as [host:port], found [" + remoteHost + "] instead"); } + return portSeparator; } public static String buildRemoteIndexName(String clusterAlias, String indexName) { diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java index 67c0e1a5aa64..15cf7899dc03 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.transport; +import java.util.function.Supplier; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.store.AlreadyClosedException; import org.apache.lucene.util.SetOnce; @@ -84,7 +85,7 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo private final String clusterAlias; private final int maxNumRemoteConnections; private final Predicate nodePredicate; - private volatile List seedNodes; + private volatile List> seedNodes; private volatile boolean skipUnavailable; private final ConnectHandler connectHandler; private SetOnce remoteClusterName = new SetOnce<>(); @@ -99,7 +100,7 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo * @param maxNumRemoteConnections the maximum number of connections to the remote cluster * @param nodePredicate a predicate to filter eligible remote nodes to connect to */ - RemoteClusterConnection(Settings settings, String clusterAlias, List seedNodes, + RemoteClusterConnection(Settings settings, String clusterAlias, List> seedNodes, TransportService transportService, int maxNumRemoteConnections, Predicate nodePredicate) { super(settings); this.localClusterName = ClusterName.CLUSTER_NAME_SETTING.get(settings); @@ -127,7 +128,7 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo /** * Updates the list of seed nodes for this cluster connection */ - synchronized void updateSeedNodes(List seedNodes, ActionListener connectListener) { + synchronized void updateSeedNodes(List> seedNodes, ActionListener connectListener) { this.seedNodes = Collections.unmodifiableList(new ArrayList<>(seedNodes)); connectHandler.connect(connectListener); } @@ -456,7 +457,7 @@ protected void doRun() { }); } - void collectRemoteNodes(Iterator seedNodes, + private void collectRemoteNodes(Iterator> seedNodes, final TransportService transportService, ActionListener listener) { if (Thread.currentThread().isInterrupted()) { listener.onFailure(new InterruptedException("remote connect thread got interrupted")); @@ -464,7 +465,7 @@ void collectRemoteNodes(Iterator seedNodes, try { if (seedNodes.hasNext()) { cancellableThreads.executeIO(() -> { - final DiscoveryNode seedNode = seedNodes.next(); + final DiscoveryNode seedNode = seedNodes.next().get(); final TransportService.HandshakeResponse handshakeResponse; Transport.Connection connection = transportService.openConnection(seedNode, ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, null, null)); @@ -554,11 +555,11 @@ private class SniffClusterStateResponseHandler implements TransportResponseHandl private final TransportService transportService; private final Transport.Connection connection; private final ActionListener listener; - private final Iterator seedNodes; + private final Iterator> seedNodes; private final CancellableThreads cancellableThreads; SniffClusterStateResponseHandler(TransportService transportService, Transport.Connection connection, - ActionListener listener, Iterator seedNodes, + ActionListener listener, Iterator> seedNodes, CancellableThreads cancellableThreads) { this.transportService = transportService; this.connection = connection; @@ -651,7 +652,7 @@ void addConnectedNode(DiscoveryNode node) { * Get the information about remote nodes to be rendered on {@code _remote/info} requests. */ public RemoteConnectionInfo getConnectionInfo() { - List seedNodeAddresses = seedNodes.stream().map(DiscoveryNode::getAddress).collect(Collectors.toList()); + List seedNodeAddresses = seedNodes.stream().map(node -> node.get().getAddress()).collect(Collectors.toList()); TimeValue initialConnectionTimeout = RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings); return new RemoteConnectionInfo(clusterAlias, seedNodeAddresses, maxNumRemoteConnections, connectedNodes.size(), initialConnectionTimeout, skipUnavailable); diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index a07de63d5373..956a0d94179e 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.transport; +import java.util.function.Supplier; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.OriginalIndices; @@ -40,7 +41,6 @@ import java.io.Closeable; import java.io.IOException; -import java.net.InetSocketAddress; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -115,7 +115,8 @@ public final class RemoteClusterService extends RemoteClusterAware implements Cl * @param seeds a cluster alias to discovery node mapping representing the remote clusters seeds nodes * @param connectionListener a listener invoked once every configured cluster has been connected to */ - private synchronized void updateRemoteClusters(Map> seeds, ActionListener connectionListener) { + private synchronized void updateRemoteClusters(Map>> seeds, + ActionListener connectionListener) { if (seeds.containsKey(LOCAL_CLUSTER_GROUP_KEY)) { throw new IllegalArgumentException("remote clusters must not have the empty string as its key"); } @@ -125,7 +126,7 @@ private synchronized void updateRemoteClusters(Map> } else { CountDown countDown = new CountDown(seeds.size()); remoteClusters.putAll(this.remoteClusters); - for (Map.Entry> entry : seeds.entrySet()) { + for (Map.Entry>> entry : seeds.entrySet()) { RemoteClusterConnection remote = this.remoteClusters.get(entry.getKey()); if (entry.getValue().isEmpty()) { // with no seed nodes we just remove the connection try { @@ -310,16 +311,17 @@ synchronized void updateSkipUnavailable(String clusterAlias, Boolean skipUnavail } } - protected void updateRemoteCluster(String clusterAlias, List addresses) { + @Override + protected void updateRemoteCluster(String clusterAlias, List addresses) { updateRemoteCluster(clusterAlias, addresses, ActionListener.wrap((x) -> {}, (x) -> {})); } void updateRemoteCluster( final String clusterAlias, - final List addresses, + final List addresses, final ActionListener connectionListener) { - final List nodes = addresses.stream().map(address -> { - final TransportAddress transportAddress = new TransportAddress(address); + final List> nodes = addresses.stream().>map(address -> () -> { + final TransportAddress transportAddress = new TransportAddress(RemoteClusterAware.parseSeedAddress(address)); final String id = clusterAlias + "#" + transportAddress.toString(); final Version version = Version.CURRENT.minimumCompatibilityVersion(); return new DiscoveryNode(id, transportAddress, version); @@ -334,7 +336,7 @@ void updateRemoteCluster( void initializeRemoteClusters() { final TimeValue timeValue = REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings); final PlainActionFuture future = new PlainActionFuture<>(); - Map> seeds = RemoteClusterAware.buildRemoteClustersSeeds(settings); + Map>> seeds = RemoteClusterAware.buildRemoteClustersSeeds(settings); updateRemoteClusters(seeds, future); try { future.get(timeValue.millis(), TimeUnit.MILLISECONDS); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index 8da4064d1c89..3d0388ccfad9 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.transport; +import java.util.function.Supplier; import org.apache.lucene.store.AlreadyClosedException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -158,8 +159,8 @@ public void testLocalProfileIsUsedForLocalCluster() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { - updateSeedNodes(connection, Arrays.asList(seedNode)); + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); @@ -198,8 +199,8 @@ public void testRemoteProfileIsUsedForRemoteCluster() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { - updateSeedNodes(connection, Arrays.asList(seedNode)); + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); @@ -254,8 +255,8 @@ public void testDiscoverSingleNode() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { - updateSeedNodes(connection, Arrays.asList(seedNode)); + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); @@ -276,7 +277,7 @@ public void testDiscoverSingleNodeWithIncompatibleSeed() throws Exception { knownNodes.add(discoverableTransport.getLocalDiscoNode()); knownNodes.add(incompatibleTransport.getLocalDiscoNode()); Collections.shuffle(knownNodes, random()); - List seedNodes = Arrays.asList(incompatibleSeedNode, seedNode); + List> seedNodes = Arrays.asList(() -> incompatibleSeedNode, () -> seedNode); Collections.shuffle(seedNodes, random()); try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { @@ -310,8 +311,8 @@ public void testNodeDisconnected() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { - updateSeedNodes(connection, Arrays.asList(seedNode)); + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); assertFalse(service.nodeConnected(spareNode)); @@ -359,8 +360,8 @@ public void testFilterDiscoveredNodes() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> n.equals(rejectedNode) == false)) { - updateSeedNodes(connection, Arrays.asList(seedNode)); + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> n.equals(rejectedNode) == false)) { + updateSeedNodes(connection, Arrays.asList(() -> seedNode)); if (rejectedNode.equals(seedNode)) { assertFalse(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); @@ -374,7 +375,7 @@ public void testFilterDiscoveredNodes() throws Exception { } } - private void updateSeedNodes(RemoteClusterConnection connection, List seedNodes) throws Exception { + private void updateSeedNodes(RemoteClusterConnection connection, List> seedNodes) throws Exception { CountDownLatch latch = new CountDownLatch(1); AtomicReference exceptionAtomicReference = new AtomicReference<>(); ActionListener listener = ActionListener.wrap(x -> latch.countDown(), x -> { @@ -398,8 +399,8 @@ public void testConnectWithIncompatibleTransports() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { - expectThrows(Exception.class, () -> updateSeedNodes(connection, Arrays.asList(seedNode))); + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + expectThrows(Exception.class, () -> updateSeedNodes(connection, Arrays.asList(() -> seedNode))); assertFalse(service.nodeConnected(seedNode)); assertTrue(connection.assertNoRunningConnections()); } @@ -461,7 +462,7 @@ public void close() { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { connection.addConnectedNode(seedNode); for (DiscoveryNode node : knownNodes) { final Transport.Connection transportConnection = connection.getConnection(node); @@ -504,7 +505,7 @@ public void run() { CountDownLatch listenerCalled = new CountDownLatch(1); AtomicReference exceptionReference = new AtomicReference<>(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { ActionListener listener = ActionListener.wrap(x -> { listenerCalled.countDown(); fail("expected exception"); @@ -512,7 +513,7 @@ public void run() { exceptionReference.set(x); listenerCalled.countDown(); }); - connection.updateSeedNodes(Arrays.asList(seedNode), listener); + connection.updateSeedNodes(Arrays.asList(() -> seedNode), listener); acceptedLatch.await(); connection.close(); // now close it, this should trigger an interrupt on the socket and we can move on assertTrue(connection.assertNoRunningConnections()); @@ -539,7 +540,7 @@ public void testFetchShards() throws Exception { try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { service.start(); service.acceptIncomingRequests(); - List nodes = Collections.singletonList(seedNode); + List> nodes = Collections.singletonList(() -> seedNode); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", nodes, service, Integer.MAX_VALUE, n -> true)) { if (randomBoolean()) { @@ -579,7 +580,7 @@ public void testFetchShardsThreadContextHeader() throws Exception { try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { service.start(); service.acceptIncomingRequests(); - List nodes = Collections.singletonList(seedNode); + List> nodes = Collections.singletonList(() -> seedNode); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", nodes, service, Integer.MAX_VALUE, n -> true)) { SearchRequest request = new SearchRequest("test-index"); @@ -635,7 +636,7 @@ public void testFetchShardsSkipUnavailable() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Collections.singletonList(seedNode), service, Integer.MAX_VALUE, n -> true)) { + Collections.singletonList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { SearchRequest request = new SearchRequest("test-index"); ClusterSearchShardsRequest searchShardsRequest = new ClusterSearchShardsRequest("test-index") @@ -738,7 +739,7 @@ public void testTriggerUpdatesConcurrently() throws IOException, InterruptedExce knownNodes.add(discoverableTransport.getLocalDiscoNode()); knownNodes.add(seedTransport1.getLocalDiscoNode()); Collections.shuffle(knownNodes, random()); - List seedNodes = Arrays.asList(seedNode1, seedNode); + List> seedNodes = Arrays.asList(() -> seedNode1, () -> seedNode); Collections.shuffle(seedNodes, random()); try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { @@ -816,7 +817,7 @@ public void testCloseWhileConcurrentlyConnecting() throws IOException, Interrupt knownNodes.add(discoverableTransport.getLocalDiscoNode()); knownNodes.add(seedTransport1.getLocalDiscoNode()); Collections.shuffle(knownNodes, random()); - List seedNodes = Arrays.asList(seedNode1, seedNode); + List> seedNodes = Arrays.asList(() -> seedNode1, () -> seedNode); Collections.shuffle(seedNodes, random()); try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { @@ -904,7 +905,7 @@ public void testGetConnectionInfo() throws Exception { knownNodes.add(transport3.getLocalDiscoNode()); knownNodes.add(transport2.getLocalDiscoNode()); Collections.shuffle(knownNodes, random()); - List seedNodes = Arrays.asList(node3, node1, node2); + List> seedNodes = Arrays.asList(() -> node3, () -> node1, () -> node2); Collections.shuffle(seedNodes, random()); try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { @@ -1059,7 +1060,7 @@ public void testEnsureConnected() throws IOException, InterruptedException { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { assertFalse(service.nodeConnected(seedNode)); assertFalse(service.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); @@ -1108,9 +1109,9 @@ public void testCollectNodes() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { if (randomBoolean()) { - updateSeedNodes(connection, Arrays.asList(seedNode)); + updateSeedNodes(connection, Arrays.asList(() -> seedNode)); } CountDownLatch responseLatch = new CountDownLatch(1); AtomicReference> reference = new AtomicReference<>(); @@ -1142,14 +1143,14 @@ public void testConnectedNodesConcurrentAccess() throws IOException, Interrupted List discoverableTransports = new CopyOnWriteArrayList<>(); try { final int numDiscoverableNodes = randomIntBetween(5, 20); - List discoverableNodes = new ArrayList<>(numDiscoverableNodes); - for (int i = 0; i < numDiscoverableNodes; i++) { + List> discoverableNodes = new ArrayList<>(numDiscoverableNodes); + for (int i = 0; i < numDiscoverableNodes; i++ ) { MockTransportService transportService = startTransport("discoverable_node" + i, knownNodes, Version.CURRENT); - discoverableNodes.add(transportService.getLocalDiscoNode()); + discoverableNodes.add(transportService::getLocalDiscoNode); discoverableTransports.add(transportService); } - List seedNodes = randomSubsetOf(discoverableNodes); + List> seedNodes = randomSubsetOf(discoverableNodes); Collections.shuffle(seedNodes, random()); try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { @@ -1198,7 +1199,7 @@ public void testConnectedNodesConcurrentAccess() throws IOException, Interrupted discoverableTransports.add(transportService); connection.addConnectedNode(transportService.getLocalDiscoNode()); } else { - DiscoveryNode node = randomFrom(discoverableNodes); + DiscoveryNode node = randomFrom(discoverableNodes).get(); connection.onNodeDisconnected(node); } } @@ -1246,12 +1247,13 @@ public void testClusterNameIsChecked() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedNode), service, Integer.MAX_VALUE, n -> true)) { - updateSeedNodes(connection, Arrays.asList(seedNode)); + Arrays.asList( () -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); - List discoveryNodes = Arrays.asList(otherClusterTransport.getLocalDiscoNode(), seedNode); + List> discoveryNodes = + Arrays.asList(() -> otherClusterTransport.getLocalDiscoNode(), () -> seedNode); Collections.shuffle(discoveryNodes, random()); updateSeedNodes(connection, discoveryNodes); assertTrue(service.nodeConnected(seedNode)); @@ -1262,7 +1264,7 @@ public void testClusterNameIsChecked() throws Exception { assertTrue(service.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, () -> - updateSeedNodes(connection, Arrays.asList(otherClusterTransport.getLocalDiscoNode()))); + updateSeedNodes(connection, Arrays.asList(() -> otherClusterTransport.getLocalDiscoNode()))); assertThat(illegalStateException.getMessage(), startsWith("handshake failed, mismatched cluster name [Cluster [otherCluster]]" + " - {other_cluster_discoverable_node}")); @@ -1325,7 +1327,7 @@ public void close() { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Collections.singletonList(connectedNode), service, Integer.MAX_VALUE, n -> true)) { + Collections.singletonList(() -> connectedNode), service, Integer.MAX_VALUE, n -> true)) { connection.addConnectedNode(connectedNode); for (int i = 0; i < 10; i++) { //always a direct connection as the remote node is already connected @@ -1348,4 +1350,34 @@ public void close() { } } } + + public void testLazyResolveTransportAddress() throws Exception { + List knownNodes = new CopyOnWriteArrayList<>(); + try (MockTransportService seedTransport = startTransport("seed_node", knownNodes, Version.CURRENT); + MockTransportService discoverableTransport = startTransport("discoverable_node", knownNodes, Version.CURRENT)) { + DiscoveryNode seedNode = seedTransport.getLocalDiscoNode(); + knownNodes.add(seedTransport.getLocalDiscoNode()); + knownNodes.add(discoverableTransport.getLocalDiscoNode()); + Collections.shuffle(knownNodes, random()); + + try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { + service.start(); + service.acceptIncomingRequests(); + CountDownLatch multipleResolveLatch = new CountDownLatch(2); + Supplier seedSupplier = () -> { + multipleResolveLatch.countDown(); + return seedNode; + }; + try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", + Arrays.asList(seedSupplier), service, Integer.MAX_VALUE, n -> true)) { + updateSeedNodes(connection, Arrays.asList(seedSupplier)); + // Closing connections leads to RemoteClusterConnection.ConnectHandler.collectRemoteNodes + // being called again so we try to resolve the same seed node's host twice + discoverableTransport.close(); + seedTransport.close(); + assertTrue(multipleResolveLatch.await(30L, TimeUnit.SECONDS)); + } + } + } + } } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java index 03d76b5a953c..c94b1cbdef54 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.transport; +import java.util.function.Supplier; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; @@ -103,10 +104,19 @@ public void testRemoteClusterSeedSetting() { .put("search.remote.foo.seeds", "192.168.0.1").build(); expectThrows(IllegalArgumentException.class, () -> RemoteClusterAware.REMOTE_CLUSTERS_SEEDS.getAllConcreteSettings(brokenSettings).forEach(setting -> setting.get(brokenSettings))); + + Settings brokenPortSettings = Settings.builder() + .put("search.remote.foo.seeds", "192.168.0.1:123456789123456789").build(); + Exception e = expectThrows( + IllegalArgumentException.class, + () -> RemoteClusterAware.REMOTE_CLUSTERS_SEEDS.getAllConcreteSettings(brokenSettings) + .forEach(setting -> setting.get(brokenPortSettings)) + ); + assertEquals("failed to parse port", e.getMessage()); } public void testBuiltRemoteClustersSeeds() throws Exception { - Map> map = RemoteClusterService.buildRemoteClustersSeeds( + Map>> map = RemoteClusterService.buildRemoteClustersSeeds( Settings.builder().put("search.remote.foo.seeds", "192.168.0.1:8080").put("search.remote.bar.seeds", "[::1]:9090").build()); assertEquals(2, map.size()); assertTrue(map.containsKey("foo")); @@ -114,13 +124,13 @@ public void testBuiltRemoteClustersSeeds() throws Exception { assertEquals(1, map.get("foo").size()); assertEquals(1, map.get("bar").size()); - DiscoveryNode foo = map.get("foo").get(0); + DiscoveryNode foo = map.get("foo").get(0).get(); assertEquals(foo.getAddress(), new TransportAddress(new InetSocketAddress(InetAddress.getByName("192.168.0.1"), 8080))); assertEquals(foo.getId(), "foo#192.168.0.1:8080"); assertEquals(foo.getVersion(), Version.CURRENT.minimumCompatibilityVersion()); - DiscoveryNode bar = map.get("bar").get(0); + DiscoveryNode bar = map.get("bar").get(0).get(); assertEquals(bar.getAddress(), new TransportAddress(new InetSocketAddress(InetAddress.getByName("[::1]"), 9090))); assertEquals(bar.getId(), "bar#[::1]:9090"); assertEquals(bar.getVersion(), Version.CURRENT.minimumCompatibilityVersion()); @@ -194,10 +204,10 @@ public void testIncrementallyAddClusters() throws IOException { assertFalse(service.isCrossClusterSearchEnabled()); service.initializeRemoteClusters(); assertFalse(service.isCrossClusterSearchEnabled()); - service.updateRemoteCluster("cluster_1", Collections.singletonList(seedNode.getAddress().address())); + service.updateRemoteCluster("cluster_1", Collections.singletonList(seedNode.getAddress().toString())); assertTrue(service.isCrossClusterSearchEnabled()); assertTrue(service.isRemoteClusterRegistered("cluster_1")); - service.updateRemoteCluster("cluster_2", Collections.singletonList(otherSeedNode.getAddress().address())); + service.updateRemoteCluster("cluster_2", Collections.singletonList(otherSeedNode.getAddress().toString())); assertTrue(service.isCrossClusterSearchEnabled()); assertTrue(service.isRemoteClusterRegistered("cluster_1")); assertTrue(service.isRemoteClusterRegistered("cluster_2")); @@ -252,22 +262,17 @@ public void testRemoteNodeAttribute() throws IOException, InterruptedException { service.initializeRemoteClusters(); assertFalse(service.isCrossClusterSearchEnabled()); - final InetSocketAddress c1N1Address = c1N1Node.getAddress().address(); - final InetSocketAddress c1N2Address = c1N2Node.getAddress().address(); - final InetSocketAddress c2N1Address = c2N1Node.getAddress().address(); - final InetSocketAddress c2N2Address = c2N2Node.getAddress().address(); - final CountDownLatch firstLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_1", - Arrays.asList(c1N1Address, c1N2Address), + Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), connectionListener(firstLatch)); firstLatch.await(); final CountDownLatch secondLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_2", - Arrays.asList(c2N1Address, c2N2Address), + Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), connectionListener(secondLatch)); secondLatch.await(); @@ -321,22 +326,17 @@ public void testRemoteNodeRoles() throws IOException, InterruptedException { service.initializeRemoteClusters(); assertFalse(service.isCrossClusterSearchEnabled()); - final InetSocketAddress c1N1Address = c1N1Node.getAddress().address(); - final InetSocketAddress c1N2Address = c1N2Node.getAddress().address(); - final InetSocketAddress c2N1Address = c2N1Node.getAddress().address(); - final InetSocketAddress c2N2Address = c2N2Node.getAddress().address(); - final CountDownLatch firstLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_1", - Arrays.asList(c1N1Address, c1N2Address), + Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), connectionListener(firstLatch)); firstLatch.await(); final CountDownLatch secondLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_2", - Arrays.asList(c2N1Address, c2N2Address), + Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), connectionListener(secondLatch)); secondLatch.await(); @@ -398,22 +398,17 @@ public void testCollectNodes() throws InterruptedException, IOException { service.initializeRemoteClusters(); assertFalse(service.isCrossClusterSearchEnabled()); - final InetSocketAddress c1N1Address = c1N1Node.getAddress().address(); - final InetSocketAddress c1N2Address = c1N2Node.getAddress().address(); - final InetSocketAddress c2N1Address = c2N1Node.getAddress().address(); - final InetSocketAddress c2N2Address = c2N2Node.getAddress().address(); - final CountDownLatch firstLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_1", - Arrays.asList(c1N1Address, c1N2Address), + Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), connectionListener(firstLatch)); firstLatch.await(); final CountDownLatch secondLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_2", - Arrays.asList(c2N1Address, c2N2Address), + Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), connectionListener(secondLatch)); secondLatch.await(); CountDownLatch latch = new CountDownLatch(1); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 2247cbe02a81..c388fd5627c3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -30,7 +30,6 @@ import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; -import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -429,7 +428,7 @@ protected Set getRemoteClusterNames() { } @Override - protected void updateRemoteCluster(String clusterAlias, List addresses) { + protected void updateRemoteCluster(String clusterAlias, List addresses) { if (addresses.isEmpty()) { clusters.remove(clusterAlias); } else { From de92d2ef1fb03dda6f07082f8e87dde1ce6a663b Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Sat, 18 Aug 2018 10:09:24 -0600 Subject: [PATCH 047/283] Move connection listener to ConnectionManager (#32956) This is a followup to #31886. After that commit the TransportConnectionListener had to be propogated to both the Transport and the ConnectionManager. This commit moves that listener to completely live in the ConnectionManager. The request and response related methods are moved to a TransportMessageListener. That listener continues to live in the Transport class. --- .../netty4/SimpleNetty4TransportTests.java | 8 -- .../nio/SimpleNioTransportTests.java | 7 -- .../transport/ConnectionManager.java | 32 +++++++- .../elasticsearch/transport/TcpTransport.java | 78 +++++-------------- .../elasticsearch/transport/Transport.java | 15 +--- .../TransportConnectionListener.java | 43 ---------- .../transport/TransportMessageListener.java | 67 ++++++++++++++++ .../transport/TransportService.java | 7 +- .../transport/FailAndRetryMockTransport.java | 10 ++- .../cluster/NodeConnectionsServiceTests.java | 11 +-- .../test/transport/CapturingTransport.java | 9 ++- .../test/transport/StubbableTransport.java | 16 ++-- .../AbstractSimpleTransportTestCase.java | 22 +++++- .../transport/MockTcpTransportTests.java | 10 --- .../nio/SimpleMockNioTransportTests.java | 7 -- .../nio/SimpleSecurityNioTransportTests.java | 14 +--- 16 files changed, 167 insertions(+), 189 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SimpleNetty4TransportTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SimpleNetty4TransportTests.java index 8d628ace2ee3..9d6f016086c2 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SimpleNetty4TransportTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SimpleNetty4TransportTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -87,13 +86,6 @@ protected MockTransportService build(Settings settings, Version version, Cluster return transportService; } - @Override - protected void closeConnectionChannel(Transport transport, Transport.Connection connection) throws IOException { - final Netty4Transport t = (Netty4Transport) transport; - final TcpTransport.NodeChannels channels = (TcpTransport.NodeChannels) connection; - CloseableChannel.closeChannels(channels.getChannels().subList(0, randomIntBetween(1, channels.getChannels().size())), true); - } - public void testConnectException() throws UnknownHostException { try { serviceA.connectToNode(new DiscoveryNode("C", new TransportAddress(InetAddress.getByName("localhost"), 9876), diff --git a/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/SimpleNioTransportTests.java b/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/SimpleNioTransportTests.java index 9322bfd71222..baae00f81a3b 100644 --- a/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/SimpleNioTransportTests.java +++ b/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/SimpleNioTransportTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -93,12 +92,6 @@ protected MockTransportService build(Settings settings, Version version, Cluster return transportService; } - @Override - protected void closeConnectionChannel(Transport transport, Transport.Connection connection) throws IOException { - TcpTransport.NodeChannels channels = (TcpTransport.NodeChannels) connection; - CloseableChannel.closeChannels(channels.getChannels().subList(0, randomIntBetween(1, channels.getChannels().size())), true); - } - public void testConnectException() throws UnknownHostException { try { serviceA.connectToNode(new DiscoveryNode("C", new TransportAddress(InetAddress.getByName("localhost"), 9876), diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java index 1ff8b701a83e..84c337399d5b 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java @@ -91,7 +91,8 @@ public void removeListener(TransportConnectionListener listener) { } public Transport.Connection openConnection(DiscoveryNode node, ConnectionProfile connectionProfile) { - return transport.openConnection(node, ConnectionProfile.resolveConnectionProfile(connectionProfile, defaultProfile)); + ConnectionProfile resolvedProfile = ConnectionProfile.resolveConnectionProfile(connectionProfile, defaultProfile); + return internalOpenConnection(node, resolvedProfile); } /** @@ -115,7 +116,7 @@ public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfil } boolean success = false; try { - connection = transport.openConnection(node, resolvedProfile); + connection = internalOpenConnection(node, resolvedProfile); connectionValidator.accept(connection, resolvedProfile); // we acquire a connection lock, so no way there is an existing connection connectedNodes.put(node, connection); @@ -227,6 +228,19 @@ public void close() { } } + private Transport.Connection internalOpenConnection(DiscoveryNode node, ConnectionProfile connectionProfile) { + Transport.Connection connection = transport.openConnection(node, connectionProfile); + try { + connectionListener.onConnectionOpened(connection); + } finally { + connection.addCloseListener(ActionListener.wrap(() -> connectionListener.onConnectionClosed(connection))); + } + if (connection.isClosed()) { + throw new ConnectTransportException(node, "a channel closed while connecting"); + } + return connection; + } + private void ensureOpen() { if (lifecycle.started() == false) { throw new IllegalStateException("connection manager is closed"); @@ -289,6 +303,20 @@ public void onNodeConnected(DiscoveryNode node) { listener.onNodeConnected(node); } } + + @Override + public void onConnectionOpened(Transport.Connection connection) { + for (TransportConnectionListener listener : listeners) { + listener.onConnectionOpened(connection); + } + } + + @Override + public void onConnectionClosed(Transport.Connection connection) { + for (TransportConnectionListener listener : listeners) { + listener.onConnectionClosed(connection); + } + } } static ConnectionProfile buildDefaultConnectionProfile(Settings settings) { diff --git a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java index 0b82417cfaa0..6d4ab80a8928 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java @@ -184,7 +184,7 @@ public abstract class TcpTransport extends AbstractLifecycleComponent implements protected final NetworkService networkService; protected final Set profileSettings; - private final DelegatingTransportConnectionListener transportListener = new DelegatingTransportConnectionListener(); + private final DelegatingTransportMessageListener messageListener = new DelegatingTransportMessageListener(); private final ConcurrentMap profileBoundAddresses = newConcurrentMap(); private final Map> serverChannels = newConcurrentMap(); @@ -248,14 +248,12 @@ public TcpTransport(String transportName, Settings settings, ThreadPool threadPo protected void doStart() { } - @Override - public void addConnectionListener(TransportConnectionListener listener) { - transportListener.listeners.add(listener); + public void addMessageListener(TransportMessageListener listener) { + messageListener.listeners.add(listener); } - @Override - public boolean removeConnectionListener(TransportConnectionListener listener) { - return transportListener.listeners.remove(listener); + public boolean removeMessageListener(TransportMessageListener listener) { + return messageListener.listeners.remove(listener); } @Override @@ -344,10 +342,6 @@ public TcpChannel channel(TransportRequestOptions.Type type) { return connectionTypeHandle.getChannel(channels); } - boolean allChannelsOpen() { - return channels.stream().allMatch(TcpChannel::isOpen); - } - @Override public boolean sendPing() { for (TcpChannel channel : channels) { @@ -481,11 +475,6 @@ public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connect // underlying channels. nodeChannels = new NodeChannels(node, channels, connectionProfile, version); final NodeChannels finalNodeChannels = nodeChannels; - try { - transportListener.onConnectionOpened(nodeChannels); - } finally { - nodeChannels.addCloseListener(ActionListener.wrap(() -> transportListener.onConnectionClosed(finalNodeChannels))); - } Consumer onClose = c -> { assert c.isOpen() == false : "channel is still open when onClose is called"; @@ -493,10 +482,6 @@ public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connect }; nodeChannels.channels.forEach(ch -> ch.addCloseListener(ActionListener.wrap(() -> onClose.accept(ch)))); - - if (nodeChannels.allChannelsOpen() == false) { - throw new ConnectTransportException(node, "a channel closed while connecting"); - } success = true; return nodeChannels; } catch (ConnectTransportException e) { @@ -907,7 +892,7 @@ private void sendRequestToChannel(final DiscoveryNode node, final TcpChannel cha final TransportRequestOptions finalOptions = options; // this might be called in a different thread SendListener onRequestSent = new SendListener(channel, stream, - () -> transportListener.onRequestSent(node, requestId, action, request, finalOptions), message.length()); + () -> messageListener.onRequestSent(node, requestId, action, request, finalOptions), message.length()); internalSendMessage(channel, message, onRequestSent); addedReleaseListener = true; } finally { @@ -961,7 +946,7 @@ public void sendErrorResponse( final BytesReference header = buildHeader(requestId, status, nodeVersion, bytes.length()); CompositeBytesReference message = new CompositeBytesReference(header, bytes); SendListener onResponseSent = new SendListener(channel, null, - () -> transportListener.onResponseSent(requestId, action, error), message.length()); + () -> messageListener.onResponseSent(requestId, action, error), message.length()); internalSendMessage(channel, message, onResponseSent); } } @@ -1010,7 +995,7 @@ private void sendResponse( final TransportResponseOptions finalOptions = options; // this might be called in a different thread SendListener listener = new SendListener(channel, stream, - () -> transportListener.onResponseSent(requestId, action, response, finalOptions), message.length()); + () -> messageListener.onResponseSent(requestId, action, response, finalOptions), message.length()); internalSendMessage(channel, message, listener); addedReleaseListener = true; } finally { @@ -1266,7 +1251,7 @@ public final void messageReceived(BytesReference reference, TcpChannel channel) if (isHandshake) { handler = pendingHandshakes.remove(requestId); } else { - TransportResponseHandler theHandler = responseHandlers.onResponseReceived(requestId, transportListener); + TransportResponseHandler theHandler = responseHandlers.onResponseReceived(requestId, messageListener); if (theHandler == null && TransportStatus.isError(status)) { handler = pendingHandshakes.remove(requestId); } else { @@ -1373,7 +1358,7 @@ protected String handleRequest(TcpChannel channel, String profileName, final Str features = Collections.emptySet(); } final String action = stream.readString(); - transportListener.onRequestReceived(requestId, action); + messageListener.onRequestReceived(requestId, action); TransportChannel transportChannel = null; try { if (TransportStatus.isHandshake(status)) { @@ -1682,26 +1667,27 @@ public ProfileSettings(Settings settings, String profileName) { } } - private static final class DelegatingTransportConnectionListener implements TransportConnectionListener { - private final List listeners = new CopyOnWriteArrayList<>(); + private static final class DelegatingTransportMessageListener implements TransportMessageListener { + + private final List listeners = new CopyOnWriteArrayList<>(); @Override public void onRequestReceived(long requestId, String action) { - for (TransportConnectionListener listener : listeners) { + for (TransportMessageListener listener : listeners) { listener.onRequestReceived(requestId, action); } } @Override public void onResponseSent(long requestId, String action, TransportResponse response, TransportResponseOptions finalOptions) { - for (TransportConnectionListener listener : listeners) { + for (TransportMessageListener listener : listeners) { listener.onResponseSent(requestId, action, response, finalOptions); } } @Override public void onResponseSent(long requestId, String action, Exception error) { - for (TransportConnectionListener listener : listeners) { + for (TransportMessageListener listener : listeners) { listener.onResponseSent(requestId, action, error); } } @@ -1709,42 +1695,14 @@ public void onResponseSent(long requestId, String action, Exception error) { @Override public void onRequestSent(DiscoveryNode node, long requestId, String action, TransportRequest request, TransportRequestOptions finalOptions) { - for (TransportConnectionListener listener : listeners) { + for (TransportMessageListener listener : listeners) { listener.onRequestSent(node, requestId, action, request, finalOptions); } } - @Override - public void onNodeDisconnected(DiscoveryNode key) { - for (TransportConnectionListener listener : listeners) { - listener.onNodeDisconnected(key); - } - } - - @Override - public void onConnectionOpened(Connection nodeChannels) { - for (TransportConnectionListener listener : listeners) { - listener.onConnectionOpened(nodeChannels); - } - } - - @Override - public void onNodeConnected(DiscoveryNode node) { - for (TransportConnectionListener listener : listeners) { - listener.onNodeConnected(node); - } - } - - @Override - public void onConnectionClosed(Connection nodeChannels) { - for (TransportConnectionListener listener : listeners) { - listener.onConnectionClosed(nodeChannels); - } - } - @Override public void onResponseReceived(long requestId, ResponseContext holder) { - for (TransportConnectionListener listener : listeners) { + for (TransportMessageListener listener : listeners) { listener.onResponseReceived(requestId, holder); } } diff --git a/server/src/main/java/org/elasticsearch/transport/Transport.java b/server/src/main/java/org/elasticsearch/transport/Transport.java index 9538119f43b8..90adf2ab9e7d 100644 --- a/server/src/main/java/org/elasticsearch/transport/Transport.java +++ b/server/src/main/java/org/elasticsearch/transport/Transport.java @@ -56,18 +56,9 @@ public interface Transport extends LifecycleComponent { */ RequestHandlerRegistry getRequestHandler(String action); - /** - * Adds a new event listener - * @param listener the listener to add - */ - void addConnectionListener(TransportConnectionListener listener); + void addMessageListener(TransportMessageListener listener); - /** - * Removes an event listener - * @param listener the listener to remove - * @return true iff the listener was removed otherwise false - */ - boolean removeConnectionListener(TransportConnectionListener listener); + boolean removeMessageListener(TransportMessageListener listener); /** * The address the transport is bound on. @@ -254,7 +245,7 @@ public List prune(Predicate predicate) { * sent request (before any processing or deserialization was done). Returns the appropriate response handler or null if not * found. */ - public TransportResponseHandler onResponseReceived(final long requestId, TransportConnectionListener listener) { + public TransportResponseHandler onResponseReceived(final long requestId, TransportMessageListener listener) { ResponseContext context = handlers.remove(requestId); listener.onResponseReceived(requestId, context); if (context == null) { diff --git a/server/src/main/java/org/elasticsearch/transport/TransportConnectionListener.java b/server/src/main/java/org/elasticsearch/transport/TransportConnectionListener.java index 0ee2ed5828d4..c41a328637c2 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportConnectionListener.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportConnectionListener.java @@ -28,42 +28,6 @@ */ public interface TransportConnectionListener { - /** - * Called once a request is received - * @param requestId the internal request ID - * @param action the request action - * - */ - default void onRequestReceived(long requestId, String action) {} - - /** - * Called for every action response sent after the response has been passed to the underlying network implementation. - * @param requestId the request ID (unique per client) - * @param action the request action - * @param response the response send - * @param finalOptions the response options - */ - default void onResponseSent(long requestId, String action, TransportResponse response, TransportResponseOptions finalOptions) {} - - /*** - * Called for every failed action response after the response has been passed to the underlying network implementation. - * @param requestId the request ID (unique per client) - * @param action the request action - * @param error the error sent back to the caller - */ - default void onResponseSent(long requestId, String action, Exception error) {} - - /** - * Called for every request sent to a server after the request has been passed to the underlying network implementation - * @param node the node the request was sent to - * @param requestId the internal request id - * @param action the action name - * @param request the actual request - * @param finalOptions the request options - */ - default void onRequestSent(DiscoveryNode node, long requestId, String action, TransportRequest request, - TransportRequestOptions finalOptions) {} - /** * Called once a connection was opened * @param connection the connection @@ -76,13 +40,6 @@ default void onConnectionOpened(Transport.Connection connection) {} */ default void onConnectionClosed(Transport.Connection connection) {} - /** - * Called for every response received - * @param requestId the request id for this reponse - * @param context the response context or null if the context was already processed ie. due to a timeout. - */ - default void onResponseReceived(long requestId, Transport.ResponseContext context) {} - /** * Called once a node connection is opened and registered. */ diff --git a/server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java b/server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java new file mode 100644 index 000000000000..a872c761b36d --- /dev/null +++ b/server/src/main/java/org/elasticsearch/transport/TransportMessageListener.java @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.transport; + +import org.elasticsearch.cluster.node.DiscoveryNode; + +public interface TransportMessageListener { + + /** + * Called once a request is received + * @param requestId the internal request ID + * @param action the request action + * + */ + default void onRequestReceived(long requestId, String action) {} + + /** + * Called for every action response sent after the response has been passed to the underlying network implementation. + * @param requestId the request ID (unique per client) + * @param action the request action + * @param response the response send + * @param finalOptions the response options + */ + default void onResponseSent(long requestId, String action, TransportResponse response, TransportResponseOptions finalOptions) {} + + /*** + * Called for every failed action response after the response has been passed to the underlying network implementation. + * @param requestId the request ID (unique per client) + * @param action the request action + * @param error the error sent back to the caller + */ + default void onResponseSent(long requestId, String action, Exception error) {} + + /** + * Called for every request sent to a server after the request has been passed to the underlying network implementation + * @param node the node the request was sent to + * @param requestId the internal request id + * @param action the action name + * @param request the actual request + * @param finalOptions the request options + */ + default void onRequestSent(DiscoveryNode node, long requestId, String action, TransportRequest request, + TransportRequestOptions finalOptions) {} + + /** + * Called for every response received + * @param requestId the request id for this reponse + * @param context the response context or null if the context was already processed ie. due to a timeout. + */ + default void onResponseReceived(long requestId, Transport.ResponseContext context) {} +} diff --git a/server/src/main/java/org/elasticsearch/transport/TransportService.java b/server/src/main/java/org/elasticsearch/transport/TransportService.java index 8eca6504b70b..fb14ae96dbf2 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportService.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportService.java @@ -77,7 +77,7 @@ import static org.elasticsearch.common.settings.Setting.listSetting; import static org.elasticsearch.common.settings.Setting.timeSetting; -public class TransportService extends AbstractLifecycleComponent implements TransportConnectionListener { +public class TransportService extends AbstractLifecycleComponent implements TransportMessageListener, TransportConnectionListener { public static final Setting CONNECTIONS_PER_NODE_RECOVERY = intSetting("transport.connections_per_node.recovery", 2, 1, Setting.Property.NodeScope); @@ -248,7 +248,8 @@ void setTracerLogExclude(List tracerLogExclude) { @Override protected void doStart() { - transport.addConnectionListener(this); + transport.addMessageListener(this); + connectionManager.addListener(this); transport.start(); if (transport.boundAddress() != null && logger.isInfoEnabled()) { logger.info("{}", transport.boundAddress()); @@ -506,12 +507,10 @@ public void disconnectFromNode(DiscoveryNode node) { } public void addConnectionListener(TransportConnectionListener listener) { - transport.addConnectionListener(listener); connectionManager.addListener(listener); } public void removeConnectionListener(TransportConnectionListener listener) { - transport.removeConnectionListener(listener); connectionManager.removeListener(listener); } diff --git a/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java b/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java index 513f07b733cd..e9ff4048bfe1 100644 --- a/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java +++ b/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java @@ -38,8 +38,8 @@ import org.elasticsearch.transport.ConnectionProfile; import org.elasticsearch.transport.RequestHandlerRegistry; import org.elasticsearch.transport.Transport; -import org.elasticsearch.transport.TransportConnectionListener; import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportMessageListener; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportResponse; @@ -62,7 +62,7 @@ abstract class FailAndRetryMockTransport imp private volatile Map requestHandlers = Collections.emptyMap(); private final Object requestHandlerMutex = new Object(); private final ResponseHandlers responseHandlers = new ResponseHandlers(); - private TransportConnectionListener listener; + private TransportMessageListener listener; private boolean connectMode = true; @@ -223,13 +223,15 @@ public RequestHandlerRegistry getRequestHandler(String action) { return requestHandlers.get(action); } + @Override - public void addConnectionListener(TransportConnectionListener listener) { + public void addMessageListener(TransportMessageListener listener) { this.listener = listener; } @Override - public boolean removeConnectionListener(TransportConnectionListener listener) { + public boolean removeMessageListener(TransportMessageListener listener) { throw new UnsupportedOperationException(); } + } diff --git a/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java index 92c1016d3e61..473f5152e8fd 100644 --- a/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java @@ -37,9 +37,9 @@ import org.elasticsearch.transport.ConnectionProfile; import org.elasticsearch.transport.RequestHandlerRegistry; import org.elasticsearch.transport.Transport; -import org.elasticsearch.transport.TransportConnectionListener; import org.elasticsearch.transport.TransportException; import org.elasticsearch.transport.TransportInterceptor; +import org.elasticsearch.transport.TransportMessageListener; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; @@ -107,7 +107,6 @@ public void testConnectAndDisconnect() { assertConnectedExactlyToNodes(event.state()); } - public void testReconnect() { List nodes = generateNodes(); NodeConnectionsService service = new NodeConnectionsService(Settings.EMPTY, threadPool, transportService); @@ -188,7 +187,7 @@ public HandshakeResponse handshake(Transport.Connection connection, long timeout private final class MockTransport implements Transport { private ResponseHandlers responseHandlers = new ResponseHandlers(); private volatile boolean randomConnectionExceptions = false; - private TransportConnectionListener listener = new TransportConnectionListener() { + private TransportMessageListener listener = new TransportMessageListener() { }; @Override @@ -201,12 +200,12 @@ public RequestHandlerRegistry getRequestHandler(String action) { } @Override - public void addConnectionListener(TransportConnectionListener listener) { + public void addMessageListener(TransportMessageListener listener) { this.listener = listener; } @Override - public boolean removeConnectionListener(TransportConnectionListener listener) { + public boolean removeMessageListener(TransportMessageListener listener) { throw new UnsupportedOperationException(); } @@ -231,7 +230,6 @@ public Connection openConnection(DiscoveryNode node, ConnectionProfile connectio if (randomConnectionExceptions && randomBoolean()) { throw new ConnectTransportException(node, "simulated"); } - listener.onNodeConnected(node); } Connection connection = new Connection() { @Override @@ -260,7 +258,6 @@ public boolean isClosed() { return false; } }; - listener.onConnectionOpened(connection); return connection; } diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java index a7d3e85d3009..60133a16a10a 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java @@ -41,9 +41,9 @@ import org.elasticsearch.transport.RequestHandlerRegistry; import org.elasticsearch.transport.SendRequestTransportException; import org.elasticsearch.transport.Transport; -import org.elasticsearch.transport.TransportConnectionListener; import org.elasticsearch.transport.TransportException; import org.elasticsearch.transport.TransportInterceptor; +import org.elasticsearch.transport.TransportMessageListener; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportResponse; @@ -72,7 +72,7 @@ public class CapturingTransport implements Transport { private volatile Map requestHandlers = Collections.emptyMap(); final Object requestHandlerMutex = new Object(); private final ResponseHandlers responseHandlers = new ResponseHandlers(); - private TransportConnectionListener listener; + private TransportMessageListener listener; public static class CapturedRequest { public final DiscoveryNode node; @@ -341,7 +341,7 @@ public RequestHandlerRegistry getRequestHandler(String action) { } @Override - public void addConnectionListener(TransportConnectionListener listener) { + public void addMessageListener(TransportMessageListener listener) { if (this.listener != null) { throw new IllegalStateException("listener already set"); } @@ -349,11 +349,12 @@ public void addConnectionListener(TransportConnectionListener listener) { } @Override - public boolean removeConnectionListener(TransportConnectionListener listener) { + public boolean removeMessageListener(TransportMessageListener listener) { if (listener == this.listener) { this.listener = null; return true; } return false; } + } diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java index 5a0dd3b7f6d5..2e78f8a9a4f0 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java @@ -29,8 +29,8 @@ import org.elasticsearch.transport.ConnectionProfile; import org.elasticsearch.transport.RequestHandlerRegistry; import org.elasticsearch.transport.Transport; -import org.elasticsearch.transport.TransportConnectionListener; import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportMessageListener; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportStats; @@ -86,13 +86,13 @@ Transport getDelegate() { } @Override - public void addConnectionListener(TransportConnectionListener listener) { - delegate.addConnectionListener(listener); + public void addMessageListener(TransportMessageListener listener) { + delegate.addMessageListener(listener); } @Override - public boolean removeConnectionListener(TransportConnectionListener listener) { - return delegate.removeConnectionListener(listener); + public boolean removeMessageListener(TransportMessageListener listener) { + return delegate.removeMessageListener(listener); } @Override @@ -179,7 +179,7 @@ public Map profileBoundAddresses() { return delegate.profileBoundAddresses(); } - private class WrappedConnection implements Transport.Connection { + public class WrappedConnection implements Transport.Connection { private final Transport.Connection connection; @@ -234,6 +234,10 @@ public Object getCacheKey() { public void close() { connection.close(); } + + public Transport.Connection getConnection() { + return connection; + } } @FunctionalInterface diff --git a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java index 29997b16ba07..4e59aaecf8de 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.network.NetworkUtils; import org.elasticsearch.common.settings.ClusterSettings; @@ -52,6 +53,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.test.transport.StubbableTransport; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.junit.After; @@ -2642,15 +2644,22 @@ public void testProfilesIncludesDefault() { public void testChannelCloseWhileConnecting() { try (MockTransportService service = build(Settings.builder().put("name", "close").build(), version0, null, true)) { - service.transport.addConnectionListener(new TransportConnectionListener() { + AtomicBoolean connectionClosedListenerCalled = new AtomicBoolean(false); + service.addConnectionListener(new TransportConnectionListener() { @Override public void onConnectionOpened(final Transport.Connection connection) { + closeConnectionChannel(connection); try { - closeConnectionChannel(service.getOriginalTransport(), connection); - } catch (final IOException e) { + assertBusy(connection::isClosed); + } catch (Exception e) { throw new AssertionError(e); } } + + @Override + public void onConnectionClosed(Transport.Connection connection) { + connectionClosedListenerCalled.set(true); + } }); final ConnectionProfile.Builder builder = new ConnectionProfile.Builder(); builder.addConnections(1, @@ -2662,10 +2671,15 @@ public void onConnectionOpened(final Transport.Connection connection) { final ConnectTransportException e = expectThrows(ConnectTransportException.class, () -> service.openConnection(nodeA, builder.build())); assertThat(e, hasToString(containsString(("a channel closed while connecting")))); + assertTrue(connectionClosedListenerCalled.get()); } } - protected abstract void closeConnectionChannel(Transport transport, Transport.Connection connection) throws IOException; + private void closeConnectionChannel(Transport.Connection connection) { + StubbableTransport.WrappedConnection wrappedConnection = (StubbableTransport.WrappedConnection) connection; + TcpTransport.NodeChannels channels = (TcpTransport.NodeChannels) wrappedConnection.getConnection(); + CloseableChannel.closeChannels(channels.getChannels().subList(0, randomIntBetween(1, channels.getChannels().size())), true); + } @SuppressForbidden(reason = "need local ephemeral port") private InetSocketAddress getLocalEphemeral() throws UnknownHostException { diff --git a/test/framework/src/test/java/org/elasticsearch/transport/MockTcpTransportTests.java b/test/framework/src/test/java/org/elasticsearch/transport/MockTcpTransportTests.java index 6d1e5116474f..42658b1d9a60 100644 --- a/test/framework/src/test/java/org/elasticsearch/transport/MockTcpTransportTests.java +++ b/test/framework/src/test/java/org/elasticsearch/transport/MockTcpTransportTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -60,13 +59,4 @@ protected Version executeHandshake(DiscoveryNode node, TcpChannel mockChannel, T public int channelsPerNodeConnection() { return 1; } - - @Override - protected void closeConnectionChannel(Transport transport, Transport.Connection connection) throws IOException { - final MockTcpTransport t = (MockTcpTransport) transport; - final TcpTransport.NodeChannels channels = - (TcpTransport.NodeChannels) connection; - CloseableChannel.closeChannels(channels.getChannels().subList(0, randomIntBetween(1, channels.getChannels().size())), true); - } - } diff --git a/test/framework/src/test/java/org/elasticsearch/transport/nio/SimpleMockNioTransportTests.java b/test/framework/src/test/java/org/elasticsearch/transport/nio/SimpleMockNioTransportTests.java index 3a78f366bd87..8b3e7dce367d 100644 --- a/test/framework/src/test/java/org/elasticsearch/transport/nio/SimpleMockNioTransportTests.java +++ b/test/framework/src/test/java/org/elasticsearch/transport/nio/SimpleMockNioTransportTests.java @@ -22,7 +22,6 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -99,12 +98,6 @@ protected int channelsPerNodeConnection() { return 3; } - @Override - protected void closeConnectionChannel(Transport transport, Transport.Connection connection) throws IOException { - TcpTransport.NodeChannels channels = (TcpTransport.NodeChannels) connection; - CloseableChannel.closeChannels(channels.getChannels().subList(0, randomIntBetween(1, channels.getChannels().size())), true); - } - public void testConnectException() throws UnknownHostException { try { serviceA.connectToNode(new DiscoveryNode("C", new TransportAddress(InetAddress.getByName("localhost"), 9876), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SimpleSecurityNioTransportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SimpleSecurityNioTransportTests.java index 70ab085fcf72..7397ebc8c7dc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SimpleSecurityNioTransportTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/nio/SimpleSecurityNioTransportTests.java @@ -10,7 +10,6 @@ import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; -import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.MockSecureSettings; @@ -35,6 +34,9 @@ import org.elasticsearch.xpack.core.ssl.SSLConfiguration; import org.elasticsearch.xpack.core.ssl.SSLService; +import javax.net.SocketFactory; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLSocket; import java.io.IOException; import java.net.InetAddress; import java.net.SocketTimeoutException; @@ -44,10 +46,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; -import javax.net.SocketFactory; -import javax.net.ssl.HandshakeCompletedListener; -import javax.net.ssl.SSLSocket; - import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; import static org.hamcrest.Matchers.containsString; @@ -118,12 +116,6 @@ protected MockTransportService build(Settings settings, Version version, Cluster return transportService; } - @Override - protected void closeConnectionChannel(Transport transport, Transport.Connection connection) throws IOException { - TcpTransport.NodeChannels channels = (TcpTransport.NodeChannels) connection; - CloseableChannel.closeChannels(channels.getChannels().subList(0, randomIntBetween(1, channels.getChannels().size())), true); - } - public void testConnectException() throws UnknownHostException { try { serviceA.connectToNode(new DiscoveryNode("C", new TransportAddress(InetAddress.getByName("localhost"), 9876), From 4b34b3f4aade488de01c7023670b6360ac7e28da Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Mon, 20 Aug 2018 09:27:02 +0300 Subject: [PATCH 048/283] Set forbidden APIs target compatibility to compiler java version (#32935) Set forbidden apis target compatibility to compiler version Fix outstanding deprecation --- .../gradle/precommit/PrecommitTasks.groovy | 6 + .../painless/InitializerTests.java | 16 +- .../common/inject/matcher/Matchers.java | 4 + .../common/unit/FuzzinessTests.java | 2 +- .../util/concurrent/EsExecutorsTests.java | 2 +- .../util/concurrent/ThreadContextTests.java | 4 +- .../highlight/HighlightBuilderTests.java | 4 +- .../test/EqualsHashCodeTestUtils.java | 2 +- .../scroll/ScrollDataExtractorTests.java | 2 +- .../ml/integration/JobResultsProviderIT.java | 2 +- .../xpack/sql/jdbc/jdbc/TypeConverter.java | 20 +- .../jdbc/jdbc/JdbcPreparedStatementTests.java | 246 +++++++++--------- .../condition/CompareConditionTests.java | 12 +- 13 files changed, 166 insertions(+), 156 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index 598d82d5d9aa..42dc29df058c 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -21,6 +21,7 @@ package org.elasticsearch.gradle.precommit import de.thetaphi.forbiddenapis.gradle.CheckForbiddenApis import de.thetaphi.forbiddenapis.gradle.ForbiddenApisPlugin import org.elasticsearch.gradle.ExportElasticsearchBuildResourcesTask +import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.file.FileCollection @@ -101,6 +102,11 @@ class PrecommitTasks { signaturesURLs = project.forbiddenApis.signaturesURLs + [ getClass().getResource('/forbidden/es-server-signatures.txt') ] } + // forbidden apis doesn't support Java 11, so stop at 10 + String targetMajorVersion = (project.compilerJavaVersion.compareTo(JavaVersion.VERSION_1_10) > 0 ? + JavaVersion.VERSION_1_10 : + project.compilerJavaVersion).getMajorVersion() + targetCompatibility = Integer.parseInt(targetMajorVersion) >= 9 ?targetMajorVersion : "1.${targetMajorVersion}" } Task forbiddenApis = project.tasks.findByName('forbiddenApis') forbiddenApis.group = "" // clear group, so this does not show up under verification tasks diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/InitializerTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/InitializerTests.java index d0d0b2165ca1..be3db76bac25 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/InitializerTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/InitializerTests.java @@ -53,7 +53,7 @@ public void testArrayInitializers() { "Object[] x = new Object[] {y, z, 1 + s, s + 'aaa'}; return x;"); assertEquals(4, objects.length); - assertEquals(new Integer(2), objects[0]); + assertEquals(Integer.valueOf(2), objects[0]); assertEquals(new ArrayList(), objects[1]); assertEquals("1aaa", objects[2]); assertEquals("aaaaaa", objects[3]); @@ -85,7 +85,7 @@ public void testListInitializers() { list = (List)exec("int y = 2; List z = new ArrayList(); String s = 'aaa'; List x = [y, z, 1 + s, s + 'aaa']; return x;"); assertEquals(4, list.size()); - assertEquals(new Integer(2), list.get(0)); + assertEquals(Integer.valueOf(2), list.get(0)); assertEquals(new ArrayList(), list.get(1)); assertEquals("1aaa", list.get(2)); assertEquals("aaaaaa", list.get(3)); @@ -100,15 +100,15 @@ public void testMapInitializers() { map = (Map)exec("[5 : 7, -1 : 14]"); assertEquals(2, map.size()); - assertEquals(new Integer(7), map.get(5)); - assertEquals(new Integer(14), map.get(-1)); + assertEquals(Integer.valueOf(7), map.get(5)); + assertEquals(Integer.valueOf(14), map.get(-1)); map = (Map)exec("int y = 2; int z = 3; Map x = [y*z : y + z, y - z : y, z : z]; return x;"); assertEquals(3, map.size()); - assertEquals(new Integer(5), map.get(6)); - assertEquals(new Integer(2), map.get(-1)); - assertEquals(new Integer(3), map.get(3)); + assertEquals(Integer.valueOf(5), map.get(6)); + assertEquals(Integer.valueOf(2), map.get(-1)); + assertEquals(Integer.valueOf(3), map.get(3)); map = (Map)exec("int y = 2; List z = new ArrayList(); String s = 'aaa';" + "def x = [y : z, 1 + s : s + 'aaa']; return x;"); @@ -139,7 +139,7 @@ public void testCrazyInitializer() { list3.add(9); assertEquals(3, map.size()); - assertEquals(new Integer(5), map.get(6)); + assertEquals(Integer.valueOf(5), map.get(6)); assertEquals(list2, map.get("s")); assertEquals(list3, map.get(3)); } diff --git a/server/src/main/java/org/elasticsearch/common/inject/matcher/Matchers.java b/server/src/main/java/org/elasticsearch/common/inject/matcher/Matchers.java index cc354145b11b..dd5d98dfabbd 100644 --- a/server/src/main/java/org/elasticsearch/common/inject/matcher/Matchers.java +++ b/server/src/main/java/org/elasticsearch/common/inject/matcher/Matchers.java @@ -16,6 +16,8 @@ package org.elasticsearch.common.inject.matcher; +import org.elasticsearch.common.SuppressForbidden; + import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -327,7 +329,9 @@ public String toString() { return "inPackage(" + targetPackage.getName() + ")"; } + @SuppressForbidden(reason = "ClassLoader.getDefinedPackage not available yet") public Object readResolve() { + // TODO minJava >= 9 : use ClassLoader.getDefinedPackage and remove @SuppressForbidden return inPackage(Package.getPackage(packageName)); } } diff --git a/server/src/test/java/org/elasticsearch/common/unit/FuzzinessTests.java b/server/src/test/java/org/elasticsearch/common/unit/FuzzinessTests.java index 0074da43fcfb..520f80fecac4 100644 --- a/server/src/test/java/org/elasticsearch/common/unit/FuzzinessTests.java +++ b/server/src/test/java/org/elasticsearch/common/unit/FuzzinessTests.java @@ -59,7 +59,7 @@ public void testParseFromXContent() throws IOException { Float floatRep = randomFloat(); Number value = intValue; if (randomBoolean()) { - value = new Float(floatRep += intValue); + value = Float.valueOf(floatRep += intValue); } XContentBuilder json = jsonBuilder().startObject() .field(Fuzziness.X_FIELD_NAME, randomBoolean() ? value.toString() : value) diff --git a/server/src/test/java/org/elasticsearch/common/util/concurrent/EsExecutorsTests.java b/server/src/test/java/org/elasticsearch/common/util/concurrent/EsExecutorsTests.java index cc6152d98962..a0fdcbf51ca1 100644 --- a/server/src/test/java/org/elasticsearch/common/util/concurrent/EsExecutorsTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/concurrent/EsExecutorsTests.java @@ -337,7 +337,7 @@ public void testInheritContext() throws InterruptedException { final CountDownLatch executed = new CountDownLatch(1); threadContext.putHeader("foo", "bar"); - final Integer one = new Integer(1); + final Integer one = Integer.valueOf(1); threadContext.putTransient("foo", one); EsThreadPoolExecutor executor = EsExecutors.newFixed(getName(), pool, queue, EsExecutors.daemonThreadFactory("dummy"), threadContext); diff --git a/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java b/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java index e71efa46424b..a0a92cad7a85 100644 --- a/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java @@ -42,7 +42,7 @@ public void testStashContext() { threadContext.putHeader("foo", "bar"); threadContext.putTransient("ctx.foo", 1); assertEquals("bar", threadContext.getHeader("foo")); - assertEquals(new Integer(1), threadContext.getTransient("ctx.foo")); + assertEquals(Integer.valueOf(1), threadContext.getTransient("ctx.foo")); assertEquals("1", threadContext.getHeader("default")); try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { assertNull(threadContext.getHeader("foo")); @@ -61,7 +61,7 @@ public void testStashAndMerge() { threadContext.putHeader("foo", "bar"); threadContext.putTransient("ctx.foo", 1); assertEquals("bar", threadContext.getHeader("foo")); - assertEquals(new Integer(1), threadContext.getTransient("ctx.foo")); + assertEquals(Integer.valueOf(1), threadContext.getTransient("ctx.foo")); assertEquals("1", threadContext.getHeader("default")); HashMap toMerge = new HashMap<>(); toMerge.put("foo", "baz"); diff --git a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java index 37359d9f20d7..50f2261c722a 100644 --- a/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/fetch/subphase/highlight/HighlightBuilderTests.java @@ -601,10 +601,10 @@ private static void setRandomCommonOptions(AbstractHighlighterBuilder highlightB value = randomAlphaOfLengthBetween(1, 10); break; case 1: - value = new Integer(randomInt(1000)); + value = Integer.valueOf(randomInt(1000)); break; case 2: - value = new Boolean(randomBoolean()); + value = Boolean.valueOf(randomBoolean()); break; } options.put(randomAlphaOfLengthBetween(1, 10), value); diff --git a/test/framework/src/main/java/org/elasticsearch/test/EqualsHashCodeTestUtils.java b/test/framework/src/main/java/org/elasticsearch/test/EqualsHashCodeTestUtils.java index 76cfcce033b1..44b845e5c2ca 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/EqualsHashCodeTestUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/test/EqualsHashCodeTestUtils.java @@ -33,7 +33,7 @@ */ public class EqualsHashCodeTestUtils { - private static Object[] someObjects = new Object[] { "some string", new Integer(1), new Double(1.0) }; + private static Object[] someObjects = new Object[] { "some string", Integer.valueOf(1), Double.valueOf(1.0) }; /** * A function that makes a copy of its input argument diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java index 7ed00a01f356..c83521591fcf 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java @@ -392,7 +392,7 @@ public void testResetScrollAfterSearchPhaseExecutionException() throws IOExcepti assertThat(extractor.hasNext(), is(true)); output = extractor.next(); assertThat(output.isPresent(), is(true)); - assertEquals(new Long(1400L), extractor.getLastTimestamp()); + assertEquals(Long.valueOf(1400L), extractor.getLastTimestamp()); // A second failure is not tolerated assertThat(extractor.hasNext(), is(true)); expectThrows(SearchPhaseExecutionException.class, extractor::next); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java index 303fce3d8132..e36c313b626c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/JobResultsProviderIT.java @@ -125,7 +125,7 @@ public void testGetCalandarByJobId() throws Exception { Long matchedCount = queryResult.stream().filter( c -> c.getId().equals("foo calendar") || c.getId().equals("foo bar calendar") || c.getId().equals("cat foo calendar")) .count(); - assertEquals(new Long(3), matchedCount); + assertEquals(Long.valueOf(3), matchedCount); queryResult = getCalendars("bar"); assertThat(queryResult, hasSize(1)); diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java index aa9d434f332e..3b5180b71f7c 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java @@ -58,7 +58,7 @@ private TypeConverter() { private static final long DAY_IN_MILLIS = 60 * 60 * 24; private static final Map, JDBCType> javaToJDBC; - + static { Map, JDBCType> aMap = Arrays.stream(DataType.values()) .filter(dataType -> dataType.javaClass() != null @@ -119,7 +119,7 @@ private static T dateTimeConvert(Long millis, Calendar c, Function T convert(Object val, JDBCType columnType, Class type) throws SQLE if (type == null) { return (T) convert(val, columnType); } - + if (type.isInstance(val)) { try { return type.cast(val); @@ -150,7 +150,7 @@ static T convert(Object val, JDBCType columnType, Class type) throws SQLE throw new SQLDataException("Unable to convert " + val.getClass().getName() + " to " + columnType, cce); } } - + if (type == String.class) { return (T) asString(convert(val, columnType)); } @@ -276,8 +276,8 @@ static boolean isSigned(JDBCType jdbcType) throws SQLException { } return dataType.isSigned(); } - - + + static JDBCType fromJavaToJDBC(Class clazz) throws SQLException { for (Entry, JDBCType> e : javaToJDBC.entrySet()) { // java.util.Calendar from {@code javaToJDBC} is an abstract class and this method can be used with concrete classes as well @@ -285,7 +285,7 @@ static JDBCType fromJavaToJDBC(Class clazz) throws SQLException { return e.getValue(); } } - + throw new SQLFeatureNotSupportedException("Objects of type " + clazz.getName() + " are not supported"); } @@ -432,7 +432,7 @@ private static Float asFloat(Object val, JDBCType columnType) throws SQLExceptio case REAL: case FLOAT: case DOUBLE: - return new Float(((Number) val).doubleValue()); + return Float.valueOf((((float) ((Number) val).doubleValue()))); default: } @@ -451,7 +451,7 @@ private static Double asDouble(Object val, JDBCType columnType) throws SQLExcept case REAL: case FLOAT: case DOUBLE: - return new Double(((Number) val).doubleValue()); + return Double.valueOf(((Number) val).doubleValue()); default: } @@ -539,4 +539,4 @@ private static long safeToLong(double x) throws SQLException { } return Math.round(x); } -} \ No newline at end of file +} diff --git a/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java index ad96825896e1..9da06f6537c0 100644 --- a/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java +++ b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java @@ -38,27 +38,27 @@ import static java.sql.JDBCType.VARCHAR; public class JdbcPreparedStatementTests extends ESTestCase { - + public void testSettingBooleanValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + jps.setBoolean(1, true); assertEquals(true, value(jps)); assertEquals(BOOLEAN, jdbcType(jps)); - + jps.setObject(1, false); assertEquals(false, value(jps)); assertEquals(BOOLEAN, jdbcType(jps)); - + jps.setObject(1, true, Types.BOOLEAN); assertEquals(true, value(jps)); assertEquals(BOOLEAN, jdbcType(jps)); assertTrue(value(jps) instanceof Boolean); - + jps.setObject(1, true, Types.INTEGER); assertEquals(1, value(jps)); assertEquals(INTEGER, jdbcType(jps)); - + jps.setObject(1, true, Types.VARCHAR); assertEquals("true", value(jps)); assertEquals(VARCHAR, jdbcType(jps)); @@ -66,264 +66,264 @@ public void testSettingBooleanValues() throws SQLException { public void testThrownExceptionsWhenSettingBooleanValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, true, Types.TIMESTAMP)); assertEquals("Conversion from type [BOOLEAN] to [Timestamp] not supported", sqle.getMessage()); } - + public void testSettingStringValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + jps.setString(1, "foo bar"); assertEquals("foo bar", value(jps)); assertEquals(VARCHAR, jdbcType(jps)); - + jps.setObject(1, "foo bar"); assertEquals("foo bar", value(jps)); assertEquals(VARCHAR, jdbcType(jps)); - + jps.setObject(1, "foo bar", Types.VARCHAR); assertEquals("foo bar", value(jps)); assertEquals(VARCHAR, jdbcType(jps)); assertTrue(value(jps) instanceof String); } - + public void testThrownExceptionsWhenSettingStringValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, "foo bar", Types.INTEGER)); assertEquals("Conversion from type [VARCHAR] to [Integer] not supported", sqle.getMessage()); } - + public void testSettingByteTypeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + jps.setByte(1, (byte) 6); assertEquals((byte) 6, value(jps)); assertEquals(TINYINT, jdbcType(jps)); - + jps.setObject(1, (byte) 6); assertEquals((byte) 6, value(jps)); assertEquals(TINYINT, jdbcType(jps)); assertTrue(value(jps) instanceof Byte); - + jps.setObject(1, (byte) 0, Types.BOOLEAN); assertEquals(false, value(jps)); assertEquals(BOOLEAN, jdbcType(jps)); - + jps.setObject(1, (byte) 123, Types.BOOLEAN); assertEquals(true, value(jps)); assertEquals(BOOLEAN, jdbcType(jps)); - + jps.setObject(1, (byte) 123, Types.INTEGER); assertEquals(123, value(jps)); assertEquals(INTEGER, jdbcType(jps)); - + jps.setObject(1, (byte) -128, Types.DOUBLE); assertEquals(-128.0, value(jps)); assertEquals(DOUBLE, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingByteTypeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, (byte) 6, Types.TIMESTAMP)); assertEquals("Conversion from type [TINYINT] to [Timestamp] not supported", sqle.getMessage()); } - + public void testSettingShortTypeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + short someShort = randomShort(); jps.setShort(1, someShort); assertEquals(someShort, value(jps)); assertEquals(SMALLINT, jdbcType(jps)); - + jps.setObject(1, someShort); assertEquals(someShort, value(jps)); assertEquals(SMALLINT, jdbcType(jps)); assertTrue(value(jps) instanceof Short); - + jps.setObject(1, (short) 1, Types.BOOLEAN); assertEquals(true, value(jps)); assertEquals(BOOLEAN, jdbcType(jps)); - + jps.setObject(1, (short) -32700, Types.DOUBLE); assertEquals(-32700.0, value(jps)); assertEquals(DOUBLE, jdbcType(jps)); - + jps.setObject(1, someShort, Types.INTEGER); assertEquals((int) someShort, value(jps)); assertEquals(INTEGER, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingShortTypeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, (short) 6, Types.TIMESTAMP)); assertEquals("Conversion from type [SMALLINT] to [Timestamp] not supported", sqle.getMessage()); - + sqle = expectThrows(SQLException.class, () -> jps.setObject(1, 256, Types.TINYINT)); assertEquals("Numeric " + 256 + " out of range", sqle.getMessage()); } - + public void testSettingIntegerValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + int someInt = randomInt(); jps.setInt(1, someInt); assertEquals(someInt, value(jps)); assertEquals(INTEGER, jdbcType(jps)); - + jps.setObject(1, someInt); assertEquals(someInt, value(jps)); assertEquals(INTEGER, jdbcType(jps)); assertTrue(value(jps) instanceof Integer); - + jps.setObject(1, someInt, Types.VARCHAR); assertEquals(String.valueOf(someInt), value(jps)); assertEquals(VARCHAR, jdbcType(jps)); - + jps.setObject(1, someInt, Types.FLOAT); assertEquals(Double.valueOf(someInt), value(jps)); assertTrue(value(jps) instanceof Double); assertEquals(FLOAT, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingIntegerValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); int someInt = randomInt(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someInt, Types.TIMESTAMP)); assertEquals("Conversion from type [INTEGER] to [Timestamp] not supported", sqle.getMessage()); - + Integer randomIntNotShort = randomIntBetween(32768, Integer.MAX_VALUE); sqle = expectThrows(SQLException.class, () -> jps.setObject(1, randomIntNotShort, Types.SMALLINT)); assertEquals("Numeric " + randomIntNotShort + " out of range", sqle.getMessage()); - + sqle = expectThrows(SQLException.class, () -> jps.setObject(1, randomIntNotShort, Types.TINYINT)); assertEquals("Numeric " + randomIntNotShort + " out of range", sqle.getMessage()); } - + public void testSettingLongValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + long someLong = randomLong(); jps.setLong(1, someLong); assertEquals(someLong, value(jps)); assertEquals(BIGINT, jdbcType(jps)); - + jps.setObject(1, someLong); assertEquals(someLong, value(jps)); assertEquals(BIGINT, jdbcType(jps)); assertTrue(value(jps) instanceof Long); - + jps.setObject(1, someLong, Types.VARCHAR); assertEquals(String.valueOf(someLong), value(jps)); assertEquals(VARCHAR, jdbcType(jps)); - + jps.setObject(1, someLong, Types.DOUBLE); assertEquals((double) someLong, value(jps)); assertEquals(DOUBLE, jdbcType(jps)); - + jps.setObject(1, someLong, Types.FLOAT); assertEquals((double) someLong, value(jps)); assertEquals(FLOAT, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingLongValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); long someLong = randomLong(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someLong, Types.TIMESTAMP)); assertEquals("Conversion from type [BIGINT] to [Timestamp] not supported", sqle.getMessage()); - + Long randomLongNotShort = randomLongBetween(Integer.MAX_VALUE + 1, Long.MAX_VALUE); sqle = expectThrows(SQLException.class, () -> jps.setObject(1, randomLongNotShort, Types.INTEGER)); assertEquals("Numeric " + randomLongNotShort + " out of range", sqle.getMessage()); - + sqle = expectThrows(SQLException.class, () -> jps.setObject(1, randomLongNotShort, Types.SMALLINT)); assertEquals("Numeric " + randomLongNotShort + " out of range", sqle.getMessage()); } - + public void testSettingFloatValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + float someFloat = randomFloat(); jps.setFloat(1, someFloat); assertEquals(someFloat, value(jps)); assertEquals(REAL, jdbcType(jps)); - + jps.setObject(1, someFloat); assertEquals(someFloat, value(jps)); assertEquals(REAL, jdbcType(jps)); assertTrue(value(jps) instanceof Float); - + jps.setObject(1, someFloat, Types.VARCHAR); assertEquals(String.valueOf(someFloat), value(jps)); assertEquals(VARCHAR, jdbcType(jps)); - + jps.setObject(1, someFloat, Types.DOUBLE); assertEquals((double) someFloat, value(jps)); assertEquals(DOUBLE, jdbcType(jps)); - + jps.setObject(1, someFloat, Types.FLOAT); assertEquals((double) someFloat, value(jps)); assertEquals(FLOAT, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingFloatValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); float someFloat = randomFloat(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someFloat, Types.TIMESTAMP)); assertEquals("Conversion from type [REAL] to [Timestamp] not supported", sqle.getMessage()); - + Float floatNotInt = 5_155_000_000f; sqle = expectThrows(SQLException.class, () -> jps.setObject(1, floatNotInt, Types.INTEGER)); - assertEquals(String.format(Locale.ROOT, "Numeric %s out of range", + assertEquals(String.format(Locale.ROOT, "Numeric %s out of range", Long.toString(Math.round(floatNotInt.doubleValue()))), sqle.getMessage()); - + sqle = expectThrows(SQLException.class, () -> jps.setObject(1, floatNotInt, Types.SMALLINT)); - assertEquals(String.format(Locale.ROOT, "Numeric %s out of range", + assertEquals(String.format(Locale.ROOT, "Numeric %s out of range", Long.toString(Math.round(floatNotInt.doubleValue()))), sqle.getMessage()); } - + public void testSettingDoubleValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + double someDouble = randomDouble(); jps.setDouble(1, someDouble); assertEquals(someDouble, value(jps)); assertEquals(DOUBLE, jdbcType(jps)); - + jps.setObject(1, someDouble); assertEquals(someDouble, value(jps)); assertEquals(DOUBLE, jdbcType(jps)); assertTrue(value(jps) instanceof Double); - + jps.setObject(1, someDouble, Types.VARCHAR); assertEquals(String.valueOf(someDouble), value(jps)); assertEquals(VARCHAR, jdbcType(jps)); - + jps.setObject(1, someDouble, Types.REAL); - assertEquals(new Float(someDouble), value(jps)); + assertEquals((float) someDouble, value(jps)); assertEquals(REAL, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingDoubleValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); double someDouble = randomDouble(); - + SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someDouble, Types.TIMESTAMP)); assertEquals("Conversion from type [DOUBLE] to [Timestamp] not supported", sqle.getMessage()); - + Double doubleNotInt = 5_155_000_000d; sqle = expectThrows(SQLException.class, () -> jps.setObject(1, doubleNotInt, Types.INTEGER)); - assertEquals(String.format(Locale.ROOT, "Numeric %s out of range", + assertEquals(String.format(Locale.ROOT, "Numeric %s out of range", Long.toString(((Number) doubleNotInt).longValue())), sqle.getMessage()); } - + public void testUnsupportedClasses() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); SQLFeatureNotSupportedException sfnse = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, new Struct() { @@ -341,23 +341,23 @@ public Object[] getAttributes() throws SQLException { } })); assertEquals("Objects of type java.sql.Struct are not supported", sfnse.getMessage()); - + sfnse = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, new URL("http://test"))); assertEquals("Objects of type java.net.URL are not supported", sfnse.getMessage()); - + sfnse = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setURL(1, new URL("http://test"))); assertEquals("Objects of type java.net.URL are not supported", sfnse.getMessage()); - + sfnse = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, this, Types.TIMESTAMP)); assertEquals("Conversion from type " + this.getClass().getName() + " to TIMESTAMP not supported", sfnse.getMessage()); - + SQLException se = expectThrows(SQLException.class, () -> jps.setObject(1, this, 1_000_000)); assertEquals("Type:1000000 is not a valid Types.java value.", se.getMessage()); - + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> jps.setObject(1, randomShort(), Types.CHAR)); assertEquals("Unsupported JDBC type [CHAR]", iae.getMessage()); } - + public void testSettingTimestampValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); @@ -365,186 +365,186 @@ public void testSettingTimestampValues() throws SQLException { jps.setTimestamp(1, someTimestamp); assertEquals(someTimestamp.getTime(), ((Date)value(jps)).getTime()); assertEquals(TIMESTAMP, jdbcType(jps)); - + Calendar nonDefaultCal = randomCalendar(); // February 29th, 2016. 01:17:55 GMT = 1456708675000 millis since epoch jps.setTimestamp(1, new Timestamp(1456708675000L), nonDefaultCal); assertEquals(1456708675000L, convertFromUTCtoCalendar(((Date)value(jps)), nonDefaultCal)); assertEquals(TIMESTAMP, jdbcType(jps)); - + long beforeEpochTime = -randomMillisSinceEpoch(); jps.setTimestamp(1, new Timestamp(beforeEpochTime), nonDefaultCal); assertEquals(beforeEpochTime, convertFromUTCtoCalendar(((Date)value(jps)), nonDefaultCal)); assertTrue(value(jps) instanceof java.util.Date); - + jps.setObject(1, someTimestamp, Types.VARCHAR); assertEquals(someTimestamp.toString(), value(jps).toString()); assertEquals(VARCHAR, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingTimestampValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); Timestamp someTimestamp = new Timestamp(randomMillisSinceEpoch()); - + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, someTimestamp, Types.INTEGER)); assertEquals("Conversion from type java.sql.Timestamp to INTEGER not supported", sqle.getMessage()); } public void testSettingTimeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + Time time = new Time(4675000); Calendar nonDefaultCal = randomCalendar(); jps.setTime(1, time, nonDefaultCal); assertEquals(4675000, convertFromUTCtoCalendar(((Date)value(jps)), nonDefaultCal)); assertEquals(TIMESTAMP, jdbcType(jps)); assertTrue(value(jps) instanceof java.util.Date); - + jps.setObject(1, time, Types.VARCHAR); assertEquals(time.toString(), value(jps).toString()); assertEquals(VARCHAR, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingTimeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); Time time = new Time(4675000); - + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, time, Types.INTEGER)); assertEquals("Conversion from type java.sql.Time to INTEGER not supported", sqle.getMessage()); } - + public void testSettingSqlDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + java.sql.Date someSqlDate = new java.sql.Date(randomMillisSinceEpoch()); jps.setDate(1, someSqlDate); assertEquals(someSqlDate.getTime(), ((Date)value(jps)).getTime()); assertEquals(TIMESTAMP, jdbcType(jps)); - + someSqlDate = new java.sql.Date(randomMillisSinceEpoch()); Calendar nonDefaultCal = randomCalendar(); jps.setDate(1, someSqlDate, nonDefaultCal); assertEquals(someSqlDate.getTime(), convertFromUTCtoCalendar(((Date)value(jps)), nonDefaultCal)); assertEquals(TIMESTAMP, jdbcType(jps)); assertTrue(value(jps) instanceof java.util.Date); - + jps.setObject(1, someSqlDate, Types.VARCHAR); assertEquals(someSqlDate.toString(), value(jps).toString()); assertEquals(VARCHAR, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingSqlDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); java.sql.Date someSqlDate = new java.sql.Date(randomMillisSinceEpoch()); - - SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, + + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, new java.sql.Date(randomMillisSinceEpoch()), Types.DOUBLE)); assertEquals("Conversion from type " + someSqlDate.getClass().getName() + " to DOUBLE not supported", sqle.getMessage()); } - + public void testSettingCalendarValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); Calendar someCalendar = randomCalendar(); someCalendar.setTimeInMillis(randomMillisSinceEpoch()); - + jps.setObject(1, someCalendar); assertEquals(someCalendar.getTime(), (Date) value(jps)); assertEquals(TIMESTAMP, jdbcType(jps)); assertTrue(value(jps) instanceof java.util.Date); - + jps.setObject(1, someCalendar, Types.VARCHAR); assertEquals(someCalendar.toString(), value(jps).toString()); assertEquals(VARCHAR, jdbcType(jps)); - + Calendar nonDefaultCal = randomCalendar(); jps.setObject(1, nonDefaultCal); assertEquals(nonDefaultCal.getTime(), (Date) value(jps)); assertEquals(TIMESTAMP, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingCalendarValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); Calendar someCalendar = randomCalendar(); - + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, someCalendar, Types.DOUBLE)); assertEquals("Conversion from type " + someCalendar.getClass().getName() + " to DOUBLE not supported", sqle.getMessage()); } - + public void testSettingDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); Date someDate = new Date(randomMillisSinceEpoch()); - + jps.setObject(1, someDate); assertEquals(someDate, (Date) value(jps)); assertEquals(TIMESTAMP, jdbcType(jps)); assertTrue(value(jps) instanceof java.util.Date); - + jps.setObject(1, someDate, Types.VARCHAR); assertEquals(someDate.toString(), value(jps).toString()); assertEquals(VARCHAR, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); Date someDate = new Date(randomMillisSinceEpoch()); - + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, someDate, Types.BIGINT)); assertEquals("Conversion from type " + someDate.getClass().getName() + " to BIGINT not supported", sqle.getMessage()); } - + public void testSettingLocalDateTimeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); LocalDateTime ldt = LocalDateTime.now(Clock.systemDefaultZone()); - + jps.setObject(1, ldt); assertEquals(Date.class, value(jps).getClass()); assertEquals(TIMESTAMP, jdbcType(jps)); assertTrue(value(jps) instanceof java.util.Date); - + jps.setObject(1, ldt, Types.VARCHAR); assertEquals(ldt.toString(), value(jps).toString()); assertEquals(VARCHAR, jdbcType(jps)); } - + public void testThrownExceptionsWhenSettingLocalDateTimeValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); LocalDateTime ldt = LocalDateTime.now(Clock.systemDefaultZone()); - + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, ldt, Types.BIGINT)); assertEquals("Conversion from type " + ldt.getClass().getName() + " to BIGINT not supported", sqle.getMessage()); } - + public void testSettingByteArrayValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - + byte[] buffer = "some data".getBytes(StandardCharsets.UTF_8); jps.setBytes(1, buffer); assertEquals(byte[].class, value(jps).getClass()); assertEquals(VARBINARY, jdbcType(jps)); - + jps.setObject(1, buffer); assertEquals(byte[].class, value(jps).getClass()); assertEquals(VARBINARY, jdbcType(jps)); assertTrue(value(jps) instanceof byte[]); - + jps.setObject(1, buffer, Types.VARBINARY); assertEquals((byte[]) value(jps), buffer); assertEquals(VARBINARY, jdbcType(jps)); - + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, buffer, Types.VARCHAR)); assertEquals("Conversion from type byte[] to VARCHAR not supported", sqle.getMessage()); - + sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, buffer, Types.DOUBLE)); assertEquals("Conversion from type byte[] to DOUBLE not supported", sqle.getMessage()); } - + public void testThrownExceptionsWhenSettingByteArrayValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); byte[] buffer = "foo".getBytes(StandardCharsets.UTF_8); - + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, buffer, Types.VARCHAR)); assertEquals("Conversion from type byte[] to VARCHAR not supported", sqle.getMessage()); - + sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, buffer, Types.DOUBLE)); assertEquals("Conversion from type byte[] to DOUBLE not supported", sqle.getMessage()); } @@ -564,14 +564,14 @@ private JDBCType jdbcType(JdbcPreparedStatement jps) throws SQLException { private Object value(JdbcPreparedStatement jps) throws SQLException { return jps.query.getParam(1).value; } - + private Calendar randomCalendar() { return Calendar.getInstance(randomTimeZone(), Locale.ROOT); } - + /* * Converts from UTC to the provided Calendar. - * Helps checking if the converted date/time values using Calendars in set*(...,Calendar) methods did convert + * Helps checking if the converted date/time values using Calendars in set*(...,Calendar) methods did convert * the values correctly to UTC. */ private long convertFromUTCtoCalendar(Date date, Calendar nonDefaultCal) throws SQLException { diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/condition/CompareConditionTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/condition/CompareConditionTests.java index 7331be4553bb..7c3abd1d808e 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/condition/CompareConditionTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/condition/CompareConditionTests.java @@ -33,7 +33,7 @@ public void testOpEvalEQ() throws Exception { assertThat(CompareCondition.Op.EQ.eval(null, null), is(true)); assertThat(CompareCondition.Op.EQ.eval(4, 3.0), is(false)); assertThat(CompareCondition.Op.EQ.eval(3, 3.0), is(true)); - assertThat(CompareCondition.Op.EQ.eval(2, new Float(3.0)), is(false)); + assertThat(CompareCondition.Op.EQ.eval(2, Float.valueOf((float)3.0)), is(false)); assertThat(CompareCondition.Op.EQ.eval(3, null), is(false)); assertThat(CompareCondition.Op.EQ.eval(2, "2"), is(true)); // comparing as strings assertThat(CompareCondition.Op.EQ.eval(3, "4"), is(false)); // comparing as strings @@ -59,7 +59,7 @@ public void testOpEvalNotEQ() throws Exception { assertThat(CompareCondition.Op.NOT_EQ.eval(null, null), is(false)); assertThat(CompareCondition.Op.NOT_EQ.eval(4, 3.0), is(true)); assertThat(CompareCondition.Op.NOT_EQ.eval(3, 3.0), is(false)); - assertThat(CompareCondition.Op.NOT_EQ.eval(2, new Float(3.0)), is(true)); + assertThat(CompareCondition.Op.NOT_EQ.eval(2, Float.valueOf((float)3.0)), is(true)); assertThat(CompareCondition.Op.NOT_EQ.eval(3, null), is(true)); assertThat(CompareCondition.Op.NOT_EQ.eval(2, "2"), is(false)); // comparing as strings assertThat(CompareCondition.Op.NOT_EQ.eval(3, "4"), is(true)); // comparing as strings @@ -83,7 +83,7 @@ public void testOpEvalNotEQ() throws Exception { public void testOpEvalGTE() throws Exception { assertThat(CompareCondition.Op.GTE.eval(4, 3.0), is(true)); assertThat(CompareCondition.Op.GTE.eval(3, 3.0), is(true)); - assertThat(CompareCondition.Op.GTE.eval(2, new Float(3.0)), is(false)); + assertThat(CompareCondition.Op.GTE.eval(2, Float.valueOf((float)3.0)), is(false)); assertThat(CompareCondition.Op.GTE.eval(3, null), is(false)); assertThat(CompareCondition.Op.GTE.eval(3, "2"), is(true)); // comparing as strings assertThat(CompareCondition.Op.GTE.eval(3, "4"), is(false)); // comparing as strings @@ -103,7 +103,7 @@ public void testOpEvalGTE() throws Exception { public void testOpEvalGT() throws Exception { assertThat(CompareCondition.Op.GT.eval(4, 3.0), is(true)); assertThat(CompareCondition.Op.GT.eval(3, 3.0), is(false)); - assertThat(CompareCondition.Op.GT.eval(2, new Float(3.0)), is(false)); + assertThat(CompareCondition.Op.GT.eval(2, Float.valueOf((float)3.0)), is(false)); assertThat(CompareCondition.Op.GT.eval(3, null), is(false)); assertThat(CompareCondition.Op.GT.eval(3, "2"), is(true)); // comparing as strings assertThat(CompareCondition.Op.GT.eval(3, "4"), is(false)); // comparing as strings @@ -124,7 +124,7 @@ public void testOpEvalGT() throws Exception { public void testOpEvalLTE() throws Exception { assertThat(CompareCondition.Op.LTE.eval(4, 3.0), is(false)); assertThat(CompareCondition.Op.LTE.eval(3, 3.0), is(true)); - assertThat(CompareCondition.Op.LTE.eval(2, new Float(3.0)), is(true)); + assertThat(CompareCondition.Op.LTE.eval(2, Float.valueOf((float) 3.0)), is(true)); assertThat(CompareCondition.Op.LTE.eval(3, null), is(false)); assertThat(CompareCondition.Op.LTE.eval(3, "2"), is(false)); // comparing as strings assertThat(CompareCondition.Op.LTE.eval(3, "4"), is(true)); // comparing as strings @@ -144,7 +144,7 @@ public void testOpEvalLTE() throws Exception { public void testOpEvalLT() throws Exception { assertThat(CompareCondition.Op.LT.eval(4, 3.0), is(false)); assertThat(CompareCondition.Op.LT.eval(3, 3.0), is(false)); - assertThat(CompareCondition.Op.LT.eval(2, new Float(3.0)), is(true)); + assertThat(CompareCondition.Op.LT.eval(2, Float.valueOf((float) 3.0)), is(true)); assertThat(CompareCondition.Op.LT.eval(3, null), is(false)); assertThat(CompareCondition.Op.LT.eval(3, "2"), is(false)); // comparing as strings assertThat(CompareCondition.Op.LT.eval(3, "4"), is(true)); // comparing as strings From dce72c798508ce48e91a50f684113868f8fc9a25 Mon Sep 17 00:00:00 2001 From: Tim Ryan Date: Mon, 20 Aug 2018 02:54:03 -0400 Subject: [PATCH 049/283] Fix some small issues in the getting started docs (#30346) * Modified a reference to real time to match the previous line reference of realtime. * Modified eg to e.g. as it's an abbreviation for the latin exempli gratia * Added missing pronoun to `_executing_filters` section. --- docs/reference/getting-started.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/getting-started.asciidoc b/docs/reference/getting-started.asciidoc index 3e44b2c41f67..8229f74bdd05 100755 --- a/docs/reference/getting-started.asciidoc +++ b/docs/reference/getting-started.asciidoc @@ -23,7 +23,7 @@ There are a few concepts that are core to Elasticsearch. Understanding these con [float] === Near Realtime (NRT) -Elasticsearch is a near real time search platform. What this means is there is a slight latency (normally one second) from the time you index a document until the time it becomes searchable. +Elasticsearch is a near-realtime search platform. What this means is there is a slight latency (normally one second) from the time you index a document until the time it becomes searchable. [float] === Cluster @@ -59,7 +59,7 @@ In a single cluster, you can define as many indexes as you want. deprecated[6.0.0,See <>] -A type used to be a logical category/partition of your index to allow you to store different types of documents in the same index, eg one type for users, another type for blog posts. It is no longer possible to create multiple types in an index, and the whole concept of types will be removed in a later version. See <> for more. +A type used to be a logical category/partition of your index to allow you to store different types of documents in the same index, e.g. one type for users, another type for blog posts. It is no longer possible to create multiple types in an index, and the whole concept of types will be removed in a later version. See <> for more. [float] === Document @@ -1066,7 +1066,7 @@ In the previous section, we skipped over a little detail called the document sco But queries do not always need to produce scores, in particular when they are only used for "filtering" the document set. Elasticsearch detects these situations and automatically optimizes query execution in order not to compute useless scores. -The {ref}/query-dsl-bool-query.html[`bool` query] that we introduced in the previous section also supports `filter` clauses which allow to use a query to restrict the documents that will be matched by other clauses, without changing how scores are computed. As an example, let's introduce the {ref}/query-dsl-range-query.html[`range` query], which allows us to filter documents by a range of values. This is generally used for numeric or date filtering. +The {ref}/query-dsl-bool-query.html[`bool` query] that we introduced in the previous section also supports `filter` clauses which allow us to use a query to restrict the documents that will be matched by other clauses, without changing how scores are computed. As an example, let's introduce the {ref}/query-dsl-range-query.html[`range` query], which allows us to filter documents by a range of values. This is generally used for numeric or date filtering. This example uses a bool query to return all accounts with balances between 20000 and 30000, inclusive. In other words, we want to find accounts with a balance that is greater than or equal to 20000 and less than or equal to 30000. From 3fa36807f87ad90af593e86f9ed843ced3260973 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 20 Aug 2018 09:18:51 +0200 Subject: [PATCH 050/283] Watcher: Properly find next valid date in cron expressions (#32734) When a list/an array of cron expressions is provided, and one of those addresses is already expired, the expired one will be considered as an option instead of the valid next one. This commit also reduces the visibility of the CronnableSchedule and refactors a comparator to look like java 8. --- .../trigger/schedule/CronnableSchedule.java | 21 +++++++++------- .../trigger/schedule/CronScheduleTests.java | 25 +++++++++++++------ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java index ec309c69476c..695c9b192eaa 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java @@ -13,20 +13,15 @@ public abstract class CronnableSchedule implements Schedule { - private static final Comparator CRON_COMPARATOR = new Comparator() { - @Override - public int compare(Cron c1, Cron c2) { - return c1.expression().compareTo(c2.expression()); - } - }; + private static final Comparator CRON_COMPARATOR = Comparator.comparing(Cron::expression); protected final Cron[] crons; - public CronnableSchedule(String... expressions) { + CronnableSchedule(String... expressions) { this(crons(expressions)); } - public CronnableSchedule(Cron... crons) { + private CronnableSchedule(Cron... crons) { assert crons.length > 0; this.crons = crons; Arrays.sort(crons, CRON_COMPARATOR); @@ -37,7 +32,15 @@ public long nextScheduledTimeAfter(long startTime, long time) { assert time >= startTime; long nextTime = Long.MAX_VALUE; for (Cron cron : crons) { - nextTime = Math.min(nextTime, cron.getNextValidTimeAfter(time)); + long nextValidTimeAfter = cron.getNextValidTimeAfter(time); + + boolean previousCronExpired = nextTime == -1; + boolean currentCronValid = nextValidTimeAfter > -1; + if (previousCronExpired && currentCronValid) { + nextTime = nextValidTimeAfter; + } else { + nextTime = Math.min(nextTime, nextValidTimeAfter); + } } return nextTime; } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronScheduleTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronScheduleTests.java index 1ade767410b5..6cbdf6e12265 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronScheduleTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronScheduleTests.java @@ -11,11 +11,15 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.hasItemInArray; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; public class CronScheduleTests extends ScheduleTestCase { public void testInvalid() throws Exception { @@ -54,18 +58,25 @@ public void testParseMultiple() throws Exception { assertThat(crons, hasItemInArray("0 0/3 * * * ?")); } + public void testMultipleCronsNextScheduledAfter() { + CronSchedule schedule = new CronSchedule("0 5 9 1 1 ? 2019", "0 5 9 1 1 ? 2020", "0 5 9 1 1 ? 2017"); + ZonedDateTime start2019 = ZonedDateTime.of(2019, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + ZonedDateTime start2020 = ZonedDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + long firstSchedule = schedule.nextScheduledTimeAfter(0, start2019.toInstant().toEpochMilli()); + long secondSchedule = schedule.nextScheduledTimeAfter(0, start2020.toInstant().toEpochMilli()); + + assertThat(firstSchedule, is(not(-1L))); + assertThat(secondSchedule, is(not(-1L))); + assertThat(firstSchedule, is(not(secondSchedule))); + } + public void testParseInvalidBadExpression() throws Exception { XContentBuilder builder = jsonBuilder().value("0 0/5 * * ?"); BytesReference bytes = BytesReference.bytes(builder); XContentParser parser = createParser(JsonXContent.jsonXContent, bytes); parser.nextToken(); - try { - new CronSchedule.Parser().parse(parser); - fail("expected cron parsing to fail when using invalid cron expression"); - } catch (ElasticsearchParseException pe) { - // expected - assertThat(pe.getCause(), instanceOf(IllegalArgumentException.class)); - } + ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, () -> new CronSchedule.Parser().parse(parser)); + assertThat(e.getCause(), instanceOf(IllegalArgumentException.class)); } public void testParseInvalidEmpty() throws Exception { From e143cce86594eae12da38bfb62de8005ac5dfea2 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Mon, 20 Aug 2018 17:23:14 +1000 Subject: [PATCH 051/283] [Kerberos] Add documentation for Kerberos realm (#32662) This commit adds documentation for configuring Kerberos realm. Configuring Kerberos realm documentation highlights important terminology and requirements before creating Kerberos realm. Most of the documentation is centered around configuration from Elasticsearch rather than go deep into Kerberos implementation. Kerberos realm settings are mentioned in the security settings for Kerberos realm. --- .../settings/security-settings.asciidoc | 27 +++ .../configuring-kerberos-realm.asciidoc | 170 ++++++++++++++++++ .../docs/en/security/configuring-es.asciidoc | 3 + 3 files changed, 200 insertions(+) create mode 100644 x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 4f29b0549b3a..9aa4483a8f20 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -1056,6 +1056,33 @@ Specifies the supported protocols for TLS/SSL. Specifies the cipher suites that should be supported. +[float] +[[ref-kerberos-settings]] +===== Kerberos realm settings + +For a Kerberos realm, the `type` must be set to `kerberos`. In addition to the +<>, you can specify +the following settings: + +`keytab.path`:: Specifies the path to the Kerberos keytab file that contains the +service principal used by this {es} node. This must be a location within the +{es} configuration directory and the file must have read permissions. Required. + +`remove_realm_name`:: Set to `true` to remove the realm part of principal names. +Principal names in Kerberos have the form `user/instance@REALM`. If this option +is `true`, the realm part (`@REALM`) will not be included in the username. +Defaults to `false`. + +`krb.debug`:: Set to `true` to enable debug logs for the Java login module that +provides support for Kerberos authentication. Defaults to `false`. + +`cache.ttl`:: The time-to-live for cached user entries. A user is cached for +this period of time. Specify the time period using the standard {es} +<>. Defaults to `20m`. + +`cache.max_users`:: The maximum number of user entries that can live in the +cache at any given time. Defaults to 100,000. + [float] [[load-balancing]] ===== Load balancing and failover diff --git a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc new file mode 100644 index 000000000000..30968355f3ca --- /dev/null +++ b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc @@ -0,0 +1,170 @@ +[role="xpack"] +[[configuring-kerberos-realm]] +=== Configuring a Kerberos realm + +Kerberos is used to protect services and uses a ticket-based authentication +protocol to authenticate users. +You can configure {es} to use the Kerberos V5 authentication protocol, which is +an industry standard protocol, to authenticate users. +In this scenario, clients must present Kerberos tickets for authentication. + +In Kerberos, users authenticate with an authentication service and later +with a ticket granting service to generate a TGT (ticket-granting ticket). +This ticket is then presented to the service for authentication. +Refer to your Kerberos installation documentation for more information about +obtaining TGT. {es} clients must first obtain a TGT then initiate the process of +authenticating with {es}. + +For a summary of Kerberos terminology, see {stack-ov}/kerberos-realm.html[Kerberos authentication]. + +==== Before you begin + +. Deploy Kerberos. ++ +-- +You must have the Kerberos infrastructure set up in your environment. + +NOTE: Kerberos requires a lot of external services to function properly, such as +time synchronization between all machines and working forward and reverse DNS +mappings in your domain. Refer to your Kerberos documentation for more details. + +These instructions do not cover setting up and configuring your Kerberos +deployment. Where examples are provided, they pertain to an MIT Kerberos V5 +deployment. For more information, see +http://web.mit.edu/kerberos/www/index.html[MIT Kerberos documentation] +-- + +. Configure Java GSS. ++ +-- + +{es} uses Java GSS framework support for Kerberos authentication. +To support Kerberos authentication, {es} needs the following files: + +* `krb5.conf`, a Kerberos configuration file +* A `keytab` file that contains credentials for the {es} service principal + +The configuration requirements depend on your Kerberos setup. Refer to your +Kerberos documentation to configure the `krb5.conf` file. + +For more information on Java GSS, see +https://docs.oracle.com/javase/10/security/kerberos-requirements1.htm[Java GSS Kerberos requirements] +-- + +==== Create a Kerberos realm + +To configure a Kerberos realm in {es}: + +. Configure the JVM to find the Kerberos configuration file. ++ +-- +{es} uses Java GSS and JAAS Krb5LoginModule to support Kerberos authentication +using a Simple and Protected GSSAPI Negotiation Mechanism (SPNEGO) mechanism. +The Kerberos configuration file (`krb5.conf`) provides information such as the +default realm, the Key Distribution Center (KDC), and other configuration details +required for Kerberos authentication. When the JVM needs some configuration +properties, it tries to find those values by locating and loading this file. The +JVM system property to configure the file path is `java.security.krb5.conf`. To +configure JVM system properties see {ref}/jvm-options.html[configuring jvm options]. +If this system property is not specified, Java tries to locate the file based on +the conventions. + +TIP: It is recommended that this system property be configured for {es}. +The method for setting this property depends on your Kerberos infrastructure. +Refer to your Kerberos documentation for more details. + +For more information, see http://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html[krb5.conf] + +-- + +. Create a keytab for the {es} node. ++ +-- +A keytab is a file that stores pairs of principals and encryption keys. {es} +uses the keys from the keytab to decrypt the tickets presented by the user. You +must create a keytab for {es} by using the tools provided by your Kerberos +implementation. For example, some tools that create keytabs are `ktpass.exe` on +Windows and `kadmin` for MIT Kerberos. +-- + +. Put the keytab file in the {es} configuration directory. ++ +-- +Make sure that this keytab file has read permissions. This file contains +credentials, therefore you must take appropriate measures to protect it. + +IMPORTANT: {es} uses Kerberos on the HTTP network layer, therefore there must be +a keytab file for the HTTP service principal on every {es} node. The service +principal name must have the format `HTTP/es.domain.local@ES.DOMAIN.LOCAL`. +The keytab files are unique for each node since they include the hostname. +An {es} node can act as any principal a client requests as long as that +principal and its credentials are found in the configured keytab. + +-- + +. Create a Kerberos realm. ++ +-- + +To enable Kerberos authentication in {es}, you must add a Kerberos realm in the +realm chain. + +NOTE: You can configure only one Kerberos realm on {es} nodes. + +To configure a Kerberos realm, there are a few mandatory realm settings and +other optional settings that you need to configure in the `elasticsearch.yml` +configuration file. Add a realm of type `kerberos` under the +`xpack.security.authc.realms` namespace. + +The most common configuration for a Kerberos realm is as follows: + +[source, yaml] +------------------------------------------------------------ +xpack.security.authc.realms.kerb1: + type: kerberos + order: 3 + keytab.path: es.keytab + remove_realm_name: false +------------------------------------------------------------ + +The `username` is extracted from the ticket presented by user and usually has +the format `username@REALM`. This `username` is used for mapping +roles to the user. If realm setting `remove_realm_name` is +set to `true`, the realm part (`@REALM`) is removed. The resulting `username` +is used for role mapping. + +For detailed information of available realm settings, +see {ref}/security-settings.html#ref-kerberos-settings[Kerberos realm settings]. + +-- + +. Restart {es} + +. Map Kerberos users to roles. ++ +-- + +The `kerberos` realm enables you to map Kerberos users to roles. You can +configure these role mappings by using the +{ref}/security-api-role-mapping.html[role-mapping API]. You identify +users by their `username` field. + +The following example uses the role mapping API to map `user@REALM` to the roles +`monitoring` and `user`: + +[source,js] +-------------------------------------------------- +POST _xpack/security/role_mapping/kerbrolemapping +{ + "roles" : [ "monitoring_user" ], + "enabled": true, + "rules" : { + "field" : { "username" : "user@REALM" } + } +} +-------------------------------------------------- +// CONSOLE + +For more information, see {stack-ov}/mapping-roles.html[Mapping users and groups to roles]. +-- + diff --git a/x-pack/docs/en/security/configuring-es.asciidoc b/x-pack/docs/en/security/configuring-es.asciidoc index 53f36afc7348..a13547263a58 100644 --- a/x-pack/docs/en/security/configuring-es.asciidoc +++ b/x-pack/docs/en/security/configuring-es.asciidoc @@ -77,6 +77,7 @@ user API. ** <>. ** <>. ** <>. +** <>. . Set up roles and users to control access to {es}. For example, to grant _John Doe_ full access to all indices that match @@ -142,5 +143,7 @@ include::authentication/configuring-ldap-realm.asciidoc[] include::authentication/configuring-native-realm.asciidoc[] 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[] include::{es-repo-dir}/settings/security-settings.asciidoc[] include::{es-repo-dir}/settings/audit-settings.asciidoc[] From 676091aafbdd7482118a71768a0dffa2889e2218 Mon Sep 17 00:00:00 2001 From: Jonathan Little Date: Mon, 20 Aug 2018 00:55:43 -0700 Subject: [PATCH 052/283] Protect ScriptedMetricIT test cases against failures on 0-doc shards (#32959) (#32968) Randomized test conditions that cause some shards to have no docs on them failed due to test asserts that relied on a lazy initialization side effect from the map script. After this fix: - Test cases with the relevant init script are protected - Test cases with the relevant combine or reduce scripts were already protected, because the combine and reduce scripts safely handle this case. --- .../search/aggregations/metrics/ScriptedMetricIT.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java index c000b7fb2289..f62598fa7c31 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java @@ -108,8 +108,14 @@ protected Map, Object>> pluginScripts() { aggScript(vars, state -> state.put((String) XContentMapValues.extractValue("params.param1", vars), XContentMapValues.extractValue("params.param2", vars)))); - scripts.put("vars.multiplier = 3", vars -> - ((Map) vars.get("vars")).put("multiplier", 3)); + scripts.put("vars.multiplier = 3", vars -> { + ((Map) vars.get("vars")).put("multiplier", 3); + + Map state = (Map) vars.get("state"); + state.put("list", new ArrayList()); + + return state; + }); scripts.put("state.list.add(vars.multiplier)", vars -> aggScript(vars, state -> { From b6e95cde3ace2355ddc03368c2821eccffea76cc Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Mon, 20 Aug 2018 10:05:25 +0100 Subject: [PATCH 053/283] Fixes libs:dissect when in eclipse --- settings.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/settings.gradle b/settings.gradle index bdd866e622bc..bdae1c396fda 100644 --- a/settings.gradle +++ b/settings.gradle @@ -84,6 +84,7 @@ if (isEclipse) { // for server-src and server-tests projects << 'server-tests' projects << 'libs:core-tests' + projects << 'libs:dissect-tests' projects << 'libs:nio-tests' projects << 'libs:x-content-tests' projects << 'libs:secure-sm-tests' @@ -103,6 +104,10 @@ if (isEclipse) { project(":libs:core").buildFileName = 'eclipse-build.gradle' project(":libs:core-tests").projectDir = new File(rootProject.projectDir, 'libs/core/src/test') project(":libs:core-tests").buildFileName = 'eclipse-build.gradle' + project(":libs:dissect").projectDir = new File(rootProject.projectDir, 'libs/dissect/src/main') + project(":libs:dissect").buildFileName = 'eclipse-build.gradle' + project(":libs:dissect-tests").projectDir = new File(rootProject.projectDir, 'libs/dissect/src/test') + project(":libs:dissect-tests").buildFileName = 'eclipse-build.gradle' project(":libs:nio").projectDir = new File(rootProject.projectDir, 'libs/nio/src/main') project(":libs:nio").buildFileName = 'eclipse-build.gradle' project(":libs:nio-tests").projectDir = new File(rootProject.projectDir, 'libs/nio/src/test') From e3700a9b8dec77691bc69b1e90c69198912c9f63 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Mon, 20 Aug 2018 13:46:21 +0300 Subject: [PATCH 054/283] Only configure publishing if it's applied externally (#32351) Only configure publishing if it's applied externally, reconfigure for hasClientJar --- .../elasticsearch/gradle/BuildPlugin.groovy | 3 +- .../gradle/plugin/PluginBuildPlugin.groovy | 109 ++++++------------ 2 files changed, 37 insertions(+), 75 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index bf3ffcabe2f6..e45ba7ce9dc1 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -528,11 +528,12 @@ class BuildPlugin implements Plugin { project.tasks.withType(GenerateMavenPom.class) { GenerateMavenPom generatePOMTask -> // The GenerateMavenPom task is aggressive about setting the destination, instead of fighting it, // just make a copy. + generatePOMTask.ext.pomFileName = "${project.archivesBaseName}-${project.version}.pom" doLast { project.copy { from generatePOMTask.destination into "${project.buildDir}/distributions" - rename { "${project.archivesBaseName}-${project.version}.pom" } + rename { generatePOMTask.ext.pomFileName } } } // build poms with assemble (if the assemble task exists) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy index 6f42e41beaa1..5216f0842742 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy @@ -19,23 +19,19 @@ package org.elasticsearch.gradle.plugin import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin -import nebula.plugin.info.scm.ScmInfoPlugin +import nebula.plugin.publishing.maven.MavenScmPlugin import org.elasticsearch.gradle.BuildPlugin import org.elasticsearch.gradle.NoticeTask import org.elasticsearch.gradle.test.RestIntegTestTask import org.elasticsearch.gradle.test.RunTask -import org.gradle.api.InvalidUserDataException import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.XmlProvider import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.plugins.MavenPublishPlugin +import org.gradle.api.publish.maven.tasks.GenerateMavenPom import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.bundling.Zip +import org.gradle.jvm.tasks.Jar -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption import java.util.regex.Matcher import java.util.regex.Pattern /** @@ -55,16 +51,10 @@ public class PluginBuildPlugin extends BuildPlugin { String name = project.pluginProperties.extension.name project.archivesBaseName = name - if (project.pluginProperties.extension.hasClientJar) { - // for plugins which work with the transport client, we copy the jar - // file to a new name, copy the nebula generated pom to the same name, - // and generate a different pom for the zip - addClientJarPomGeneration(project) - addClientJarTask(project) - } - // while the jar isn't normally published, we still at least build a pom of deps - // in case it is published, for instance when other plugins extend this plugin - configureJarPom(project) + // set teh project description so it will be picked up by publishing + project.description = project.pluginProperties.extension.description + + configurePublishing(project) project.integTestCluster.dependsOn(project.bundlePlugin) project.tasks.run.dependsOn(project.bundlePlugin) @@ -94,6 +84,32 @@ public class PluginBuildPlugin extends BuildPlugin { project.tasks.create('run', RunTask) // allow running ES with this plugin in the foreground of a build } + private void configurePublishing(Project project) { + // Only configure publishing if applied externally + if (project.pluginProperties.extension.hasClientJar) { + project.plugins.apply(MavenScmPlugin.class) + // Only change Jar tasks, we don't want a -client zip so we can't change archivesBaseName + project.tasks.withType(Jar) { + baseName = baseName + "-client" + } + // always configure publishing for client jars + project.plugins.apply(MavenScmPlugin.class) + project.publishing.publications.nebula(MavenPublication).artifactId( + project.pluginProperties.extension.name + "-client" + ) + project.tasks.withType(GenerateMavenPom.class) { GenerateMavenPom generatePOMTask -> + generatePOMTask.ext.pomFileName = "${project.archivesBaseName}-client-${project.version}.pom" + } + } else { + project.plugins.withType(MavenPublishPlugin).whenPluginAdded { + project.publishing.publications.nebula(MavenPublication).artifactId( + project.pluginProperties.extension.name + ) + } + + } + } + private static void configureDependencies(Project project) { project.dependencies { compileOnly "org.elasticsearch:elasticsearch:${project.versions.elasticsearch}" @@ -161,33 +177,6 @@ public class PluginBuildPlugin extends BuildPlugin { } /** Adds a task to move jar and associated files to a "-client" name. */ - protected static void addClientJarTask(Project project) { - Task clientJar = project.tasks.create('clientJar') - clientJar.dependsOn(project.jar, project.tasks.generatePomFileForClientJarPublication, project.javadocJar, project.sourcesJar) - clientJar.doFirst { - Path jarFile = project.jar.outputs.files.singleFile.toPath() - String clientFileName = jarFile.fileName.toString().replace(project.version, "client-${project.version}") - Files.copy(jarFile, jarFile.resolveSibling(clientFileName), StandardCopyOption.REPLACE_EXISTING) - - String clientPomFileName = clientFileName.replace('.jar', '.pom') - Files.copy( - project.tasks.generatePomFileForClientJarPublication.outputs.files.singleFile.toPath(), - jarFile.resolveSibling(clientPomFileName), - StandardCopyOption.REPLACE_EXISTING - ) - - String sourcesFileName = jarFile.fileName.toString().replace('.jar', '-sources.jar') - String clientSourcesFileName = clientFileName.replace('.jar', '-sources.jar') - Files.copy(jarFile.resolveSibling(sourcesFileName), jarFile.resolveSibling(clientSourcesFileName), - StandardCopyOption.REPLACE_EXISTING) - - String javadocFileName = jarFile.fileName.toString().replace('.jar', '-javadoc.jar') - String clientJavadocFileName = clientFileName.replace('.jar', '-javadoc.jar') - Files.copy(jarFile.resolveSibling(javadocFileName), jarFile.resolveSibling(clientJavadocFileName), - StandardCopyOption.REPLACE_EXISTING) - } - project.assemble.dependsOn(clientJar) - } static final Pattern GIT_PATTERN = Pattern.compile(/git@([^:]+):([^\.]+)\.git/) @@ -209,39 +198,11 @@ public class PluginBuildPlugin extends BuildPlugin { /** Adds nebula publishing task to generate a pom file for the plugin. */ protected static void addClientJarPomGeneration(Project project) { - project.plugins.apply(MavenPublishPlugin.class) - - project.publishing { - publications { - clientJar(MavenPublication) { - from project.components.java - artifactId = project.pluginProperties.extension.name + '-client' - pom.withXml { XmlProvider xml -> - Node root = xml.asNode() - root.appendNode('name', project.pluginProperties.extension.name) - root.appendNode('description', project.pluginProperties.extension.description) - root.appendNode('url', urlFromOrigin(project.scminfo.origin)) - Node scmNode = root.appendNode('scm') - scmNode.appendNode('url', project.scminfo.origin) - } - } - } - } + project.plugins.apply(MavenScmPlugin.class) + project.description = project.pluginProperties.extension.description } /** Configure the pom for the main jar of this plugin */ - protected static void configureJarPom(Project project) { - project.plugins.apply(ScmInfoPlugin.class) - project.plugins.apply(MavenPublishPlugin.class) - - project.publishing { - publications { - nebula(MavenPublication) { - artifactId project.pluginProperties.extension.name - } - } - } - } protected void addNoticeGeneration(Project project) { File licenseFile = project.pluginProperties.extension.licenseFile From f853f6f03c7eb0590def1be73458d33ccbe50087 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 20 Aug 2018 08:55:24 -0400 Subject: [PATCH 055/283] HLRC: Forbid all Elasticsearch logging infra (#32784) All of the Elasticsearch logging infrastructure relies on log4j but we don't want the high level rest client to rely on log4j2. All of its logging goes through commons-logging because our dependencies drag in commons logging already. Anyway, this bans direct use of Elasticsearch's logging infrastructure in the high level REST client. It is still possible to use it indirectly though and there isn't anything we can really do about that until we split the high level rest client from Elasticsearch's server jar. --- .../resources/forbidden/rest-high-level-signatures.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/rest-high-level/src/main/resources/forbidden/rest-high-level-signatures.txt b/client/rest-high-level/src/main/resources/forbidden/rest-high-level-signatures.txt index 33e136a66f44..cc179e12e316 100644 --- a/client/rest-high-level/src/main/resources/forbidden/rest-high-level-signatures.txt +++ b/client/rest-high-level/src/main/resources/forbidden/rest-high-level-signatures.txt @@ -20,5 +20,14 @@ org.apache.http.entity.ContentType#create(java.lang.String,java.lang.String) org.apache.http.entity.ContentType#create(java.lang.String,java.nio.charset.Charset) org.apache.http.entity.ContentType#create(java.lang.String,org.apache.http.NameValuePair[]) +@defaultMessage ES's logging infrastructure uses log4j2 which we don't want to force on high level rest client users +org.elasticsearch.common.logging.DeprecationLogger +org.elasticsearch.common.logging.ESLoggerFactory +org.elasticsearch.common.logging.LogConfigurator +org.elasticsearch.common.logging.LoggerMessageFormat +org.elasticsearch.common.logging.Loggers +org.elasticsearch.common.logging.NodeNamePatternConverter +org.elasticsearch.common.logging.PrefixLogger + @defaultMessage We can't rely on log4j2 being on the classpath so don't log deprecations! org.elasticsearch.common.xcontent.LoggingDeprecationHandler From a883e7dffcb96750cc961803d15c2e7de27f915d Mon Sep 17 00:00:00 2001 From: Yu Date: Mon, 20 Aug 2018 15:21:31 +0200 Subject: [PATCH 056/283] Update docs for node specifications (#30468) Expands and clarifies exactly what is and isn't allowed when specifying a subset of the nodes as targets of a cluster API, and adds missing links to this from the hot threads and cluster stats API docs. Co-authored-by: David Turner Co-authored-by: Yu --- docs/reference/cluster.asciidoc | 69 ++++++++++++++++--- .../cluster/nodes-hot-threads.asciidoc | 21 ++++-- docs/reference/cluster/stats.asciidoc | 9 +++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/docs/reference/cluster.asciidoc b/docs/reference/cluster.asciidoc index f093b6ebcfae..f92e364bae10 100644 --- a/docs/reference/cluster.asciidoc +++ b/docs/reference/cluster.asciidoc @@ -6,23 +6,70 @@ ["float",id="cluster-nodes"] == Node specification -Most cluster level APIs allow to specify which nodes to execute on (for -example, getting the node stats for a node). Nodes can be identified in -the APIs either using their internal node id, the node name, address, -custom attributes, or just the `_local` node receiving the request. For -example, here are some sample executions of nodes info: +Some cluster-level APIs may operate on a subset of the nodes which can be +specified with _node filters_. For example, the <>, +<>, and <> APIs +can all report results from a filtered set of nodes rather than from all nodes. + +_Node filters_ are written as a comma-separated list of individual filters, +each of which adds or removes nodes from the chosen subset. Each filter can be +one of the following: + +* `_all`, to add all nodes to the subset. +* `_local`, to add the local node to the subset. +* `_master`, to add the currently-elected master node to the subset. +* a node id or name, to add this node to the subset. +* an IP address or hostname, to add all matching nodes to the subset. +* a pattern, using `*` wildcards, which adds all nodes to the subset + whose name, address or hostname matches the pattern. +* `master:true`, `data:true`, `ingest:true` or `coordinating_only:true`, which + respectively add to the subset all master-eligible nodes, all data nodes, + all ingest nodes, and all coordinating-only nodes. +* `master:false`, `data:false`, `ingest:false` or `coordinating_only:false`, + which respectively remove from the subset all master-eligible nodes, all data + nodes, all ingest nodes, and all coordinating-only nodes. +* a pair of patterns, using `*` wildcards, of the form `attrname:attrvalue`, + which adds to the subset all nodes with a custom node attribute whose name + and value match the respective patterns. Custom node attributes are + configured by setting properties in the configuration file of the form + `node.attr.attrname: attrvalue`. + +NOTE: node filters run in the order in which they are given, which is important +if using filters that remove nodes from the set. For example +`_all,master:false` means all the nodes except the master-eligible ones, but +`master:false,_all` means the same as `_all` because the `_all` filter runs +after the `master:false` filter. + +NOTE: if no filters are given, the default is to select all nodes. However, if +any filters are given then they run starting with an empty chosen subset. This +means that filters such as `master:false` which remove nodes from the chosen +subset are only useful if they come after some other filters. When used on its +own, `master:false` selects no nodes. + +Here are some examples of the use of node filters with the +<> APIs. [source,js] -------------------------------------------------- -# Local +# If no filters are given, the default is to select all nodes +GET /_nodes +# Explicitly select all nodes +GET /_nodes/_all +# Select just the local node GET /_nodes/_local -# Address -GET /_nodes/10.0.0.3,10.0.0.4 -GET /_nodes/10.0.0.* -# Names +# Select the elected master node +GET /_nodes/_master +# Select nodes by name, which can include wildcards GET /_nodes/node_name_goes_here GET /_nodes/node_name_goes_* -# Attributes (set something like node.attr.rack: 2 in the config) +# Select nodes by address, which can include wildcards +GET /_nodes/10.0.0.3,10.0.0.4 +GET /_nodes/10.0.0.* +# Select nodes by role +GET /_nodes/_all,master:false +GET /_nodes/data:true,ingest:true +GET /_nodes/coordinating_only:true +# Select nodes by custom attribute (e.g. with something like `node.attr.rack: 2` in the configuration file) GET /_nodes/rack:2 GET /_nodes/ra*:2 GET /_nodes/ra*:2* diff --git a/docs/reference/cluster/nodes-hot-threads.asciidoc b/docs/reference/cluster/nodes-hot-threads.asciidoc index c8fa2c9bf7c4..541ee51a58ad 100644 --- a/docs/reference/cluster/nodes-hot-threads.asciidoc +++ b/docs/reference/cluster/nodes-hot-threads.asciidoc @@ -1,12 +1,23 @@ [[cluster-nodes-hot-threads]] == Nodes hot_threads -An API allowing to get the current hot threads on each node in the -cluster. Endpoints are `/_nodes/hot_threads`, and -`/_nodes/{nodesIds}/hot_threads`. +This API yields a breakdown of the hot threads on each selected node in the +cluster. Its endpoints are `/_nodes/hot_threads` and +`/_nodes/{nodes}/hot_threads`: -The output is plain text with a breakdown of each node's top hot -threads. Parameters allowed are: +[source,js] +-------------------------------------------------- +GET /_nodes/hot_threads +GET /_nodes/nodeId1,nodeId2/hot_threads +-------------------------------------------------- +// CONSOLE + +The first command gets the hot threads of all the nodes in the cluster. The +second command gets the hot threads of only `nodeId1` and `nodeId2`. Nodes can +be selected using <>. + +The output is plain text with a breakdown of each node's top hot threads. The +allowed parameters are: [horizontal] `threads`:: number of hot threads to provide, defaults to 3. diff --git a/docs/reference/cluster/stats.asciidoc b/docs/reference/cluster/stats.asciidoc index 3de850418719..78bccc8bd695 100644 --- a/docs/reference/cluster/stats.asciidoc +++ b/docs/reference/cluster/stats.asciidoc @@ -213,3 +213,12 @@ Will return, for example: // 3. All of the numbers and strings on the right hand side of *every* field in // the response are ignored. So we're really only asserting things about the // the shape of this response, not the values in it. + +This API can be restricted to a subset of the nodes using the `?nodeId` +parameter, which accepts <>: + +[source,js] +-------------------------------------------------- +GET /_cluster/stats?nodeId=node1,node*,master:false +-------------------------------------------------- +// CONSOLE From 6905ca9d6cbc9b1227b07f252f7c3598fb5a6f57 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 20 Aug 2018 17:01:10 +0300 Subject: [PATCH 057/283] Use settings from the context in BootstrapChecks (#32908) Use settings from the context in BootstrapChecks instead of passing them in the constructor --- .../FIPS140JKSKeystoreBootstrapCheck.java | 10 ++-------- .../FIPS140LicenseBootstrapCheck.java | 9 ++------- ...asswordHashingAlgorithmBootstrapCheck.java | 9 +-------- .../xpack/security/Security.java | 6 +++--- ...FIPS140JKSKeystoreBootstrapCheckTests.java | 14 ++++++------- .../FIPS140LicenseBootstrapCheckTests.java | 20 ++++++++++--------- ...rdHashingAlgorithmBootstrapCheckTests.java | 6 +++--- 7 files changed, 29 insertions(+), 45 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java index cd5a720eef1d..28f2756cf262 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheck.java @@ -13,12 +13,6 @@ public class FIPS140JKSKeystoreBootstrapCheck implements BootstrapCheck { - private final boolean fipsModeEnabled; - - FIPS140JKSKeystoreBootstrapCheck(Settings settings) { - this.fipsModeEnabled = XPackSettings.FIPS_MODE_ENABLED.get(settings); - } - /** * Test if the node fails the check. * @@ -28,7 +22,7 @@ public class FIPS140JKSKeystoreBootstrapCheck implements BootstrapCheck { @Override public BootstrapCheckResult check(BootstrapContext context) { - if (fipsModeEnabled) { + if (XPackSettings.FIPS_MODE_ENABLED.get(context.settings)) { final Settings settings = context.settings; Settings keystoreTypeSettings = settings.filter(k -> k.endsWith("keystore.type")) .filter(k -> settings.get(k).equalsIgnoreCase("jks")); @@ -50,6 +44,6 @@ public BootstrapCheckResult check(BootstrapContext context) { @Override public boolean alwaysEnforce() { - return fipsModeEnabled; + return true; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheck.java index d1bce0dcdd24..957276bdad2f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheck.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheck.java @@ -10,6 +10,7 @@ import org.elasticsearch.bootstrap.BootstrapContext; import org.elasticsearch.license.License; import org.elasticsearch.license.LicenseService; +import org.elasticsearch.xpack.core.XPackSettings; import java.util.EnumSet; @@ -21,15 +22,9 @@ final class FIPS140LicenseBootstrapCheck implements BootstrapCheck { static final EnumSet ALLOWED_LICENSE_OPERATION_MODES = EnumSet.of(License.OperationMode.PLATINUM, License.OperationMode.TRIAL); - private final boolean isInFipsMode; - - FIPS140LicenseBootstrapCheck(boolean isInFipsMode) { - this.isInFipsMode = isInFipsMode; - } - @Override public BootstrapCheckResult check(BootstrapContext context) { - if (isInFipsMode) { + if (XPackSettings.FIPS_MODE_ENABLED.get(context.settings)) { License license = LicenseService.getLicense(context.metaData); if (license != null && ALLOWED_LICENSE_OPERATION_MODES.contains(license.operationMode()) == false) { return BootstrapCheckResult.failure("FIPS mode is only allowed with a Platinum or Trial license"); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java index 751d63be4fb3..3faec3d74757 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheck.java @@ -7,19 +7,12 @@ import org.elasticsearch.bootstrap.BootstrapCheck; import org.elasticsearch.bootstrap.BootstrapContext; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.core.XPackSettings; import java.util.Locale; public class FIPS140PasswordHashingAlgorithmBootstrapCheck implements BootstrapCheck { - private final boolean fipsModeEnabled; - - FIPS140PasswordHashingAlgorithmBootstrapCheck(final Settings settings) { - this.fipsModeEnabled = XPackSettings.FIPS_MODE_ENABLED.get(settings); - } - /** * Test if the node fails the check. * @@ -28,7 +21,7 @@ public class FIPS140PasswordHashingAlgorithmBootstrapCheck implements BootstrapC */ @Override public BootstrapCheckResult check(final BootstrapContext context) { - if (fipsModeEnabled) { + if (XPackSettings.FIPS_MODE_ENABLED.get(context.settings)) { final String selectedAlgorithm = XPackSettings.PASSWORD_HASHING_ALGORITHM.get(context.settings); if (selectedAlgorithm.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) { return BootstrapCheckResult.failure("Only PBKDF2 is allowed for password hashing in a FIPS-140 JVM. Please set the " + 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 02910b5dd74f..2a392478cbcd 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 @@ -300,9 +300,9 @@ public Security(Settings settings, final Path configPath) { new PkiRealmBootstrapCheck(getSslService()), new TLSLicenseBootstrapCheck(), new FIPS140SecureSettingsBootstrapCheck(settings, env), - new FIPS140JKSKeystoreBootstrapCheck(settings), - new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings), - new FIPS140LicenseBootstrapCheck(XPackSettings.FIPS_MODE_ENABLED.get(settings)))); + new FIPS140JKSKeystoreBootstrapCheck(), + new FIPS140PasswordHashingAlgorithmBootstrapCheck(), + new FIPS140LicenseBootstrapCheck())); checks.addAll(InternalRealms.getBootstrapChecks(settings, env)); this.bootstrapChecks = Collections.unmodifiableList(checks); Automatons.updateMaxDeterminizedStates(settings); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java index 1d4da71e11b5..b659adf22cfc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140JKSKeystoreBootstrapCheckTests.java @@ -14,7 +14,7 @@ public class FIPS140JKSKeystoreBootstrapCheckTests extends ESTestCase { public void testNoKeystoreIsAllowed() { final Settings.Builder settings = Settings.builder() .put("xpack.security.fips_mode.enabled", "true"); - assertFalse(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + assertFalse(new FIPS140JKSKeystoreBootstrapCheck().check(new BootstrapContext(settings.build(), null)).isFailure()); } public void testSSLKeystoreTypeIsNotAllowed() { @@ -22,7 +22,7 @@ public void testSSLKeystoreTypeIsNotAllowed() { .put("xpack.security.fips_mode.enabled", "true") .put("xpack.ssl.keystore.path", "/this/is/the/path") .put("xpack.ssl.keystore.type", "JKS"); - assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck().check(new BootstrapContext(settings.build(), null)).isFailure()); } public void testSSLImplicitKeystoreTypeIsNotAllowed() { @@ -30,7 +30,7 @@ public void testSSLImplicitKeystoreTypeIsNotAllowed() { .put("xpack.security.fips_mode.enabled", "true") .put("xpack.ssl.keystore.path", "/this/is/the/path") .put("xpack.ssl.keystore.type", "JKS"); - assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck().check(new BootstrapContext(settings.build(), null)).isFailure()); } public void testTransportSSLKeystoreTypeIsNotAllowed() { @@ -38,7 +38,7 @@ public void testTransportSSLKeystoreTypeIsNotAllowed() { .put("xpack.security.fips_mode.enabled", "true") .put("xpack.security.transport.ssl.keystore.path", "/this/is/the/path") .put("xpack.security.transport.ssl.keystore.type", "JKS"); - assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck().check(new BootstrapContext(settings.build(), null)).isFailure()); } public void testHttpSSLKeystoreTypeIsNotAllowed() { @@ -46,7 +46,7 @@ public void testHttpSSLKeystoreTypeIsNotAllowed() { .put("xpack.security.fips_mode.enabled", "true") .put("xpack.security.http.ssl.keystore.path", "/this/is/the/path") .put("xpack.security.http.ssl.keystore.type", "JKS"); - assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck().check(new BootstrapContext(settings.build(), null)).isFailure()); } public void testRealmKeystoreTypeIsNotAllowed() { @@ -54,13 +54,13 @@ public void testRealmKeystoreTypeIsNotAllowed() { .put("xpack.security.fips_mode.enabled", "true") .put("xpack.security.authc.realms.ldap.ssl.keystore.path", "/this/is/the/path") .put("xpack.security.authc.realms.ldap.ssl.keystore.type", "JKS"); - assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck().check(new BootstrapContext(settings.build(), null)).isFailure()); } public void testImplicitRealmKeystoreTypeIsNotAllowed() { final Settings.Builder settings = Settings.builder() .put("xpack.security.fips_mode.enabled", "true") .put("xpack.security.authc.realms.ldap.ssl.keystore.path", "/this/is/the/path"); - assertTrue(new FIPS140JKSKeystoreBootstrapCheck(settings.build()).check(new BootstrapContext(settings.build(), null)).isFailure()); + assertTrue(new FIPS140JKSKeystoreBootstrapCheck().check(new BootstrapContext(settings.build(), null)).isFailure()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheckTests.java index a2ec8f9fb205..fb4c9e21a258 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheckTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140LicenseBootstrapCheckTests.java @@ -17,27 +17,29 @@ public class FIPS140LicenseBootstrapCheckTests extends ESTestCase { public void testBootstrapCheck() throws Exception { - assertTrue(new FIPS140LicenseBootstrapCheck(false) - .check(new BootstrapContext(Settings.EMPTY, MetaData.EMPTY_META_DATA)).isSuccess()); - assertTrue(new FIPS140LicenseBootstrapCheck(randomBoolean()) + assertTrue(new FIPS140LicenseBootstrapCheck() .check(new BootstrapContext(Settings.EMPTY, MetaData.EMPTY_META_DATA)).isSuccess()); + assertTrue(new FIPS140LicenseBootstrapCheck() + .check(new BootstrapContext(Settings.builder().put("xpack.security.fips_mode.enabled", randomBoolean()).build(), MetaData + .EMPTY_META_DATA)).isSuccess()); - License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(24)); MetaData.Builder builder = MetaData.builder(); + License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(24)); TestUtils.putLicense(builder, license); MetaData metaData = builder.build(); + if (FIPS140LicenseBootstrapCheck.ALLOWED_LICENSE_OPERATION_MODES.contains(license.operationMode())) { - assertTrue(new FIPS140LicenseBootstrapCheck(true).check(new BootstrapContext( + assertTrue(new FIPS140LicenseBootstrapCheck().check(new BootstrapContext( Settings.builder().put("xpack.security.fips_mode.enabled", true).build(), metaData)).isSuccess()); - assertTrue(new FIPS140LicenseBootstrapCheck(false).check(new BootstrapContext( + assertTrue(new FIPS140LicenseBootstrapCheck().check(new BootstrapContext( Settings.builder().put("xpack.security.fips_mode.enabled", false).build(), metaData)).isSuccess()); } else { - assertTrue(new FIPS140LicenseBootstrapCheck(false).check(new BootstrapContext( + assertTrue(new FIPS140LicenseBootstrapCheck().check(new BootstrapContext( Settings.builder().put("xpack.security.fips_mode.enabled", false).build(), metaData)).isSuccess()); - assertTrue(new FIPS140LicenseBootstrapCheck(true).check(new BootstrapContext( + assertTrue(new FIPS140LicenseBootstrapCheck().check(new BootstrapContext( Settings.builder().put("xpack.security.fips_mode.enabled", true).build(), metaData)).isFailure()); assertEquals("FIPS mode is only allowed with a Platinum or Trial license", - new FIPS140LicenseBootstrapCheck(true).check(new BootstrapContext( + new FIPS140LicenseBootstrapCheck().check(new BootstrapContext( Settings.builder().put("xpack.security.fips_mode.enabled", true).build(), metaData)).getMessage()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java index 8632400866a0..6376ca211dca 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/FIPS140PasswordHashingAlgorithmBootstrapCheckTests.java @@ -25,7 +25,7 @@ public void testPBKDF2AlgorithmIsAllowed() { .put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "PBKDF2_10000") .build(); final BootstrapCheck.BootstrapCheckResult result = - new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)); + new FIPS140PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)); assertFalse(result.isFailure()); } @@ -35,7 +35,7 @@ public void testPBKDF2AlgorithmIsAllowed() { .put(XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), "PBKDF2") .build(); final BootstrapCheck.BootstrapCheckResult result = - new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)); + new FIPS140PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)); assertFalse(result.isFailure()); } } @@ -55,7 +55,7 @@ private void runBCRYPTTest(final boolean fipsModeEnabled, final String passwordH } final Settings settings = builder.build(); final BootstrapCheck.BootstrapCheckResult result = - new FIPS140PasswordHashingAlgorithmBootstrapCheck(settings).check(new BootstrapContext(settings, null)); + new FIPS140PasswordHashingAlgorithmBootstrapCheck().check(new BootstrapContext(settings, null)); assertThat(result.isFailure(), equalTo(fipsModeEnabled)); } From eef0e35913007d4068d06d5274b8e1f24251b7d9 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 20 Aug 2018 17:12:02 +0300 Subject: [PATCH 058/283] Add mzn and dz to unsupported locales (#32957) Add mzn and dz to the list of unsupported locales for Kerberos tests. --- .../xpack/security/authc/kerberos/KerberosTestCase.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java index 0f88148a9a97..2bd1bdf906ad 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java @@ -72,6 +72,8 @@ public abstract class KerberosTestCase extends ESTestCase { unsupportedLocaleLanguages.add("ks"); unsupportedLocaleLanguages.add("ckb"); unsupportedLocaleLanguages.add("ne"); + unsupportedLocaleLanguages.add("dz"); + unsupportedLocaleLanguages.add("mzn"); } @BeforeClass From faa42de66d67f6ddcdd0e89c8bfb6cde96c11471 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Mon, 20 Aug 2018 08:54:55 -0600 Subject: [PATCH 059/283] Pass DiscoveryNode to initiateChannel (#32958) This is related to #32517. This commit passes the DiscoveryNode to the initiateChannel method for different Transport implementation. This will allow additional attributes (besides just the socket address) to be used when opening channels. --- .../org/elasticsearch/transport/netty4/Netty4Transport.java | 4 +++- .../java/org/elasticsearch/transport/nio/NioTransport.java | 4 +++- .../main/java/org/elasticsearch/transport/TcpTransport.java | 6 +++--- .../java/org/elasticsearch/transport/TcpTransportTests.java | 2 +- .../java/org/elasticsearch/transport/MockTcpTransport.java | 4 +++- .../org/elasticsearch/transport/nio/MockNioTransport.java | 4 +++- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java index e310f3012a9f..7eb34bcdcd3a 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java @@ -39,6 +39,7 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -222,7 +223,8 @@ protected ChannelHandler getClientChannelInitializer() { static final AttributeKey SERVER_CHANNEL_KEY = AttributeKey.newInstance("es-server-channel"); @Override - protected Netty4TcpChannel initiateChannel(InetSocketAddress address, ActionListener listener) throws IOException { + protected Netty4TcpChannel initiateChannel(DiscoveryNode node, ActionListener listener) throws IOException { + InetSocketAddress address = node.getAddress().address(); ChannelFuture channelFuture = bootstrap.connect(address); Channel channel = channelFuture.channel(); if (channel == null) { diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransport.java b/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransport.java index 47229a0df2f6..129f0ada77d5 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransport.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/transport/nio/NioTransport.java @@ -21,6 +21,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.recycler.Recycler; @@ -82,7 +83,8 @@ protected NioTcpServerChannel bind(String name, InetSocketAddress address) throw } @Override - protected NioTcpChannel initiateChannel(InetSocketAddress address, ActionListener connectListener) throws IOException { + protected NioTcpChannel initiateChannel(DiscoveryNode node, ActionListener connectListener) throws IOException { + InetSocketAddress address = node.getAddress().address(); NioTcpChannel channel = nioGroup.openChannel(address, clientChannelFactory); channel.addConnectListener(ActionListener.toBiConsumer(connectListener)); return channel; diff --git a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java index 6d4ab80a8928..d71e459fccdf 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java @@ -441,7 +441,7 @@ public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connect try { PlainActionFuture connectFuture = PlainActionFuture.newFuture(); connectionFutures.add(connectFuture); - TcpChannel channel = initiateChannel(node.getAddress().address(), connectFuture); + TcpChannel channel = initiateChannel(node, connectFuture); logger.trace(() -> new ParameterizedMessage("Tcp transport client channel opened: {}", channel)); channels.add(channel); } catch (Exception e) { @@ -841,12 +841,12 @@ protected void serverAcceptedChannel(TcpChannel channel) { /** * Initiate a single tcp socket channel. * - * @param address address for the initiated connection + * @param node for the initiated connection * @param connectListener listener to be called when connection complete * @return the pending connection * @throws IOException if an I/O exception occurs while opening the channel */ - protected abstract TcpChannel initiateChannel(InetSocketAddress address, ActionListener connectListener) throws IOException; + protected abstract TcpChannel initiateChannel(DiscoveryNode node, ActionListener connectListener) throws IOException; /** * Called to tear down internal resources diff --git a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java index a3d2e1bbc574..0b6112eb51c9 100644 --- a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java +++ b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java @@ -188,7 +188,7 @@ protected FakeChannel bind(String name, InetSocketAddress address) throws IOExce } @Override - protected FakeChannel initiateChannel(InetSocketAddress address, ActionListener connectListener) throws IOException { + protected FakeChannel initiateChannel(DiscoveryNode node, ActionListener connectListener) throws IOException { return new FakeChannel(messageCaptor); } diff --git a/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java b/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java index e6d80ac24d88..996508bdb887 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java @@ -19,6 +19,7 @@ package org.elasticsearch.transport; import org.elasticsearch.cli.SuppressForbidden; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -162,7 +163,8 @@ private void readMessage(MockChannel mockChannel, StreamInput input) throws IOEx @Override @SuppressForbidden(reason = "real socket for mocking remote connections") - protected MockChannel initiateChannel(InetSocketAddress address, ActionListener connectListener) throws IOException { + protected MockChannel initiateChannel(DiscoveryNode node, ActionListener connectListener) throws IOException { + InetSocketAddress address = node.getAddress().address(); final MockSocket socket = new MockSocket(); final MockChannel channel = new MockChannel(socket, address, "none"); diff --git a/test/framework/src/main/java/org/elasticsearch/transport/nio/MockNioTransport.java b/test/framework/src/main/java/org/elasticsearch/transport/nio/MockNioTransport.java index fbe61db6ee72..19543cfdcbb1 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/nio/MockNioTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/nio/MockNioTransport.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.network.NetworkService; @@ -85,7 +86,8 @@ protected MockServerChannel bind(String name, InetSocketAddress address) throws } @Override - protected MockSocketChannel initiateChannel(InetSocketAddress address, ActionListener connectListener) throws IOException { + protected MockSocketChannel initiateChannel(DiscoveryNode node, ActionListener connectListener) throws IOException { + InetSocketAddress address = node.getAddress().address(); MockSocketChannel channel = nioGroup.openChannel(address, clientChannelFactory); channel.addConnectListener(ActionListener.toBiConsumer(connectListener)); return channel; From 34295fad8713c5fcf6393085bb3dd368146d3ca2 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 20 Aug 2018 13:05:55 -0400 Subject: [PATCH 060/283] HLREST: AwaitsFix ML Test It leaks state into other tests causing them to fail sometimes. Relates to #32993 --- .../test/java/org/elasticsearch/client/ClusterClientIT.java | 6 ++++++ .../java/org/elasticsearch/client/MachineLearningIT.java | 2 ++ 2 files changed, 8 insertions(+) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java index 58b4b268788b..a914008376a5 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.client; +import org.apache.http.util.EntityUtils; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; @@ -34,6 +35,7 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.client.Request; import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.rest.RestStatus; @@ -174,6 +176,8 @@ public void testClusterHealthYellowClusterLevel() throws IOException { request.timeout("5s"); ClusterHealthResponse response = execute(request, highLevelClient().cluster()::health, highLevelClient().cluster()::healthAsync); + logger.info("Shard stats\n{}", EntityUtils.toString( + client().performRequest(new Request("GET", "/_cat/shards")).getEntity())); assertYellowShards(response); assertThat(response.getIndices().size(), equalTo(0)); } @@ -186,6 +190,8 @@ public void testClusterHealthYellowIndicesLevel() throws IOException { request.level(ClusterHealthRequest.Level.INDICES); ClusterHealthResponse response = execute(request, highLevelClient().cluster()::health, highLevelClient().cluster()::healthAsync); + logger.info("Shard stats\n{}", EntityUtils.toString( + client().performRequest(new Request("GET", "/_cat/shards")).getEntity())); assertYellowShards(response); assertThat(response.getIndices().size(), equalTo(2)); for (Map.Entry entry : response.getIndices().entrySet()) { 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 0037460150f1..95a29e99e526 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 @@ -19,6 +19,7 @@ package org.elasticsearch.client; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; @@ -36,6 +37,7 @@ import static org.hamcrest.Matchers.is; +@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32993") public class MachineLearningIT extends ESRestHighLevelClientTestCase { public void testPutJob() throws Exception { From 0749b18181c8dbfc3f85a4a6d64dd09beeb411ac Mon Sep 17 00:00:00 2001 From: Andrey Ershov Date: Mon, 20 Aug 2018 19:22:10 +0200 Subject: [PATCH 061/283] All Translog inner closes should happen after tragedy exception is set (#32674) All Translog inner closes should happen after tragedy exception is set (#32674) We faced with the nasty race condition. See #32526 InternalEngine.failOnTragic method has thrown AssertionError. If you carefully look at if branches in this method, you will spot that its only possible, if either Lucene IndexWriterhas closed from inside or Translog, has closed from inside, but tragedy exception is not set. For now, let us concentrate on the Translog class. We found out that there are two methods in Translog - namely rollGeneration and trimOperations that are closing Translog in case of Exception without tragedy exception being set. This commit fixes these 2 methods. To fix it, we pull tragedyException from TranslogWriter up-to Translog class, because in these 2 methods IndexWriter could be innocent, but still Translog needs to be closed. Also, tragedyException is wrapped with TragicExceptionHolder to reuse CAS/addSuppresed functionality in Translog and TranslogWriter. Also to protect us in the future and make sure close method is never called from inside Translog special assertion examining stack trace is added. Since we're still targeting Java 8 for runtime - no StackWalker API is used in the implementation. In the stack-trace checking method, we're considering inner caller not only Translog methods but Translog child classes methods as well. It does mean that Translog is meant for extending it, but it's needed to be able to test this method. Closes #32526 --- .../index/translog/TragicExceptionHolder.java | 43 ++++++++++++++++ .../index/translog/Translog.java | 42 ++++++++++++---- .../index/translog/TranslogWriter.java | 35 +++++-------- .../translog/TranslogDeletionPolicyTests.java | 2 +- .../index/translog/TranslogTests.java | 49 ++++++++++++++++++- 5 files changed, 136 insertions(+), 35 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/translog/TragicExceptionHolder.java diff --git a/server/src/main/java/org/elasticsearch/index/translog/TragicExceptionHolder.java b/server/src/main/java/org/elasticsearch/index/translog/TragicExceptionHolder.java new file mode 100644 index 000000000000..b823a920039b --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/translog/TragicExceptionHolder.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.index.translog; + +import java.util.concurrent.atomic.AtomicReference; + +public class TragicExceptionHolder { + private final AtomicReference tragedy = new AtomicReference<>(); + + /** + * Sets the tragic exception or if the tragic exception is already set adds passed exception as suppressed exception + * @param ex tragic exception to set + */ + public void setTragicException(Exception ex) { + assert ex != null; + if (tragedy.compareAndSet(null, ex) == false) { + if (tragedy.get() != ex) { // to ensure there is no self-suppression + tragedy.get().addSuppressed(ex); + } + } + } + + public Exception get() { + return tragedy.get(); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index e426b3a7253e..72c6210535f9 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -66,6 +66,7 @@ import java.util.function.LongSupplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -117,6 +118,7 @@ public class Translog extends AbstractIndexShardComponent implements IndexShardC private final Path location; private TranslogWriter current; + protected final TragicExceptionHolder tragedy = new TragicExceptionHolder(); private final AtomicBoolean closed = new AtomicBoolean(); private final TranslogConfig config; private final LongSupplier globalCheckpointSupplier; @@ -310,8 +312,28 @@ public boolean isOpen() { return closed.get() == false; } + private static boolean calledFromOutsideOrViaTragedyClose() { + List frames = Stream.of(Thread.currentThread().getStackTrace()). + skip(3). //skip getStackTrace, current method and close method frames + limit(10). //limit depth of analysis to 10 frames, it should be enough to catch closing with, e.g. IOUtils + filter(f -> + { + try { + return Translog.class.isAssignableFrom(Class.forName(f.getClassName())); + } catch (Exception ignored) { + return false; + } + } + ). //find all inner callers including Translog subclasses + collect(Collectors.toList()); + //the list of inner callers should be either empty or should contain closeOnTragicEvent method + return frames.isEmpty() || frames.stream().anyMatch(f -> f.getMethodName().equals("closeOnTragicEvent")); + } + @Override public void close() throws IOException { + assert calledFromOutsideOrViaTragedyClose() : + "Translog.close method is called from inside Translog, but not via closeOnTragicEvent method"; if (closed.compareAndSet(false, true)) { try (ReleasableLock lock = writeLock.acquire()) { try { @@ -462,7 +484,7 @@ TranslogWriter createWriter(long fileGeneration, long initialMinTranslogGen, lon getChannelFactory(), config.getBufferSize(), initialMinTranslogGen, initialGlobalCheckpoint, - globalCheckpointSupplier, this::getMinFileGeneration, primaryTermSupplier.getAsLong()); + globalCheckpointSupplier, this::getMinFileGeneration, primaryTermSupplier.getAsLong(), tragedy); } catch (final IOException e) { throw new TranslogException(shardId, "failed to create new translog file", e); } @@ -726,7 +748,8 @@ public void trimOperations(long belowTerm, long aboveSeqNo) throws IOException { } } catch (IOException e) { IOUtils.closeWhileHandlingException(newReaders); - close(); + tragedy.setTragicException(e); + closeOnTragicEvent(e); throw e; } @@ -779,10 +802,10 @@ public boolean ensureSynced(Stream locations) throws IOException { * * @param ex if an exception occurs closing the translog, it will be suppressed into the provided exception */ - private void closeOnTragicEvent(final Exception ex) { + protected void closeOnTragicEvent(final Exception ex) { // we can not hold a read lock here because closing will attempt to obtain a write lock and that would result in self-deadlock assert readLock.isHeldByCurrentThread() == false : Thread.currentThread().getName(); - if (current.getTragicException() != null) { + if (tragedy.get() != null) { try { close(); } catch (final AlreadyClosedException inner) { @@ -1556,7 +1579,8 @@ public void rollGeneration() throws IOException { current = createWriter(current.getGeneration() + 1); logger.trace("current translog set to [{}]", current.getGeneration()); } catch (final Exception e) { - IOUtils.closeWhileHandlingException(this); // tragic event + tragedy.setTragicException(e); + closeOnTragicEvent(e); throw e; } } @@ -1669,7 +1693,7 @@ long getFirstOperationPosition() { // for testing private void ensureOpen() { if (closed.get()) { - throw new AlreadyClosedException("translog is already closed", current.getTragicException()); + throw new AlreadyClosedException("translog is already closed", tragedy.get()); } } @@ -1683,7 +1707,7 @@ ChannelFactory getChannelFactory() { * Otherwise (no tragic exception has occurred) it returns null. */ public Exception getTragicException() { - return current.getTragicException(); + return tragedy.get(); } /** Reads and returns the current checkpoint */ @@ -1766,8 +1790,8 @@ static String createEmptyTranslog(Path location, long initialGlobalCheckpoint, S final String translogUUID = UUIDs.randomBase64UUID(); TranslogWriter writer = TranslogWriter.create(shardId, translogUUID, 1, location.resolve(getFilename(1)), channelFactory, new ByteSizeValue(10), 1, initialGlobalCheckpoint, - () -> { throw new UnsupportedOperationException(); }, () -> { throw new UnsupportedOperationException(); }, primaryTerm - ); + () -> { throw new UnsupportedOperationException(); }, () -> { throw new UnsupportedOperationException(); }, primaryTerm, + new TragicExceptionHolder()); writer.close(); return translogUUID; } diff --git a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java index b779644cd5c5..f48f2ceb7927 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java @@ -51,7 +51,7 @@ public class TranslogWriter extends BaseTranslogReader implements Closeable { /* the number of translog operations written to this file */ private volatile int operationCounter; /* if we hit an exception that we can't recover from we assign it to this var and ship it with every AlreadyClosedException we throw */ - private volatile Exception tragedy; + private final TragicExceptionHolder tragedy; /* A buffered outputstream what writes to the writers channel */ private final OutputStream outputStream; /* the total offset of this file including the bytes written to the file as well as into the buffer */ @@ -76,7 +76,10 @@ private TranslogWriter( final FileChannel channel, final Path path, final ByteSizeValue bufferSize, - final LongSupplier globalCheckpointSupplier, LongSupplier minTranslogGenerationSupplier, TranslogHeader header) throws IOException { + final LongSupplier globalCheckpointSupplier, LongSupplier minTranslogGenerationSupplier, TranslogHeader header, + TragicExceptionHolder tragedy) + throws + IOException { super(initialCheckpoint.generation, channel, path, header); assert initialCheckpoint.offset == channel.position() : "initial checkpoint offset [" + initialCheckpoint.offset + "] is different than current channel position [" @@ -94,12 +97,13 @@ private TranslogWriter( assert initialCheckpoint.trimmedAboveSeqNo == SequenceNumbers.UNASSIGNED_SEQ_NO : initialCheckpoint.trimmedAboveSeqNo; this.globalCheckpointSupplier = globalCheckpointSupplier; this.seenSequenceNumbers = Assertions.ENABLED ? new HashMap<>() : null; + this.tragedy = tragedy; } public static TranslogWriter create(ShardId shardId, String translogUUID, long fileGeneration, Path file, ChannelFactory channelFactory, ByteSizeValue bufferSize, final long initialMinTranslogGen, long initialGlobalCheckpoint, final LongSupplier globalCheckpointSupplier, final LongSupplier minTranslogGenerationSupplier, - final long primaryTerm) + final long primaryTerm, TragicExceptionHolder tragedy) throws IOException { final FileChannel channel = channelFactory.open(file); try { @@ -120,7 +124,7 @@ public static TranslogWriter create(ShardId shardId, String translogUUID, long f writerGlobalCheckpointSupplier = globalCheckpointSupplier; } return new TranslogWriter(channelFactory, shardId, checkpoint, channel, file, bufferSize, - writerGlobalCheckpointSupplier, minTranslogGenerationSupplier, header); + writerGlobalCheckpointSupplier, minTranslogGenerationSupplier, header, tragedy); } catch (Exception exception) { // if we fail to bake the file-generation into the checkpoint we stick with the file and once we recover and that // file exists we remove it. We only apply this logic to the checkpoint.generation+1 any other file with a higher generation is an error condition @@ -129,24 +133,8 @@ public static TranslogWriter create(ShardId shardId, String translogUUID, long f } } - /** - * If this {@code TranslogWriter} was closed as a side-effect of a tragic exception, - * e.g. disk full while flushing a new segment, this returns the root cause exception. - * Otherwise (no tragic exception has occurred) it returns null. - */ - public Exception getTragicException() { - return tragedy; - } - private synchronized void closeWithTragicEvent(final Exception ex) { - assert ex != null; - if (tragedy == null) { - tragedy = ex; - } else if (tragedy != ex) { - // it should be safe to call closeWithTragicEvents on multiple layers without - // worrying about self suppression. - tragedy.addSuppressed(ex); - } + tragedy.setTragicException(ex); try { close(); } catch (final IOException | RuntimeException e) { @@ -296,7 +284,8 @@ public TranslogReader closeIntoReader() throws IOException { if (closed.compareAndSet(false, true)) { return new TranslogReader(getLastSyncedCheckpoint(), channel, path, header); } else { - throw new AlreadyClosedException("translog [" + getGeneration() + "] is already closed (path [" + path + "]", tragedy); + throw new AlreadyClosedException("translog [" + getGeneration() + "] is already closed (path [" + path + "]", + tragedy.get()); } } } @@ -406,7 +395,7 @@ Checkpoint getLastSyncedCheckpoint() { protected final void ensureOpen() { if (isClosed()) { - throw new AlreadyClosedException("translog [" + getGeneration() + "] is already closed", tragedy); + throw new AlreadyClosedException("translog [" + getGeneration() + "] is already closed", tragedy.get()); } } diff --git a/server/src/test/java/org/elasticsearch/index/translog/TranslogDeletionPolicyTests.java b/server/src/test/java/org/elasticsearch/index/translog/TranslogDeletionPolicyTests.java index 9ae502fecb58..c8d4dbd43df2 100644 --- a/server/src/test/java/org/elasticsearch/index/translog/TranslogDeletionPolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/translog/TranslogDeletionPolicyTests.java @@ -171,7 +171,7 @@ private Tuple, TranslogWriter> createReadersAndWriter(final } writer = TranslogWriter.create(new ShardId("index", "uuid", 0), translogUUID, gen, tempDir.resolve(Translog.getFilename(gen)), FileChannel::open, TranslogConfig.DEFAULT_BUFFER_SIZE, 1L, 1L, () -> 1L, - () -> 1L, randomNonNegativeLong()); + () -> 1L, randomNonNegativeLong(), new TragicExceptionHolder()); writer = Mockito.spy(writer); Mockito.doReturn(now - (numberOfReaders - gen + 1) * 1000).when(writer).getLastModifiedTime(); diff --git a/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java b/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java index 1c27a59e0ecb..4ec479334ba6 100644 --- a/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java +++ b/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java @@ -33,6 +33,7 @@ import org.apache.lucene.store.MockDirectoryWrapper; import org.apache.lucene.util.LineFileDocs; import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.Assertions; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Randomness; import org.elasticsearch.common.Strings; @@ -108,6 +109,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongSupplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.LongStream; @@ -1655,7 +1657,7 @@ public void testRandomExceptionsOnTrimOperations( ) throws Exception { } assertThat(expectedException, is(not(nullValue()))); - + assertThat(failableTLog.getTragicException(), equalTo(expectedException)); assertThat(fileChannels, is(not(empty()))); assertThat("all file channels have to be closed", fileChannels.stream().filter(f -> f.isOpen()).findFirst().isPresent(), is(false)); @@ -2505,11 +2507,13 @@ public void testWithRandomException() throws IOException { syncedDocs.addAll(unsynced); unsynced.clear(); } catch (TranslogException | MockDirectoryWrapper.FakeIOException ex) { - // fair enough + assertEquals(failableTLog.getTragicException(), ex); } catch (IOException ex) { assertEquals(ex.getMessage(), "__FAKE__ no space left on device"); + assertEquals(failableTLog.getTragicException(), ex); } catch (RuntimeException ex) { assertEquals(ex.getMessage(), "simulated"); + assertEquals(failableTLog.getTragicException(), ex); } finally { Checkpoint checkpoint = Translog.readCheckpoint(config.getTranslogPath()); if (checkpoint.numOps == unsynced.size() + syncedDocs.size()) { @@ -2931,6 +2935,47 @@ public void testCloseSnapshotTwice() throws Exception { } } + // close method should never be called directly from Translog (the only exception is closeOnTragicEvent) + public void testTranslogCloseInvariant() throws IOException { + assumeTrue("test only works with assertions enabled", Assertions.ENABLED); + class MisbehavingTranslog extends Translog { + MisbehavingTranslog(TranslogConfig config, String translogUUID, TranslogDeletionPolicy deletionPolicy, LongSupplier globalCheckpointSupplier, LongSupplier primaryTermSupplier) throws IOException { + super(config, translogUUID, deletionPolicy, globalCheckpointSupplier, primaryTermSupplier); + } + + void callCloseDirectly() throws IOException { + close(); + } + + void callCloseUsingIOUtilsWithExceptionHandling() { + IOUtils.closeWhileHandlingException(this); + } + + void callCloseUsingIOUtils() throws IOException { + IOUtils.close(this); + } + + void callCloseOnTragicEvent() { + Exception e = new Exception("test tragic exception"); + tragedy.setTragicException(e); + closeOnTragicEvent(e); + } + } + + + globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + Path path = createTempDir(); + final TranslogConfig translogConfig = getTranslogConfig(path); + final TranslogDeletionPolicy deletionPolicy = createTranslogDeletionPolicy(translogConfig.getIndexSettings()); + final String translogUUID = Translog.createEmptyTranslog(path, SequenceNumbers.NO_OPS_PERFORMED, shardId, primaryTerm.get()); + MisbehavingTranslog misbehavingTranslog = new MisbehavingTranslog(translogConfig, translogUUID, deletionPolicy, () -> globalCheckpoint.get(), primaryTerm::get); + + expectThrows(AssertionError.class, () -> misbehavingTranslog.callCloseDirectly()); + expectThrows(AssertionError.class, () -> misbehavingTranslog.callCloseUsingIOUtils()); + expectThrows(AssertionError.class, () -> misbehavingTranslog.callCloseUsingIOUtilsWithExceptionHandling()); + misbehavingTranslog.callCloseOnTragicEvent(); + } + static class SortedSnapshot implements Translog.Snapshot { private final Translog.Snapshot snapshot; private List operations = null; From 462e91d362c9ce8aa807a702081f9bbd76f8d019 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 20 Aug 2018 13:53:15 -0400 Subject: [PATCH 062/283] Logging: Use settings when building daemon threads (#32751) Subclasses of `EsIntegTestCase` run multiple Elasticsearch nodes in the same JVM and when we log we look at the name of the thread to figure out the node name. This makes sure that all calls to `daemonThreadFactory` include the node name. Closes #32574 I'd like to follow this up with more drastic changes that make it impossible to do this incorrectly but that change is much larger than this and I'd like to get these log lines fixed up sooner rather than later. --- .../common/util/concurrent/EsExecutors.java | 2 ++ .../elasticsearch/indices/IndicesService.java | 2 +- .../elasticsearch/license/LicenseService.java | 2 +- .../xpack/core/scheduler/SchedulerEngine.java | 5 ++-- .../elasticsearch/xpack/rollup/Rollup.java | 2 +- .../xpack/rollup/job/RollupJobTaskTests.java | 24 +++++++++++-------- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/EsExecutors.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/EsExecutors.java index 057a970470b1..d38eb03fae3d 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/EsExecutors.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/EsExecutors.java @@ -160,11 +160,13 @@ public static String threadName(Settings settings, String namePrefix) { if (Node.NODE_NAME_SETTING.exists(settings)) { return threadName(Node.NODE_NAME_SETTING.get(settings), namePrefix); } else { + // TODO this should only be allowed in tests return threadName("", namePrefix); } } public static String threadName(final String nodeName, final String namePrefix) { + // TODO missing node names should only be allowed in tests return "elasticsearch" + (nodeName.isEmpty() ? "" : "[") + nodeName + (nodeName.isEmpty() ? "" : "]") + "[" + namePrefix + "]"; } diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index 39346fecbef2..5c097ba774f4 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -233,7 +233,7 @@ public void onRemoval(ShardId shardId, String fieldName, boolean wasEvicted, lon @Override protected void doStop() { - ExecutorService indicesStopExecutor = Executors.newFixedThreadPool(5, EsExecutors.daemonThreadFactory("indices_shutdown")); + ExecutorService indicesStopExecutor = Executors.newFixedThreadPool(5, EsExecutors.daemonThreadFactory(settings, "indices_shutdown")); // Copy indices because we modify it asynchronously in the body of the loop final Set indices = this.indices.values().stream().map(s -> s.index()).collect(Collectors.toSet()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java index e08743892439..0619aef6961c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java @@ -120,7 +120,7 @@ public LicenseService(Settings settings, ClusterService clusterService, Clock cl super(settings); this.clusterService = clusterService; this.clock = clock; - this.scheduler = new SchedulerEngine(clock); + this.scheduler = new SchedulerEngine(settings, clock); this.licenseState = licenseState; this.operationModeFileWatcher = new OperationModeFileWatcher(resourceWatcherService, XPackPlugin.resolveConfigFile(env, "license_mode"), logger, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java index e405c5e4e007..ffc0257313b3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.scheduler; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.FutureUtils; @@ -92,9 +93,9 @@ public interface Schedule { private final Clock clock; private final List listeners = new CopyOnWriteArrayList<>(); - public SchedulerEngine(Clock clock) { + public SchedulerEngine(Settings settings, Clock clock) { this.clock = clock; - this.scheduler = Executors.newScheduledThreadPool(1, EsExecutors.daemonThreadFactory("trigger_engine_scheduler")); + this.scheduler = Executors.newScheduledThreadPool(1, EsExecutors.daemonThreadFactory(settings, "trigger_engine_scheduler")); } public void register(Listener listener) { diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/Rollup.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/Rollup.java index 0fc4d838f7ce..09b2ccd079a7 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/Rollup.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/Rollup.java @@ -194,7 +194,7 @@ public List> getPersistentTasksExecutor(ClusterServic return emptyList(); } - SchedulerEngine schedulerEngine = new SchedulerEngine(getClock()); + SchedulerEngine schedulerEngine = new SchedulerEngine(settings, getClock()); return Collections.singletonList(new RollupJobTask.RollupJobPersistentTasksExecutor(settings, client, schedulerEngine, threadPool)); } diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java index 13290f09e8eb..9a75d6fc6759 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.node.Node; import org.elasticsearch.persistent.PersistentTaskState; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.search.aggregations.Aggregations; @@ -47,6 +48,9 @@ public class RollupJobTaskTests extends ESTestCase { + private static final Settings SETTINGS = Settings.builder() + .put(Node.NODE_NAME_SETTING.getKey(), "test") + .build(); private static ThreadPool pool = new TestThreadPool("test"); @AfterClass @@ -62,7 +66,7 @@ public void testInitialStatusStopped() { RollupJobStatus status = new RollupJobStatus(IndexerState.STOPPED, Collections.singletonMap("foo", "bar"), randomBoolean()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STOPPED)); @@ -75,7 +79,7 @@ public void testInitialStatusAborting() { RollupJobStatus status = new RollupJobStatus(IndexerState.ABORTING, Collections.singletonMap("foo", "bar"), randomBoolean()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STOPPED)); @@ -88,7 +92,7 @@ public void testInitialStatusStopping() { RollupJobStatus status = new RollupJobStatus(IndexerState.STOPPING, Collections.singletonMap("foo", "bar"), randomBoolean()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STOPPED)); @@ -101,7 +105,7 @@ public void testInitialStatusStarted() { RollupJobStatus status = new RollupJobStatus(IndexerState.STARTED, Collections.singletonMap("foo", "bar"), randomBoolean()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STARTED)); @@ -114,7 +118,7 @@ public void testInitialStatusIndexingOldID() { RollupJobStatus status = new RollupJobStatus(IndexerState.INDEXING, Collections.singletonMap("foo", "bar"), false); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STARTED)); @@ -128,7 +132,7 @@ public void testInitialStatusIndexingNewID() { RollupJobStatus status = new RollupJobStatus(IndexerState.INDEXING, Collections.singletonMap("foo", "bar"), true); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STARTED)); @@ -141,7 +145,7 @@ public void testNoInitialStatus() { RollupJob job = new RollupJob(ConfigTestHelpers.randomRollupJobConfig(random()), Collections.emptyMap()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, null, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STOPPED)); @@ -154,7 +158,7 @@ public void testStartWhenStarted() throws InterruptedException { RollupJobStatus status = new RollupJobStatus(IndexerState.STARTED, Collections.singletonMap("foo", "bar"), randomBoolean()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STARTED)); @@ -641,7 +645,7 @@ public void testStopWhenStopped() throws InterruptedException { RollupJobStatus status = new RollupJobStatus(IndexerState.STOPPED, null, randomBoolean()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); RollupJobTask task = new RollupJobTask(1, "type", "action", new TaskId("node", 123), job, status, client, schedulerEngine, pool, Collections.emptyMap()); assertThat(((RollupJobStatus)task.getStatus()).getIndexerState(), equalTo(IndexerState.STOPPED)); @@ -748,7 +752,7 @@ public void testStopWhenAborting() throws InterruptedException { RollupJobStatus status = new RollupJobStatus(IndexerState.STOPPED, null, randomBoolean()); Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); - SchedulerEngine schedulerEngine = new SchedulerEngine(Clock.systemUTC()); + SchedulerEngine schedulerEngine = new SchedulerEngine(SETTINGS, Clock.systemUTC()); CountDownLatch latch = new CountDownLatch(2); From 815c56b6770cb9a4b1a58dd831655cd312df09b6 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Mon, 20 Aug 2018 11:00:11 -0700 Subject: [PATCH 063/283] Fix an inaccuracy in the dynamic templates documentation. (#32890) --- docs/reference/mapping/dynamic/templates.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/mapping/dynamic/templates.asciidoc b/docs/reference/mapping/dynamic/templates.asciidoc index bdb009167552..4fbed6644980 100644 --- a/docs/reference/mapping/dynamic/templates.asciidoc +++ b/docs/reference/mapping/dynamic/templates.asciidoc @@ -38,10 +38,10 @@ Dynamic templates are specified as an array of named objects: <3> The mapping that the matched field should use. -Templates are processed in order -- the first matching template wins. New -templates can be appended to the end of the list with the -<> API. If a new template has the same -name as an existing template, it will replace the old version. +Templates are processed in order -- the first matching template wins. When +putting new dynamic templates through the <> API, +all existing templates are overwritten. This allows for dynamic templates to be +reordered or deleted after they were initially added. [[match-mapping-type]] ==== `match_mapping_type` From 40f1bb5e5eab4445f0113131f415963664a1c530 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 20 Aug 2018 15:13:19 -0400 Subject: [PATCH 064/283] Trim translog when safe commit advanced (#32967) Since #28140 when the global checkpoint is advanced, we try to move the safe commit forward, and clean up old index commits if possible. However, we forget to trim unreferenced translog. This change makes sure that we prune both old translog and index commits when the safe commit advanced. Relates #28140 Closes #32089 --- .../elasticsearch/index/engine/InternalEngine.java | 3 +++ .../index/engine/InternalEngineTests.java | 13 ++++++++++--- .../indices/recovery/RecoveryTests.java | 1 - 3 files changed, 13 insertions(+), 4 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 a30127a24ae2..982b220f25b2 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -478,6 +478,7 @@ public Translog.Location getTranslogLastWriteLocation() { private void revisitIndexDeletionPolicyOnTranslogSynced() throws IOException { if (combinedDeletionPolicy.hasUnreferencedCommits()) { indexWriter.deleteUnusedFiles(); + translog.trimUnreferencedReaders(); } } @@ -1736,6 +1737,8 @@ private void releaseIndexCommit(IndexCommit snapshot) throws IOException { // Revisit the deletion policy if we can clean up the snapshotting commit. if (combinedDeletionPolicy.releaseCommit(snapshot)) { ensureOpen(); + // Here we don't have to trim translog because snapshotting an index commit + // does not lock translog or prevents unreferenced files from trimming. indexWriter.deleteUnusedFiles(); } } 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 9151fa24fc9a..f6df22242883 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -152,6 +152,7 @@ import org.elasticsearch.index.translog.Translog; import org.elasticsearch.index.translog.TranslogConfig; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; +import org.elasticsearch.test.IndexSettingsModule; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; @@ -4342,13 +4343,18 @@ public void testAcquireIndexCommit() throws Exception { public void testCleanUpCommitsWhenGlobalCheckpointAdvanced() throws Exception { IOUtils.close(engine, store); - store = createStore(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings("test", + Settings.builder().put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), -1) + .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), -1).build()); final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); - try (InternalEngine engine = createEngine(store, createTempDir(), globalCheckpoint::get)) { + try (Store store = createStore(); + InternalEngine engine = + createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get))) { final int numDocs = scaledRandomIntBetween(10, 100); for (int docId = 0; docId < numDocs; docId++) { index(engine, docId); - if (frequently()) { + if (rarely()) { engine.flush(randomBoolean(), randomBoolean()); } } @@ -4362,6 +4368,7 @@ public void testCleanUpCommitsWhenGlobalCheckpointAdvanced() throws Exception { globalCheckpoint.set(randomLongBetween(engine.getLocalCheckpoint(), Long.MAX_VALUE)); engine.syncTranslog(); assertThat(DirectoryReader.listCommits(store.directory()), contains(commits.get(commits.size() - 1))); + assertThat(engine.estimateTranslogOperationsFromMinSeq(0L), equalTo(0)); } } diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java index 0f663eca75d7..5547a629ab2a 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java @@ -73,7 +73,6 @@ public void testTranslogHistoryTransferred() throws Exception { } } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32089") public void testRetentionPolicyChangeDuringRecovery() throws Exception { try (ReplicationGroup shards = createGroup(0)) { shards.startPrimary(); From 9050c7e846b6a34e7a33e05418b29f5789154943 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 20 Aug 2018 15:33:29 -0400 Subject: [PATCH 065/283] Generalize remote license checker (#32971) Machine learning has baked a remote license checker for use in checking license compatibility of a remote license. This remote license checker has general usage for any feature that relies on a remote cluster. For example, cross-cluster replication will pull changes from a remote cluster and require that the local and remote clusters have platinum licenses. This commit generalizes the remote cluster license check for use in cross-cluster replication. --- .../license/RemoteClusterLicenseChecker.java | 281 ++++++++++++ .../RemoteClusterLicenseCheckerTests.java | 414 ++++++++++++++++++ .../action/TransportStartDatafeedAction.java | 58 ++- .../ml/datafeed/DatafeedNodeSelector.java | 3 +- .../ml/datafeed/MlRemoteLicenseChecker.java | 193 -------- .../TransportStartDatafeedActionTests.java | 3 +- .../datafeed/MlRemoteLicenseCheckerTests.java | 199 --------- 7 files changed, 736 insertions(+), 415 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java delete mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseChecker.java delete mode 100644 x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseCheckerTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java new file mode 100644 index 000000000000..043224e357b9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java @@ -0,0 +1,281 @@ +/* + * 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.license; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.protocol.xpack.XPackInfoRequest; +import org.elasticsearch.protocol.xpack.XPackInfoResponse; +import org.elasticsearch.protocol.xpack.license.LicenseStatus; +import org.elasticsearch.transport.RemoteClusterAware; +import org.elasticsearch.xpack.core.action.XPackInfoAction; + +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Checks remote clusters for license compatibility with a specified license predicate. + */ +public final class RemoteClusterLicenseChecker { + + /** + * Encapsulates the license info of a remote cluster. + */ + public static final class RemoteClusterLicenseInfo { + + private final String clusterAlias; + + /** + * The alias of the remote cluster. + * + * @return the cluster alias + */ + public String clusterAlias() { + return clusterAlias; + } + + private final XPackInfoResponse.LicenseInfo licenseInfo; + + /** + * The license info of the remote cluster. + * + * @return the license info + */ + public XPackInfoResponse.LicenseInfo licenseInfo() { + return licenseInfo; + } + + RemoteClusterLicenseInfo(final String clusterAlias, final XPackInfoResponse.LicenseInfo licenseInfo) { + this.clusterAlias = clusterAlias; + this.licenseInfo = licenseInfo; + } + + } + + /** + * Encapsulates a remote cluster license check. The check is either successful if the license of the remote cluster is compatible with + * the predicate used to check license compatibility, or the check is a failure. + */ + public static final class LicenseCheck { + + private final RemoteClusterLicenseInfo remoteClusterLicenseInfo; + + /** + * The remote cluster license info. This method should only be invoked if this instance represents a failing license check. + * + * @return the remote cluster license info + */ + public RemoteClusterLicenseInfo remoteClusterLicenseInfo() { + assert isSuccess() == false; + return remoteClusterLicenseInfo; + } + + private static final LicenseCheck SUCCESS = new LicenseCheck(null); + + /** + * A successful license check. + * + * @return a successful license check instance + */ + public static LicenseCheck success() { + return SUCCESS; + } + + /** + * Test if this instance represents a successful license check. + * + * @return true if this instance represents a successful license check, otherwise false + */ + public boolean isSuccess() { + return this == SUCCESS; + } + + /** + * Creates a failing license check encapsulating the specified remote cluster license info. + * + * @param remoteClusterLicenseInfo the remote cluster license info + * @return a failing license check + */ + public static LicenseCheck failure(final RemoteClusterLicenseInfo remoteClusterLicenseInfo) { + return new LicenseCheck(remoteClusterLicenseInfo); + } + + private LicenseCheck(final RemoteClusterLicenseInfo remoteClusterLicenseInfo) { + this.remoteClusterLicenseInfo = remoteClusterLicenseInfo; + } + + } + + private final Client client; + private final Predicate predicate; + + /** + * Constructs a remote cluster license checker with the specified license predicate for checking license compatibility. The predicate + * does not need to check for the active license state as this is handled by the remote cluster license checker. + * + * @param client the client + * @param predicate the license predicate + */ + public RemoteClusterLicenseChecker(final Client client, final Predicate predicate) { + this.client = client; + this.predicate = predicate; + } + + public static boolean isLicensePlatinumOrTrial(final XPackInfoResponse.LicenseInfo licenseInfo) { + final License.OperationMode mode = License.OperationMode.resolve(licenseInfo.getMode()); + return mode == License.OperationMode.PLATINUM || mode == License.OperationMode.TRIAL; + } + + /** + * Checks the specified clusters for license compatibility. The specified callback will be invoked once if all clusters are + * license-compatible, otherwise the specified callback will be invoked once on the first cluster that is not license-compatible. + * + * @param clusterAliases the cluster aliases to check + * @param listener a callback + */ + public void checkRemoteClusterLicenses(final List clusterAliases, final ActionListener listener) { + final Iterator clusterAliasesIterator = clusterAliases.iterator(); + if (clusterAliasesIterator.hasNext() == false) { + listener.onResponse(LicenseCheck.success()); + return; + } + + final AtomicReference clusterAlias = new AtomicReference<>(); + + final ActionListener infoListener = new ActionListener() { + + @Override + public void onResponse(final XPackInfoResponse xPackInfoResponse) { + final XPackInfoResponse.LicenseInfo licenseInfo = xPackInfoResponse.getLicenseInfo(); + if ((licenseInfo.getStatus() == LicenseStatus.ACTIVE) == false || predicate.test(licenseInfo) == false) { + listener.onResponse(LicenseCheck.failure(new RemoteClusterLicenseInfo(clusterAlias.get(), licenseInfo))); + return; + } + + if (clusterAliasesIterator.hasNext()) { + clusterAlias.set(clusterAliasesIterator.next()); + // recurse to the next cluster + remoteClusterLicense(clusterAlias.get(), this); + } else { + listener.onResponse(LicenseCheck.success()); + } + } + + @Override + public void onFailure(final Exception e) { + final String message = "could not determine the license type for cluster [" + clusterAlias.get() + "]"; + listener.onFailure(new ElasticsearchException(message, e)); + } + + }; + + // check the license on the first cluster, and then we recursively check licenses on the remaining clusters + clusterAlias.set(clusterAliasesIterator.next()); + remoteClusterLicense(clusterAlias.get(), infoListener); + } + + private void remoteClusterLicense(final String clusterAlias, final ActionListener listener) { + final ThreadContext threadContext = client.threadPool().getThreadContext(); + final ContextPreservingActionListener contextPreservingActionListener = + new ContextPreservingActionListener<>(threadContext.newRestorableContext(false), listener); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + // we stash any context here since this is an internal execution and should not leak any existing context information + threadContext.markAsSystemContext(); + + final XPackInfoRequest request = new XPackInfoRequest(); + request.setCategories(EnumSet.of(XPackInfoRequest.Category.LICENSE)); + try { + client.getRemoteClusterClient(clusterAlias).execute(XPackInfoAction.INSTANCE, request, contextPreservingActionListener); + } catch (final Exception e) { + contextPreservingActionListener.onFailure(e); + } + } + } + + /** + * Predicate to test if the index name represents the name of a remote index. + * + * @param index the index name + * @return true if the collection of indices contains a remote index, otherwise false + */ + public static boolean isRemoteIndex(final String index) { + return index.indexOf(RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR) != -1; + } + + /** + * Predicate to test if the collection of index names contains any that represent the name of a remote index. + * + * @param indices the collection of index names + * @return true if the collection of index names contains a name that represents a remote index, otherwise false + */ + public static boolean containsRemoteIndex(final List indices) { + return indices.stream().anyMatch(RemoteClusterLicenseChecker::isRemoteIndex); + } + + /** + * Filters the collection of index names for names that represent a remote index. Remote index names are of the form + * {@code cluster_name:index_name}. + * + * @param indices the collection of index names + * @return list of index names that represent remote index names + */ + public static List remoteIndices(final List indices) { + return indices.stream().filter(RemoteClusterLicenseChecker::isRemoteIndex).collect(Collectors.toList()); + } + + /** + * Extract the list of remote cluster aliases from the list of index names. Remote index names are of the form + * {@code cluster_alias:index_name} and the cluster_alias is extracted for each index name that represents a remote index. + * + * @param indices the collection of index names + * @return the remote cluster names + */ + public static List remoteClusterAliases(final List indices) { + return indices.stream() + .filter(RemoteClusterLicenseChecker::isRemoteIndex) + .map(index -> index.substring(0, index.indexOf(RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR))) + .distinct() + .collect(Collectors.toList()); + } + + /** + * Constructs an error message for license incompatibility. + * + * @param feature the name of the feature that initiated the remote cluster license check. + * @param remoteClusterLicenseInfo the remote cluster license info of the cluster that failed the license check + * @return an error message representing license incompatibility + */ + public static String buildErrorMessage( + final String feature, + final RemoteClusterLicenseInfo remoteClusterLicenseInfo, + final Predicate predicate) { + final StringBuilder error = new StringBuilder(); + if (remoteClusterLicenseInfo.licenseInfo().getStatus() != LicenseStatus.ACTIVE) { + error.append(String.format(Locale.ROOT, "the license on cluster [%s] is not active", remoteClusterLicenseInfo.clusterAlias())); + } else { + assert predicate.test(remoteClusterLicenseInfo.licenseInfo()) == false : "license must be incompatible to build error message"; + final String message = String.format( + Locale.ROOT, + "the license mode [%s] on cluster [%s] does not enable [%s]", + License.OperationMode.resolve(remoteClusterLicenseInfo.licenseInfo().getMode()), + remoteClusterLicenseInfo.clusterAlias(), + feature); + error.append(message); + } + + return error.toString(); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java new file mode 100644 index 000000000000..a8627d215420 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java @@ -0,0 +1,414 @@ +/* + * 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.license; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.protocol.xpack.XPackInfoResponse; +import org.elasticsearch.protocol.xpack.license.LicenseStatus; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.action.XPackInfoAction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.same; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public final class RemoteClusterLicenseCheckerTests extends ESTestCase { + + public void testIsNotRemoteIndex() { + assertFalse(RemoteClusterLicenseChecker.isRemoteIndex("local-index")); + } + + public void testIsRemoteIndex() { + assertTrue(RemoteClusterLicenseChecker.isRemoteIndex("remote-cluster:remote-index")); + } + + public void testNoRemoteIndex() { + final List indices = Arrays.asList("local-index1", "local-index2"); + assertFalse(RemoteClusterLicenseChecker.containsRemoteIndex(indices)); + } + + public void testRemoteIndex() { + final List indices = Arrays.asList("local-index", "remote-cluster:remote-index"); + assertTrue(RemoteClusterLicenseChecker.containsRemoteIndex(indices)); + } + + public void testNoRemoteIndices() { + final List indices = Collections.singletonList("local-index"); + assertThat(RemoteClusterLicenseChecker.remoteIndices(indices), is(empty())); + } + + public void testRemoteIndices() { + final List indices = Arrays.asList("local-index1", "remote-cluster1:index1", "local-index2", "remote-cluster2:index1"); + assertThat( + RemoteClusterLicenseChecker.remoteIndices(indices), + containsInAnyOrder("remote-cluster1:index1", "remote-cluster2:index1")); + } + + public void testNoRemoteClusterAliases() { + final List indices = Arrays.asList("local-index1", "local-index2"); + assertThat(RemoteClusterLicenseChecker.remoteClusterAliases(indices), empty()); + } + + public void testOneRemoteClusterAlias() { + final List indices = Arrays.asList("local-index1", "remote-cluster1:remote-index1"); + assertThat(RemoteClusterLicenseChecker.remoteClusterAliases(indices), contains("remote-cluster1")); + } + + public void testMoreThanOneRemoteClusterAlias() { + final List indices = Arrays.asList("remote-cluster1:remote-index1", "local-index1", "remote-cluster2:remote-index1"); + assertThat(RemoteClusterLicenseChecker.remoteClusterAliases(indices), contains("remote-cluster1", "remote-cluster2")); + } + + public void testDuplicateRemoteClusterAlias() { + final List indices = Arrays.asList( + "remote-cluster1:remote-index1", "local-index1", "remote-cluster2:index1", "remote-cluster2:remote-index2"); + assertThat(RemoteClusterLicenseChecker.remoteClusterAliases(indices), contains("remote-cluster1", "remote-cluster2")); + } + + public void testCheckRemoteClusterLicensesGivenCompatibleLicenses() { + final AtomicInteger index = new AtomicInteger(); + final List responses = new ArrayList<>(); + + final ThreadPool threadPool = createMockThreadPool(); + final Client client = createMockClient(threadPool); + doAnswer(invocationMock -> { + @SuppressWarnings("unchecked") ActionListener listener = + (ActionListener) invocationMock.getArguments()[2]; + listener.onResponse(responses.get(index.getAndIncrement())); + return null; + }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); + + final List remoteClusterAliases = Arrays.asList("valid1", "valid2", "valid3"); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + + final RemoteClusterLicenseChecker licenseChecker = + new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + final AtomicReference licenseCheck = new AtomicReference<>(); + + licenseChecker.checkRemoteClusterLicenses( + remoteClusterAliases, + doubleInvocationProtectingListener(new ActionListener() { + + @Override + public void onResponse(final RemoteClusterLicenseChecker.LicenseCheck response) { + licenseCheck.set(response); + } + + @Override + public void onFailure(final Exception e) { + fail(e.getMessage()); + } + + })); + + verify(client, times(3)).execute(same(XPackInfoAction.INSTANCE), any(), any()); + assertNotNull(licenseCheck.get()); + assertTrue(licenseCheck.get().isSuccess()); + } + + public void testCheckRemoteClusterLicensesGivenIncompatibleLicense() { + final AtomicInteger index = new AtomicInteger(); + final List remoteClusterAliases = Arrays.asList("good", "cluster-with-basic-license", "good2"); + final List responses = new ArrayList<>(); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createBasicLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + + final ThreadPool threadPool = createMockThreadPool(); + final Client client = createMockClient(threadPool); + doAnswer(invocationMock -> { + @SuppressWarnings("unchecked") ActionListener listener = + (ActionListener) invocationMock.getArguments()[2]; + listener.onResponse(responses.get(index.getAndIncrement())); + return null; + }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); + + final RemoteClusterLicenseChecker licenseChecker = + new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + final AtomicReference licenseCheck = new AtomicReference<>(); + + licenseChecker.checkRemoteClusterLicenses( + remoteClusterAliases, + doubleInvocationProtectingListener(new ActionListener() { + + @Override + public void onResponse(final RemoteClusterLicenseChecker.LicenseCheck response) { + licenseCheck.set(response); + } + + @Override + public void onFailure(final Exception e) { + fail(e.getMessage()); + } + + })); + + verify(client, times(2)).execute(same(XPackInfoAction.INSTANCE), any(), any()); + assertNotNull(licenseCheck.get()); + assertFalse(licenseCheck.get().isSuccess()); + assertThat(licenseCheck.get().remoteClusterLicenseInfo().clusterAlias(), equalTo("cluster-with-basic-license")); + assertThat(licenseCheck.get().remoteClusterLicenseInfo().licenseInfo().getType(), equalTo("BASIC")); + } + + public void testCheckRemoteClusterLicencesGivenNonExistentCluster() { + final AtomicInteger index = new AtomicInteger(); + final List responses = new ArrayList<>(); + + final List remoteClusterAliases = Arrays.asList("valid1", "valid2", "valid3"); + final String failingClusterAlias = randomFrom(remoteClusterAliases); + final ThreadPool threadPool = createMockThreadPool(); + final Client client = createMockClientThatThrowsOnGetRemoteClusterClient(threadPool, failingClusterAlias); + doAnswer(invocationMock -> { + @SuppressWarnings("unchecked") ActionListener listener = + (ActionListener) invocationMock.getArguments()[2]; + listener.onResponse(responses.get(index.getAndIncrement())); + return null; + }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); + + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + + final RemoteClusterLicenseChecker licenseChecker = + new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + final AtomicReference exception = new AtomicReference<>(); + + licenseChecker.checkRemoteClusterLicenses( + remoteClusterAliases, + doubleInvocationProtectingListener(new ActionListener() { + + @Override + public void onResponse(final RemoteClusterLicenseChecker.LicenseCheck response) { + fail(); + } + + @Override + public void onFailure(final Exception e) { + exception.set(e); + } + + })); + + assertNotNull(exception.get()); + assertThat(exception.get(), instanceOf(ElasticsearchException.class)); + assertThat(exception.get().getMessage(), equalTo("could not determine the license type for cluster [" + failingClusterAlias + "]")); + assertNotNull(exception.get().getCause()); + assertThat(exception.get().getCause(), instanceOf(IllegalArgumentException.class)); + } + + public void testRemoteClusterLicenseCallUsesSystemContext() throws InterruptedException { + final ThreadPool threadPool = new TestThreadPool(getTestName()); + + try { + final Client client = createMockClient(threadPool); + doAnswer(invocationMock -> { + assertTrue(threadPool.getThreadContext().isSystemContext()); + @SuppressWarnings("unchecked") ActionListener listener = + (ActionListener) invocationMock.getArguments()[2]; + listener.onResponse(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + return null; + }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); + + final RemoteClusterLicenseChecker licenseChecker = + new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + + final List remoteClusterAliases = Collections.singletonList("valid"); + licenseChecker.checkRemoteClusterLicenses( + remoteClusterAliases, doubleInvocationProtectingListener(ActionListener.wrap(() -> {}))); + + verify(client, times(1)).execute(same(XPackInfoAction.INSTANCE), any(), any()); + } finally { + terminate(threadPool); + } + } + + public void testListenerIsExecutedWithCallingContext() throws InterruptedException { + final AtomicInteger index = new AtomicInteger(); + final List responses = new ArrayList<>(); + + final ThreadPool threadPool = new TestThreadPool(getTestName()); + + try { + final List remoteClusterAliases = Arrays.asList("valid1", "valid2", "valid3"); + final Client client; + final boolean failure = randomBoolean(); + if (failure) { + client = createMockClientThatThrowsOnGetRemoteClusterClient(threadPool, randomFrom(remoteClusterAliases)); + } else { + client = createMockClient(threadPool); + } + doAnswer(invocationMock -> { + @SuppressWarnings("unchecked") ActionListener listener = + (ActionListener) invocationMock.getArguments()[2]; + listener.onResponse(responses.get(index.getAndIncrement())); + return null; + }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); + + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); + + final RemoteClusterLicenseChecker licenseChecker = + new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + + final AtomicBoolean listenerInvoked = new AtomicBoolean(); + threadPool.getThreadContext().putHeader("key", "value"); + licenseChecker.checkRemoteClusterLicenses( + remoteClusterAliases, + doubleInvocationProtectingListener(new ActionListener() { + + @Override + public void onResponse(final RemoteClusterLicenseChecker.LicenseCheck response) { + if (failure) { + fail(); + } + assertThat(threadPool.getThreadContext().getHeader("key"), equalTo("value")); + assertFalse(threadPool.getThreadContext().isSystemContext()); + listenerInvoked.set(true); + } + + @Override + public void onFailure(final Exception e) { + if (failure == false) { + fail(); + } + assertThat(threadPool.getThreadContext().getHeader("key"), equalTo("value")); + assertFalse(threadPool.getThreadContext().isSystemContext()); + listenerInvoked.set(true); + } + + })); + + assertTrue(listenerInvoked.get()); + } finally { + terminate(threadPool); + } + } + + public void testBuildErrorMessageForActiveCompatibleLicense() { + final XPackInfoResponse.LicenseInfo platinumLicence = createPlatinumLicenseResponse(); + final RemoteClusterLicenseChecker.RemoteClusterLicenseInfo info = + new RemoteClusterLicenseChecker.RemoteClusterLicenseInfo("platinum-cluster", platinumLicence); + final AssertionError e = expectThrows( + AssertionError.class, + () -> RemoteClusterLicenseChecker.buildErrorMessage("", info, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial)); + assertThat(e, hasToString(containsString("license must be incompatible to build error message"))); + } + + public void testBuildErrorMessageForIncompatibleLicense() { + final XPackInfoResponse.LicenseInfo basicLicense = createBasicLicenseResponse(); + final RemoteClusterLicenseChecker.RemoteClusterLicenseInfo info = + new RemoteClusterLicenseChecker.RemoteClusterLicenseInfo("basic-cluster", basicLicense); + assertThat( + RemoteClusterLicenseChecker.buildErrorMessage("Feature", info, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial), + equalTo("the license mode [BASIC] on cluster [basic-cluster] does not enable [Feature]")); + } + + public void testBuildErrorMessageForInactiveLicense() { + final XPackInfoResponse.LicenseInfo expiredLicense = createExpiredLicenseResponse(); + final RemoteClusterLicenseChecker.RemoteClusterLicenseInfo info = + new RemoteClusterLicenseChecker.RemoteClusterLicenseInfo("expired-cluster", expiredLicense); + assertThat( + RemoteClusterLicenseChecker.buildErrorMessage("Feature", info, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial), + equalTo("the license on cluster [expired-cluster] is not active")); + } + + private ActionListener doubleInvocationProtectingListener( + final ActionListener listener) { + final AtomicBoolean listenerInvoked = new AtomicBoolean(); + return new ActionListener() { + + @Override + public void onResponse(final RemoteClusterLicenseChecker.LicenseCheck response) { + if (listenerInvoked.compareAndSet(false, true) == false) { + fail("listener invoked twice"); + } + listener.onResponse(response); + } + + @Override + public void onFailure(final Exception e) { + if (listenerInvoked.compareAndSet(false, true) == false) { + fail("listener invoked twice"); + } + listener.onFailure(e); + } + + }; + } + + private ThreadPool createMockThreadPool() { + final ThreadPool threadPool = mock(ThreadPool.class); + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + return threadPool; + } + + private Client createMockClient(final ThreadPool threadPool) { + return createMockClient(threadPool, client -> when(client.getRemoteClusterClient(anyString())).thenReturn(client)); + } + + private Client createMockClientThatThrowsOnGetRemoteClusterClient(final ThreadPool threadPool, final String clusterAlias) { + return createMockClient( + threadPool, + client -> { + when(client.getRemoteClusterClient(clusterAlias)).thenThrow(new IllegalArgumentException()); + when(client.getRemoteClusterClient(argThat(not(clusterAlias)))).thenReturn(client); + }); + } + + private Client createMockClient(final ThreadPool threadPool, final Consumer finish) { + final Client client = mock(Client.class); + when(client.threadPool()).thenReturn(threadPool); + finish.accept(client); + return client; + } + + private XPackInfoResponse.LicenseInfo createPlatinumLicenseResponse() { + return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.ACTIVE, randomNonNegativeLong()); + } + + private XPackInfoResponse.LicenseInfo createBasicLicenseResponse() { + return new XPackInfoResponse.LicenseInfo("uid", "BASIC", "BASIC", LicenseStatus.ACTIVE, randomNonNegativeLong()); + } + + private XPackInfoResponse.LicenseInfo createExpiredLicenseResponse() { + return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.EXPIRED, randomNonNegativeLong()); + } + +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java index 0ea9eb776480..d6ebdd0449e9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.persistent.PersistentTaskState; @@ -46,10 +47,10 @@ import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.ml.datafeed.DatafeedManager; import org.elasticsearch.xpack.ml.datafeed.DatafeedNodeSelector; -import org.elasticsearch.xpack.ml.datafeed.MlRemoteLicenseChecker; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorFactory; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Predicate; @@ -141,19 +142,22 @@ public void onFailure(Exception e) { DatafeedConfig datafeed = mlMetadata.getDatafeed(params.getDatafeedId()); Job job = mlMetadata.getJobs().get(datafeed.getJobId()); - if (MlRemoteLicenseChecker.containsRemoteIndex(datafeed.getIndices())) { - MlRemoteLicenseChecker remoteLicenseChecker = new MlRemoteLicenseChecker(client); - remoteLicenseChecker.checkRemoteClusterLicenses(MlRemoteLicenseChecker.remoteClusterNames(datafeed.getIndices()), + if (RemoteClusterLicenseChecker.containsRemoteIndex(datafeed.getIndices())) { + final RemoteClusterLicenseChecker remoteClusterLicenseChecker = + new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + remoteClusterLicenseChecker.checkRemoteClusterLicenses( + RemoteClusterLicenseChecker.remoteClusterAliases(datafeed.getIndices()), ActionListener.wrap( response -> { - if (response.isViolated()) { + if (response.isSuccess() == false) { listener.onFailure(createUnlicensedError(datafeed.getId(), response)); } else { createDataExtractor(job, datafeed, params, waitForTaskListener); } }, - e -> listener.onFailure(createUnknownLicenseError(datafeed.getId(), - MlRemoteLicenseChecker.remoteIndices(datafeed.getIndices()), e)) + e -> listener.onFailure( + createUnknownLicenseError( + datafeed.getId(), RemoteClusterLicenseChecker.remoteIndices(datafeed.getIndices()), e)) )); } else { createDataExtractor(job, datafeed, params, waitForTaskListener); @@ -232,23 +236,35 @@ public void onFailure(Exception e) { ); } - private ElasticsearchStatusException createUnlicensedError(String datafeedId, - MlRemoteLicenseChecker.LicenseViolation licenseViolation) { - String message = "Cannot start datafeed [" + datafeedId + "] as it is configured to use " - + "indices on a remote cluster [" + licenseViolation.get().getClusterName() - + "] that is not licensed for Machine Learning. " - + MlRemoteLicenseChecker.buildErrorMessage(licenseViolation.get()); - + private ElasticsearchStatusException createUnlicensedError( + final String datafeedId, final RemoteClusterLicenseChecker.LicenseCheck licenseCheck) { + final String message = String.format( + Locale.ROOT, + "cannot start datafeed [%s] as it is configured to use indices on remote cluster [%s] that is not licensed for ml; %s", + datafeedId, + licenseCheck.remoteClusterLicenseInfo().clusterAlias(), + RemoteClusterLicenseChecker.buildErrorMessage( + "ml", + licenseCheck.remoteClusterLicenseInfo(), + RemoteClusterLicenseChecker::isLicensePlatinumOrTrial)); return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST); } - private ElasticsearchStatusException createUnknownLicenseError(String datafeedId, List remoteIndices, - Exception cause) { - String message = "Cannot start datafeed [" + datafeedId + "] as it is configured to use" - + " indices on a remote cluster " + remoteIndices - + " but the license type could not be verified"; - - return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST, new Exception(cause.getMessage())); + private ElasticsearchStatusException createUnknownLicenseError( + final String datafeedId, final List remoteIndices, final Exception cause) { + final int numberOfRemoteClusters = RemoteClusterLicenseChecker.remoteClusterAliases(remoteIndices).size(); + assert numberOfRemoteClusters > 0; + final String remoteClusterQualifier = numberOfRemoteClusters == 1 ? "a remote cluster" : "remote clusters"; + final String licenseTypeQualifier = numberOfRemoteClusters == 1 ? "" : "s"; + final String message = String.format( + Locale.ROOT, + "cannot start datafeed [%s] as it uses indices on %s %s but the license type%s could not be verified", + datafeedId, + remoteClusterQualifier, + remoteIndices, + licenseTypeQualifier); + + return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST, cause); } public static class StartDatafeedPersistentTasksExecutor extends PersistentTasksExecutor { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedNodeSelector.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedNodeSelector.java index a6be04764862..ce3f611b2227 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedNodeSelector.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedNodeSelector.java @@ -12,6 +12,7 @@ import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.license.RemoteClusterLicenseChecker; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.xpack.core.ml.MlMetadata; import org.elasticsearch.xpack.core.ml.MlTasks; @@ -92,7 +93,7 @@ private AssignmentFailure verifyIndicesActive(DatafeedConfig datafeed) { List indices = datafeed.getIndices(); for (String index : indices) { - if (MlRemoteLicenseChecker.isRemoteIndex(index)) { + if (RemoteClusterLicenseChecker.isRemoteIndex(index)) { // We cannot verify remote indices continue; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseChecker.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseChecker.java deleted file mode 100644 index b0eeed2c800e..000000000000 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseChecker.java +++ /dev/null @@ -1,193 +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.ml.datafeed; - -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.client.Client; -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.license.License; -import org.elasticsearch.protocol.xpack.XPackInfoRequest; -import org.elasticsearch.protocol.xpack.XPackInfoResponse; -import org.elasticsearch.protocol.xpack.license.LicenseStatus; -import org.elasticsearch.transport.ActionNotFoundTransportException; -import org.elasticsearch.transport.RemoteClusterAware; -import org.elasticsearch.xpack.core.action.XPackInfoAction; - -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -/** - * ML datafeeds can use cross cluster search to access data in a remote cluster. - * The remote cluster should be licenced for ML this class performs that check - * using the _xpack (info) endpoint. - */ -public class MlRemoteLicenseChecker { - - private final Client client; - - public static class RemoteClusterLicenseInfo { - private final String clusterName; - private final XPackInfoResponse.LicenseInfo licenseInfo; - - RemoteClusterLicenseInfo(String clusterName, XPackInfoResponse.LicenseInfo licenseInfo) { - this.clusterName = clusterName; - this.licenseInfo = licenseInfo; - } - - public String getClusterName() { - return clusterName; - } - - public XPackInfoResponse.LicenseInfo getLicenseInfo() { - return licenseInfo; - } - } - - public class LicenseViolation { - private final RemoteClusterLicenseInfo licenseInfo; - - private LicenseViolation(@Nullable RemoteClusterLicenseInfo licenseInfo) { - this.licenseInfo = licenseInfo; - } - - public boolean isViolated() { - return licenseInfo != null; - } - - public RemoteClusterLicenseInfo get() { - return licenseInfo; - } - } - - public MlRemoteLicenseChecker(Client client) { - this.client = client; - } - - /** - * Check each cluster is licensed for ML. - * This function evaluates lazily and will terminate when the first cluster - * that is not licensed is found or an error occurs. - * - * @param clusterNames List of remote cluster names - * @param listener Response listener - */ - public void checkRemoteClusterLicenses(List clusterNames, ActionListener listener) { - final Iterator itr = clusterNames.iterator(); - if (itr.hasNext() == false) { - listener.onResponse(new LicenseViolation(null)); - return; - } - - final AtomicReference clusterName = new AtomicReference<>(itr.next()); - - ActionListener infoListener = new ActionListener() { - @Override - public void onResponse(XPackInfoResponse xPackInfoResponse) { - if (licenseSupportsML(xPackInfoResponse.getLicenseInfo()) == false) { - listener.onResponse(new LicenseViolation( - new RemoteClusterLicenseInfo(clusterName.get(), xPackInfoResponse.getLicenseInfo()))); - return; - } - - if (itr.hasNext()) { - clusterName.set(itr.next()); - remoteClusterLicense(clusterName.get(), this); - } else { - listener.onResponse(new LicenseViolation(null)); - } - } - - @Override - public void onFailure(Exception e) { - String message = "Could not determine the X-Pack licence type for cluster [" + clusterName.get() + "]"; - if (e instanceof ActionNotFoundTransportException) { - // This is likely to be because x-pack is not installed in the target cluster - message += ". Is X-Pack installed on the target cluster?"; - } - listener.onFailure(new ElasticsearchException(message, e)); - } - }; - - remoteClusterLicense(clusterName.get(), infoListener); - } - - private void remoteClusterLicense(String clusterName, ActionListener listener) { - Client remoteClusterClient = client.getRemoteClusterClient(clusterName); - ThreadContext threadContext = remoteClusterClient.threadPool().getThreadContext(); - try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { - // we stash any context here since this is an internal execution and should not leak any - // existing context information. - threadContext.markAsSystemContext(); - - XPackInfoRequest request = new XPackInfoRequest(); - request.setCategories(EnumSet.of(XPackInfoRequest.Category.LICENSE)); - remoteClusterClient.execute(XPackInfoAction.INSTANCE, request, listener); - } - } - - static boolean licenseSupportsML(XPackInfoResponse.LicenseInfo licenseInfo) { - License.OperationMode mode = License.OperationMode.resolve(licenseInfo.getMode()); - return licenseInfo.getStatus() == LicenseStatus.ACTIVE && - (mode == License.OperationMode.PLATINUM || mode == License.OperationMode.TRIAL); - } - - public static boolean isRemoteIndex(String index) { - return index.indexOf(RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR) != -1; - } - - public static boolean containsRemoteIndex(List indices) { - return indices.stream().anyMatch(MlRemoteLicenseChecker::isRemoteIndex); - } - - /** - * Get any remote indices used in cross cluster search. - * Remote indices are of the form {@code cluster_name:index_name} - * @return List of remote cluster indices - */ - public static List remoteIndices(List indices) { - return indices.stream().filter(MlRemoteLicenseChecker::isRemoteIndex).collect(Collectors.toList()); - } - - /** - * Extract the list of remote cluster names from the list of indices. - * @param indices List of indices. Remote cluster indices are prefixed - * with {@code cluster-name:} - * @return Every cluster name found in {@code indices} - */ - public static List remoteClusterNames(List indices) { - return indices.stream() - .filter(MlRemoteLicenseChecker::isRemoteIndex) - .map(index -> index.substring(0, index.indexOf(RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR))) - .distinct() - .collect(Collectors.toList()); - } - - public static String buildErrorMessage(RemoteClusterLicenseInfo clusterLicenseInfo) { - StringBuilder error = new StringBuilder(); - if (clusterLicenseInfo.licenseInfo.getStatus() != LicenseStatus.ACTIVE) { - error.append("The license on cluster [").append(clusterLicenseInfo.clusterName) - .append("] is not active. "); - } else { - License.OperationMode mode = License.OperationMode.resolve(clusterLicenseInfo.licenseInfo.getMode()); - if (mode != License.OperationMode.PLATINUM && mode != License.OperationMode.TRIAL) { - error.append("The license mode [").append(mode) - .append("] on cluster [") - .append(clusterLicenseInfo.clusterName) - .append("] does not enable Machine Learning. "); - } - } - - error.append(Strings.toString(clusterLicenseInfo.licenseInfo)); - return error.toString(); - } -} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedActionTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedActionTests.java index 72c8d361dd88..610a5c1b92fb 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedActionTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedActionTests.java @@ -3,10 +3,12 @@ * 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.action; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ml.MlMetadata; @@ -14,7 +16,6 @@ import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.config.JobState; -import org.elasticsearch.persistent.PersistentTasksCustomMetaData; import org.elasticsearch.xpack.ml.datafeed.DatafeedManager; import org.elasticsearch.xpack.ml.datafeed.DatafeedManagerTests; diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseCheckerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseCheckerTests.java deleted file mode 100644 index 81e4c75cfad7..000000000000 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseCheckerTests.java +++ /dev/null @@ -1,199 +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.ml.datafeed; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.client.Client; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.protocol.xpack.XPackInfoResponse; -import org.elasticsearch.protocol.xpack.license.LicenseStatus; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.xpack.core.action.XPackInfoAction; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.same; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class MlRemoteLicenseCheckerTests extends ESTestCase { - - public void testIsRemoteIndex() { - List indices = Arrays.asList("local-index1", "local-index2"); - assertFalse(MlRemoteLicenseChecker.containsRemoteIndex(indices)); - indices = Arrays.asList("local-index1", "remote-cluster:remote-index2"); - assertTrue(MlRemoteLicenseChecker.containsRemoteIndex(indices)); - } - - public void testRemoteIndices() { - List indices = Collections.singletonList("local-index"); - assertThat(MlRemoteLicenseChecker.remoteIndices(indices), is(empty())); - indices = Arrays.asList("local-index", "remote-cluster:index1", "local-index2", "remote-cluster2:index1"); - assertThat(MlRemoteLicenseChecker.remoteIndices(indices), containsInAnyOrder("remote-cluster:index1", "remote-cluster2:index1")); - } - - public void testRemoteClusterNames() { - List indices = Arrays.asList("local-index1", "local-index2"); - assertThat(MlRemoteLicenseChecker.remoteClusterNames(indices), empty()); - indices = Arrays.asList("local-index1", "remote-cluster1:remote-index2"); - assertThat(MlRemoteLicenseChecker.remoteClusterNames(indices), contains("remote-cluster1")); - indices = Arrays.asList("remote-cluster1:index2", "index1", "remote-cluster2:index1"); - assertThat(MlRemoteLicenseChecker.remoteClusterNames(indices), contains("remote-cluster1", "remote-cluster2")); - indices = Arrays.asList("remote-cluster1:index2", "index1", "remote-cluster2:index1", "remote-cluster2:index2"); - assertThat(MlRemoteLicenseChecker.remoteClusterNames(indices), contains("remote-cluster1", "remote-cluster2")); - } - - public void testLicenseSupportsML() { - XPackInfoResponse.LicenseInfo licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "trial", "trial", - LicenseStatus.ACTIVE, randomNonNegativeLong()); - assertTrue(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo)); - - licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "trial", "trial", LicenseStatus.EXPIRED, randomNonNegativeLong()); - assertFalse(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo)); - - licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "GOLD", "GOLD", LicenseStatus.ACTIVE, randomNonNegativeLong()); - assertFalse(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo)); - - licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.ACTIVE, randomNonNegativeLong()); - assertTrue(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo)); - } - - public void testCheckRemoteClusterLicenses_givenValidLicenses() { - final AtomicInteger index = new AtomicInteger(0); - final List responses = new ArrayList<>(); - - Client client = createMockClient(); - doAnswer(invocationMock -> { - @SuppressWarnings("raw_types") - ActionListener listener = (ActionListener) invocationMock.getArguments()[2]; - listener.onResponse(responses.get(index.getAndIncrement())); - return null; - }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); - - - List remoteClusterNames = Arrays.asList("valid1", "valid2", "valid3"); - responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); - responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); - responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); - - MlRemoteLicenseChecker licenseChecker = new MlRemoteLicenseChecker(client); - AtomicReference licCheckResponse = new AtomicReference<>(); - - licenseChecker.checkRemoteClusterLicenses(remoteClusterNames, - new ActionListener() { - @Override - public void onResponse(MlRemoteLicenseChecker.LicenseViolation response) { - licCheckResponse.set(response); - } - - @Override - public void onFailure(Exception e) { - fail(e.getMessage()); - } - }); - - verify(client, times(3)).execute(same(XPackInfoAction.INSTANCE), any(), any()); - assertNotNull(licCheckResponse.get()); - assertFalse(licCheckResponse.get().isViolated()); - assertNull(licCheckResponse.get().get()); - } - - public void testCheckRemoteClusterLicenses_givenInvalidLicense() { - final AtomicInteger index = new AtomicInteger(0); - List remoteClusterNames = Arrays.asList("good", "cluster-with-basic-license", "good2"); - final List responses = new ArrayList<>(); - responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); - responses.add(new XPackInfoResponse(null, createBasicLicenseResponse(), null)); - responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); - - Client client = createMockClient(); - doAnswer(invocationMock -> { - @SuppressWarnings("raw_types") - ActionListener listener = (ActionListener) invocationMock.getArguments()[2]; - listener.onResponse(responses.get(index.getAndIncrement())); - return null; - }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); - - MlRemoteLicenseChecker licenseChecker = new MlRemoteLicenseChecker(client); - AtomicReference licCheckResponse = new AtomicReference<>(); - - licenseChecker.checkRemoteClusterLicenses(remoteClusterNames, - new ActionListener() { - @Override - public void onResponse(MlRemoteLicenseChecker.LicenseViolation response) { - licCheckResponse.set(response); - } - - @Override - public void onFailure(Exception e) { - fail(e.getMessage()); - } - }); - - verify(client, times(2)).execute(same(XPackInfoAction.INSTANCE), any(), any()); - assertNotNull(licCheckResponse.get()); - assertTrue(licCheckResponse.get().isViolated()); - assertEquals("cluster-with-basic-license", licCheckResponse.get().get().getClusterName()); - assertEquals("BASIC", licCheckResponse.get().get().getLicenseInfo().getType()); - } - - public void testBuildErrorMessage() { - XPackInfoResponse.LicenseInfo platinumLicence = createPlatinumLicenseResponse(); - MlRemoteLicenseChecker.RemoteClusterLicenseInfo info = - new MlRemoteLicenseChecker.RemoteClusterLicenseInfo("platinum-cluster", platinumLicence); - assertEquals(Strings.toString(platinumLicence), MlRemoteLicenseChecker.buildErrorMessage(info)); - - XPackInfoResponse.LicenseInfo basicLicense = createBasicLicenseResponse(); - info = new MlRemoteLicenseChecker.RemoteClusterLicenseInfo("basic-cluster", basicLicense); - String expected = "The license mode [BASIC] on cluster [basic-cluster] does not enable Machine Learning. " - + Strings.toString(basicLicense); - assertEquals(expected, MlRemoteLicenseChecker.buildErrorMessage(info)); - - XPackInfoResponse.LicenseInfo expiredLicense = createExpiredLicenseResponse(); - info = new MlRemoteLicenseChecker.RemoteClusterLicenseInfo("expired-cluster", expiredLicense); - expected = "The license on cluster [expired-cluster] is not active. " + Strings.toString(expiredLicense); - assertEquals(expected, MlRemoteLicenseChecker.buildErrorMessage(info)); - } - - private Client createMockClient() { - Client client = mock(Client.class); - ThreadPool threadPool = mock(ThreadPool.class); - when(client.threadPool()).thenReturn(threadPool); - when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); - when(client.getRemoteClusterClient(anyString())).thenReturn(client); - return client; - } - - private XPackInfoResponse.LicenseInfo createPlatinumLicenseResponse() { - return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.ACTIVE, randomNonNegativeLong()); - } - - private XPackInfoResponse.LicenseInfo createBasicLicenseResponse() { - return new XPackInfoResponse.LicenseInfo("uid", "BASIC", "BASIC", LicenseStatus.ACTIVE, randomNonNegativeLong()); - } - - private XPackInfoResponse.LicenseInfo createExpiredLicenseResponse() { - return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.EXPIRED, randomNonNegativeLong()); - } -} From 77d7547be2782f5157b7dbb88aa805f8e9593413 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 20 Aug 2018 16:33:15 -0400 Subject: [PATCH 066/283] Fix compilation after merge from master --- .../org/elasticsearch/index/engine/InternalEngineTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 45f77036ba64..123c65da46cc 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -4472,7 +4472,7 @@ public void testCleanUpCommitsWhenGlobalCheckpointAdvanced() throws Exception { globalCheckpoint.set(randomLongBetween(engine.getLocalCheckpoint(), Long.MAX_VALUE)); engine.syncTranslog(); assertThat(DirectoryReader.listCommits(store.directory()), contains(commits.get(commits.size() - 1))); - assertThat(engine.estimateTranslogOperationsFromMinSeq(0L), equalTo(0)); + assertThat(engine.getTranslog().totalOperations(), equalTo(0)); } } From 3fbaae10af1a8ed26d7d1dea2b51ad8371036223 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 20 Aug 2018 16:05:56 -0500 Subject: [PATCH 067/283] HLRC: ML Close Job (#32943) * HLRC: Adding ML Close Job API HLRC: Adding ML Close Job API * reconciling request converters * Adding serialization tests and addressing PR comments * Changing constructor order --- .../client/MLRequestConverters.java | 26 +++ .../client/MachineLearningClient.java | 38 ++++ .../client/MLRequestConvertersTests.java | 26 ++- .../client/MachineLearningIT.java | 15 ++ .../MlClientDocumentationIT.java | 54 +++++ .../high-level/ml/close-job.asciidoc | 59 ++++++ .../java-rest/high-level/ml/open-job.asciidoc | 2 +- .../high-level/supported-apis.asciidoc | 2 + .../protocol/xpack/ml/CloseJobRequest.java | 191 ++++++++++++++++++ .../protocol/xpack/ml/CloseJobResponse.java | 89 ++++++++ .../xpack/ml/CloseJobRequestTests.java | 81 ++++++++ .../xpack/ml/CloseJobResponseTests.java | 42 ++++ 12 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 docs/java-rest/high-level/ml/close-job.asciidoc create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java 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 e26a4c629a0b..7178a9c7fc3e 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 @@ -23,6 +23,8 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.RequestConverters.EndpointBuilder; +import org.elasticsearch.common.Strings; +import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -61,6 +63,30 @@ static Request openJob(OpenJobRequest openJobRequest) throws IOException { return request; } + static Request closeJob(CloseJobRequest closeJobRequest) { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(Strings.collectionToCommaDelimitedString(closeJobRequest.getJobIds())) + .addPathPartAsIs("_close") + .build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + + RequestConverters.Params params = new RequestConverters.Params(request); + if (closeJobRequest.isForce() != null) { + params.putParam("force", Boolean.toString(closeJobRequest.isForce())); + } + if (closeJobRequest.isAllowNoJobs() != null) { + params.putParam("allow_no_jobs", Boolean.toString(closeJobRequest.isAllowNoJobs())); + } + if (closeJobRequest.getTimeout() != null) { + params.putParam("timeout", closeJobRequest.getTimeout().getStringRep()); + } + + return request; + } + static Request deleteJob(DeleteJobRequest deleteJobRequest) { 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 32b6cd6cf2c6..2073d613ac66 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.protocol.xpack.ml.CloseJobRequest; +import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; @@ -166,4 +168,40 @@ public void openJobAsync(OpenJobRequest request, RequestOptions options, ActionL listener, Collections.emptySet()); } + + /** + * Closes one or more Machine Learning Jobs. A job can be opened and closed multiple times throughout its lifecycle. + * + * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results. + * + * @param request request containing job_ids and additional options. See {@link CloseJobRequest} + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return response containing if the job was successfully closed or not. + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public CloseJobResponse closeJob(CloseJobRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::closeJob, + options, + CloseJobResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Closes one or more Machine Learning Jobs asynchronously, notifies listener on completion + * + * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results. + * + * @param request request containing job_ids and additional options. See {@link CloseJobRequest} + * @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 closeJobAsync(CloseJobRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::closeJob, + options, + CloseJobResponse::fromXContent, + listener, + Collections.emptySet()); + } } 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 43a41960e003..a313b99a54f5 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.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -66,6 +67,29 @@ public void testOpenJob() throws Exception { assertEquals(bos.toString("UTF-8"), "{\"job_id\":\""+ jobId +"\",\"timeout\":\"10m\"}"); } + public void testCloseJob() { + String jobId = "somejobid"; + CloseJobRequest closeJobRequest = new CloseJobRequest(jobId); + + Request request = MLRequestConverters.closeJob(closeJobRequest); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_close", request.getEndpoint()); + assertFalse(request.getParameters().containsKey("force")); + assertFalse(request.getParameters().containsKey("allow_no_jobs")); + assertFalse(request.getParameters().containsKey("timeout")); + + closeJobRequest = new CloseJobRequest(jobId, "otherjobs*"); + closeJobRequest.setForce(true); + closeJobRequest.setAllowNoJobs(false); + closeJobRequest.setTimeout(TimeValue.timeValueMinutes(10)); + request = MLRequestConverters.closeJob(closeJobRequest); + + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + ",otherjobs*/_close", request.getEndpoint()); + assertEquals(Boolean.toString(true), request.getParameters().get("force")); + assertEquals(Boolean.toString(false), request.getParameters().get("allow_no_jobs")); + assertEquals("10m", request.getParameters().get("timeout")); + } + public void testDeleteJob() { String jobId = randomAlphaOfLength(10); DeleteJobRequest deleteJobRequest = new DeleteJobRequest(jobId); @@ -87,4 +111,4 @@ private static Job createValidJob(String jobId) { jobBuilder.setAnalysisConfig(analysisConfig); return jobBuilder.build(); } -} \ No newline at end of file +} 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 95a29e99e526..6c4fa6e4514a 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 @@ -21,6 +21,8 @@ import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; +import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; @@ -77,6 +79,19 @@ public void testOpenJob() throws Exception { assertTrue(response.isOpened()); } + public void testCloseJob() throws Exception { + String jobId = randomValidJobId(); + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + machineLearningClient.openJob(new OpenJobRequest(jobId), RequestOptions.DEFAULT); + + CloseJobResponse response = execute(new CloseJobRequest(jobId), + machineLearningClient::closeJob, + machineLearningClient::closeJobAsync); + assertTrue(response.isClosed()); + } + 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 a77d8b43e573..05aad96fff6a 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 @@ -25,6 +25,8 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; +import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; @@ -221,4 +223,56 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testCloseJob() throws Exception { + RestHighLevelClient client = highLevelClient(); + + { + Job job = MachineLearningIT.buildJob("closing-my-first-machine-learning-job"); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT); + + //tag::x-pack-ml-close-job-request + CloseJobRequest closeJobRequest = new CloseJobRequest("closing-my-first-machine-learning-job", "otherjobs*"); //<1> + closeJobRequest.setForce(false); //<2> + closeJobRequest.setAllowNoJobs(true); //<3> + closeJobRequest.setTimeout(TimeValue.timeValueMinutes(10)); //<4> + //end::x-pack-ml-close-job-request + + //tag::x-pack-ml-close-job-execute + CloseJobResponse closeJobResponse = client.machineLearning().closeJob(closeJobRequest, RequestOptions.DEFAULT); + boolean isClosed = closeJobResponse.isClosed(); //<1> + //end::x-pack-ml-close-job-execute + + } + { + Job job = MachineLearningIT.buildJob("closing-my-second-machine-learning-job"); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT); + + //tag::x-pack-ml-close-job-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(CloseJobResponse closeJobResponse) { + //<1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::x-pack-ml-close-job-listener + CloseJobRequest closeJobRequest = new CloseJobRequest("closing-my-second-machine-learning-job"); + // 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-close-job-execute-async + client.machineLearning().closeJobAsync(closeJobRequest, RequestOptions.DEFAULT, listener); //<1> + // end::x-pack-ml-close-job-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } } diff --git a/docs/java-rest/high-level/ml/close-job.asciidoc b/docs/java-rest/high-level/ml/close-job.asciidoc new file mode 100644 index 000000000000..edadb9f40a21 --- /dev/null +++ b/docs/java-rest/high-level/ml/close-job.asciidoc @@ -0,0 +1,59 @@ +[[java-rest-high-x-pack-ml-close-job]] +=== Close Job API + +The Close Job API provides the ability to close {ml} jobs in the cluster. +It accepts a `CloseJobRequest` object and responds +with a `CloseJobResponse` object. + +[[java-rest-high-x-pack-ml-close-job-request]] +==== Close Job Request + +A `CloseJobRequest` object gets created with an existing non-null `jobId`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-close-job-request] +-------------------------------------------------- +<1> Constructing a new request referencing existing job IDs +<2> Optionally used to close a failed job, or to forcefully close a job +which has not responded to its initial close request. +<3> Optionally set to ignore if a wildcard expression matches no jobs. + (This includes `_all` string or when no jobs have been specified) +<4> Optionally setting the `timeout` value for how long the +execution should wait for the job to be closed. + +[[java-rest-high-x-pack-ml-close-job-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-close-job-execute] +-------------------------------------------------- +<1> `isClosed()` from the `CloseJobResponse` indicates if the job was successfully +closed or not. + +[[java-rest-high-x-pack-ml-close-job-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-close-job-execute-async] +-------------------------------------------------- +<1> The `CloseJobRequest` 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 `CloseJobResponse` may +look like + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-close-job-listener] +-------------------------------------------------- +<1> `onResponse` is called back when the action is completed successfully +<2> `onFailure` is called back when some unexpected error occurs diff --git a/docs/java-rest/high-level/ml/open-job.asciidoc b/docs/java-rest/high-level/ml/open-job.asciidoc index ad575121818b..be6a518df193 100644 --- a/docs/java-rest/high-level/ml/open-job.asciidoc +++ b/docs/java-rest/high-level/ml/open-job.asciidoc @@ -44,7 +44,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-open-job-exec the execution completes The method does not block and returns immediately. The passed `ActionListener` is used -to notify the caller of completion. A typical `ActionListner` for `OpenJobResponse` may +to notify the caller of completion. A typical `ActionListener` for `OpenJobResponse` may look like ["source","java",subs="attributes,callouts,macros"] diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 6bcb736243a7..96e93ba204cf 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -207,10 +207,12 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> include::ml/put-job.asciidoc[] include::ml/delete-job.asciidoc[] include::ml/open-job.asciidoc[] +include::ml/close-job.asciidoc[] == Migration APIs diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java new file mode 100644 index 000000000000..1df5a02889e2 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java @@ -0,0 +1,191 @@ +/* + * 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.protocol.xpack.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.ParseField; +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.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class CloseJobRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField JOB_IDS = new ParseField("job_ids"); + public static final ParseField TIMEOUT = new ParseField("timeout"); + public static final ParseField FORCE = new ParseField("force"); + public static final ParseField ALLOW_NO_JOBS = new ParseField("allow_no_jobs"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "close_job_request", + true, a -> new CloseJobRequest((List) a[0])); + + static { + PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), JOB_IDS); + PARSER.declareString((obj, val) -> obj.setTimeout(TimeValue.parseTimeValue(val, TIMEOUT.getPreferredName())), TIMEOUT); + PARSER.declareBoolean(CloseJobRequest::setForce, FORCE); + PARSER.declareBoolean(CloseJobRequest::setAllowNoJobs, ALLOW_NO_JOBS); + } + + private static final String ALL_JOBS = "_all"; + + private final List jobIds; + private TimeValue timeout; + private Boolean force; + private Boolean allowNoJobs; + + /** + * Explicitly close all jobs + * + * @return a {@link CloseJobRequest} for all existing jobs + */ + public static CloseJobRequest closeAllJobsRequest(){ + return new CloseJobRequest(ALL_JOBS); + } + + CloseJobRequest(List jobIds) { + if (jobIds.isEmpty()) { + throw new InvalidParameterException("jobIds must not be empty"); + } + if (jobIds.stream().anyMatch(Objects::isNull)) { + throw new NullPointerException("jobIds must not contain null values"); + } + this.jobIds = new ArrayList<>(jobIds); + } + + /** + * Close the specified Jobs via their unique jobIds + * + * @param jobIds must be non-null and non-empty and each jobId must be non-null + */ + public CloseJobRequest(String... jobIds) { + this(Arrays.asList(jobIds)); + } + + /** + * All the jobIds to be closed + */ + public List getJobIds() { + return jobIds; + } + + /** + * How long to wait for the close request to complete before timing out. + * + * Default: 30 minutes + */ + public TimeValue getTimeout() { + return timeout; + } + + /** + * {@link CloseJobRequest#getTimeout()} + */ + public void setTimeout(TimeValue timeout) { + this.timeout = timeout; + } + + /** + * Should the closing be forced. + * + * Use to close a failed job, or to forcefully close a job which has not responded to its initial close request. + */ + public Boolean isForce() { + return force; + } + + /** + * {@link CloseJobRequest#isForce()} + */ + public void setForce(boolean force) { + this.force = force; + } + + /** + * Whether to ignore if a wildcard expression matches no jobs. + * + * This includes `_all` string or when no jobs have been specified + */ + public Boolean isAllowNoJobs() { + return allowNoJobs; + } + + /** + * {@link CloseJobRequest#isAllowNoJobs()} + */ + public void setAllowNoJobs(boolean allowNoJobs) { + this.allowNoJobs = allowNoJobs; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public int hashCode() { + return Objects.hash(jobIds, timeout, allowNoJobs, force); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + CloseJobRequest that = (CloseJobRequest) other; + return Objects.equals(jobIds, that.jobIds) && + Objects.equals(timeout, that.timeout) && + Objects.equals(allowNoJobs, that.allowNoJobs) && + Objects.equals(force, that.force); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + builder.field(JOB_IDS.getPreferredName(), jobIds); + + if (timeout != null) { + builder.field(TIMEOUT.getPreferredName(), timeout.getStringRep()); + } + if (force != null) { + builder.field(FORCE.getPreferredName(), force); + } + if (allowNoJobs != null) { + builder.field(ALLOW_NO_JOBS.getPreferredName(), allowNoJobs); + } + + builder.endObject(); + return builder; + } +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java new file mode 100644 index 000000000000..9e1f38ef6bab --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.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.protocol.xpack.ml; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; +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 class CloseJobResponse extends ActionResponse implements ToXContentObject { + + private static final ParseField CLOSED = new ParseField("closed"); + + public static final ObjectParser PARSER = + new ObjectParser<>("close_job_response", true, CloseJobResponse::new); + + static { + PARSER.declareBoolean(CloseJobResponse::setClosed, CLOSED); + } + + private boolean closed; + + CloseJobResponse() { + } + + public CloseJobResponse(boolean closed) { + this.closed = closed; + } + + public static CloseJobResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + public boolean isClosed() { + return closed; + } + + public void setClosed(boolean closed) { + this.closed = closed; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + CloseJobResponse that = (CloseJobResponse) other; + return isClosed() == that.isClosed(); + } + + @Override + public int hashCode() { + return Objects.hash(isClosed()); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(CLOSED.getPreferredName(), closed); + builder.endObject(); + return builder; + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.java new file mode 100644 index 000000000000..435504b52983 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.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.protocol.xpack.ml; + +import org.elasticsearch.common.unit.TimeValue; +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 CloseJobRequestTests extends AbstractXContentTestCase { + + public void testCloseAllJobsRequest() { + CloseJobRequest request = CloseJobRequest.closeAllJobsRequest(); + assertEquals(request.getJobIds().size(), 1); + assertEquals(request.getJobIds().get(0), "_all"); + } + + public void testWithNullJobIds() { + Exception exception = expectThrows(IllegalArgumentException.class, CloseJobRequest::new); + assertEquals(exception.getMessage(), "jobIds must not be empty"); + + exception = expectThrows(NullPointerException.class, () -> new CloseJobRequest("job1", null)); + assertEquals(exception.getMessage(), "jobIds must not contain null values"); + } + + + @Override + protected CloseJobRequest createTestInstance() { + int jobCount = randomIntBetween(1, 10); + List jobIds = new ArrayList<>(jobCount); + + for (int i = 0; i < jobCount; i++) { + jobIds.add(randomAlphaOfLength(10)); + } + + CloseJobRequest request = new CloseJobRequest(jobIds.toArray(new String[0])); + + if (randomBoolean()) { + request.setAllowNoJobs(randomBoolean()); + } + + if (randomBoolean()) { + request.setTimeout(TimeValue.timeValueMinutes(randomIntBetween(1, 10))); + } + + if (randomBoolean()) { + request.setForce(randomBoolean()); + } + + return request; + } + + @Override + protected CloseJobRequest doParseInstance(XContentParser parser) throws IOException { + return CloseJobRequest.PARSER.parse(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java new file mode 100644 index 000000000000..d161fde536ec --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.ml; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class CloseJobResponseTests extends AbstractXContentTestCase { + + @Override + protected CloseJobResponse createTestInstance() { + return new CloseJobResponse(randomBoolean()); + } + + @Override + protected CloseJobResponse doParseInstance(XContentParser parser) throws IOException { + return CloseJobResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} From 2feda8aae0ccd39c5c01ee7a232907a0eb499ab8 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 20 Aug 2018 14:30:42 -0700 Subject: [PATCH 068/283] [DOC] Splits role mapping APIs into separate pages (#32797) --- docs/reference/redirects.asciidoc | 8 + x-pack/docs/en/rest-api/defs.asciidoc | 7 +- x-pack/docs/en/rest-api/security.asciidoc | 14 +- .../security/create-role-mappings.asciidoc | 239 +++++++++++ .../security/delete-role-mappings.asciidoc | 50 +++ .../security/get-role-mappings.asciidoc | 74 ++++ .../security/role-mapping-resources.asciidoc | 89 ++++ .../rest-api/security/role-mapping.asciidoc | 404 ------------------ ...onfiguring-active-directory-realm.asciidoc | 2 +- .../configuring-ldap-realm.asciidoc | 2 +- .../configuring-pki-realm.asciidoc | 2 +- .../authentication/saml-guide.asciidoc | 6 +- .../authorization/mapping-roles.asciidoc | 2 +- .../xpack.security.delete_role_mapping.json | 2 +- .../api/xpack.security.get_role_mapping.json | 2 +- .../api/xpack.security.put_role_mapping.json | 2 +- 16 files changed, 487 insertions(+), 418 deletions(-) create mode 100644 x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/delete-role-mappings.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/get-role-mappings.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/role-mapping-resources.asciidoc delete mode 100644 x-pack/docs/en/rest-api/security/role-mapping.asciidoc diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index d67d8a733ac0..6498637873a5 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -531,3 +531,11 @@ native realm: * <>, <> * <> * <> + +[role="exclude",id="security-api-role-mapping"] +=== Role mapping APIs + +You can use the following APIs to add, remove, and retrieve role mappings: + +* <>, <> +* <> \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/defs.asciidoc b/x-pack/docs/en/rest-api/defs.asciidoc index 349ce343c7ae..ed53929391bf 100644 --- a/x-pack/docs/en/rest-api/defs.asciidoc +++ b/x-pack/docs/en/rest-api/defs.asciidoc @@ -2,8 +2,8 @@ [[ml-api-definitions]] == Definitions -These resource definitions are used in {ml} APIs and in {kib} advanced -job configuration options. +These resource definitions are used in {ml} and {security} APIs and in {kib} +advanced {ml} job configuration options. * <> * <> @@ -13,6 +13,7 @@ job configuration options. * <> * <> * <> +* <> * <> [role="xpack"] @@ -26,6 +27,8 @@ include::ml/jobresource.asciidoc[] [role="xpack"] include::ml/jobcounts.asciidoc[] [role="xpack"] +include::security/role-mapping-resources.asciidoc[] +[role="xpack"] include::ml/snapshotresource.asciidoc[] [role="xpack"] include::ml/resultsresource.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index f5b0c8eef667..f34f119ba795 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -7,7 +7,6 @@ You can use the following APIs to perform {security} activities. * <> * <> * <> -* <> * <> [float] @@ -20,6 +19,15 @@ You can use the following APIs to add, remove, and retrieve roles in the native * <> * <> +[float] +[[security-role-mapping-apis]] +=== Role mappings + +You can use the following APIs to add, remove, and retrieve role mappings: + +* <>, <> +* <> + [float] [[security-token-apis]] === Tokens @@ -44,17 +52,19 @@ native realm: include::security/authenticate.asciidoc[] include::security/change-password.asciidoc[] include::security/clear-cache.asciidoc[] +include::security/create-role-mappings.asciidoc[] include::security/clear-roles-cache.asciidoc[] include::security/create-roles.asciidoc[] include::security/create-users.asciidoc[] +include::security/delete-role-mappings.asciidoc[] include::security/delete-roles.asciidoc[] include::security/delete-tokens.asciidoc[] include::security/delete-users.asciidoc[] include::security/disable-users.asciidoc[] include::security/enable-users.asciidoc[] +include::security/get-role-mappings.asciidoc[] include::security/get-roles.asciidoc[] include::security/get-tokens.asciidoc[] include::security/get-users.asciidoc[] include::security/privileges.asciidoc[] -include::security/role-mapping.asciidoc[] include::security/ssl.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc new file mode 100644 index 000000000000..b16ac6ee4dc4 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc @@ -0,0 +1,239 @@ +[role="xpack"] +[[security-api-put-role-mapping]] +=== Add role mappings API + +Adds and updates role mappings. + +==== Request + +`POST /_xpack/security/role_mapping/` + + +`PUT /_xpack/security/role_mapping/` + + +==== Description + +Role mappings define which roles are assigned to each user. Each mapping has +_rules_ that identify users and a list of _roles_ that are +granted to those users. + +NOTE: This API does not create roles. Rather, it maps users to existing roles. +Roles can be created by using <> or +{stack-ov}/defining-roles.html#roles-management-file[roles files]. + +For more information, see +{stack-ov}/mapping-roles.html[Mapping users and groups to roles]. + + +==== Path Parameters + +`name`:: + (string) The distinct name that identifies the role mapping. The name is + used solely as an identifier to facilitate interaction via the API; it does + not affect the behavior of the mapping in any way. + + +==== Request Body + +The following parameters can be specified in the body of a PUT or POST request +and pertain to adding a role mapping: + +`enabled` (required):: +(boolean) Mappings that have `enabled` set to `false` are ignored when role +mapping is performed. + +`metadata`:: +(object) Additional metadata that helps define which roles are assigned to each +user. Within the `metadata` object, keys beginning with `_` are reserved for +system usage. + +`roles` (required):: +(list) A list of roles that are granted to the users that match the role mapping +rules. + +`rules` (required):: +(object) The rules that determine which users should be matched by the mapping. +A rule is a logical condition that is expressed by using a JSON DSL. See +<>. + + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +The following example assigns the "user" role to all users: + +[source, js] +------------------------------------------------------------ +POST /_xpack/security/role_mapping/mapping1 +{ + "roles": [ "user"], + "enabled": true, <1> + "rules": { + "field" : { "username" : "*" } + }, + "metadata" : { <2> + "version" : 1 + } +} +------------------------------------------------------------ +// CONSOLE +<1> Mappings that have `enabled` set to `false` are ignored when role mapping + is performed. +<2> Metadata is optional. + +A successful call returns a JSON structure that shows whether the mapping has +been created or updated. + +[source,js] +-------------------------------------------------- +{ + "role_mapping" : { + "created" : true <1> + } +} +-------------------------------------------------- +// TESTRESPONSE +<1> When an existing mapping is updated, `created` is set to false. + +The following example assigns the "user" and "admin" roles to specific users: + +[source,js] +-------------------------------------------------- +POST /_xpack/security/role_mapping/mapping2 +{ + "roles": [ "user", "admin" ], + "enabled": true, + "rules": { + "field" : { "username" : [ "esadmin01", "esadmin02" ] } + } +} +-------------------------------------------------- +// CONSOLE + +The following example matches any user where either the username is `esadmin` +or the user is in the `cn=admin,dc=example,dc=com` group: + +[source, js] +------------------------------------------------------------ +POST /_xpack/security/role_mapping/mapping3 +{ + "roles": [ "superuser" ], + "enabled": true, + "rules": { + "any": [ + { + "field": { + "username": "esadmin" + } + }, + { + "field": { + "groups": "cn=admins,dc=example,dc=com" + } + } + ] + } +} +------------------------------------------------------------ +// CONSOLE + +The following example matches users who authenticated against a specific realm: +[source, js] +------------------------------------------------------------ +POST /_xpack/security/role_mapping/mapping4 +{ + "roles": [ "ldap-user" ], + "enabled": true, + "rules": { + "field" : { "realm.name" : "ldap1" } + } +} +------------------------------------------------------------ +// CONSOLE + +The following example matches users within a specific LDAP sub-tree: + +[source, js] +------------------------------------------------------------ +POST /_xpack/security/role_mapping/mapping5 +{ + "roles": [ "example-user" ], + "enabled": true, + "rules": { + "field" : { "dn" : "*,ou=subtree,dc=example,dc=com" } + } +} +------------------------------------------------------------ +// CONSOLE + +The following example matches users within a particular LDAP sub-tree in a +specific realm: + +[source, js] +------------------------------------------------------------ +POST /_xpack/security/role_mapping/mapping6 +{ + "roles": [ "ldap-example-user" ], + "enabled": true, + "rules": { + "all": [ + { "field" : { "dn" : "*,ou=subtree,dc=example,dc=com" } }, + { "field" : { "realm.name" : "ldap1" } } + ] + } +} +------------------------------------------------------------ +// CONSOLE + +The rules can be more complex and include wildcard matching. For example, the +following mapping matches any user where *all* of these conditions are met: + +- the _Distinguished Name_ matches the pattern `*,ou=admin,dc=example,dc=com`, + or the username is `es-admin`, or the username is `es-system` +- the user in in the `cn=people,dc=example,dc=com` group +- the user does not have a `terminated_date` + + +[source, js] +------------------------------------------------------------ +POST /_xpack/security/role_mapping/mapping7 +{ + "roles": [ "superuser" ], + "enabled": true, + "rules": { + "all": [ + { + "any": [ + { + "field": { + "dn": "*,ou=admin,dc=example,dc=com" + } + }, + { + "field": { + "username": [ "es-admin", "es-system" ] + } + } + ] + }, + { + "field": { + "groups": "cn=people,dc=example,dc=com" + } + }, + { + "except": { + "field": { + "metadata.terminated_date": null + } + } + } + ] + } +} +------------------------------------------------------------ +// CONSOLE diff --git a/x-pack/docs/en/rest-api/security/delete-role-mappings.asciidoc b/x-pack/docs/en/rest-api/security/delete-role-mappings.asciidoc new file mode 100644 index 000000000000..dc9bf2ba1090 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/delete-role-mappings.asciidoc @@ -0,0 +1,50 @@ +[role="xpack"] +[[security-api-delete-role-mapping]] +=== Delete role mappings API + +Removes role mappings. + +==== Request + +`DELETE /_xpack/security/role_mapping/` + +==== Description + +Role mappings define which roles are assigned to each user. For more information, +see {stack-ov}/mapping-roles.html[Mapping users and groups to roles]. + +==== Path Parameters + +`name`:: + (string) The distinct name that identifies the role mapping. The name is + used solely as an identifier to facilitate interaction via the API; it does + not affect the behavior of the mapping in any way. + +//==== Request Body + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +The following example delete a role mapping: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/security/role_mapping/mapping1 +-------------------------------------------------- +// CONSOLE +// TEST[setup:role_mapping] + +If the mapping is successfully deleted, the request returns `{"found": true}`. +Otherwise, `found` is set to false. + +[source,js] +-------------------------------------------------- +{ + "found" : true +} +-------------------------------------------------- +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/security/get-role-mappings.asciidoc b/x-pack/docs/en/rest-api/security/get-role-mappings.asciidoc new file mode 100644 index 000000000000..7abe34b32f56 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/get-role-mappings.asciidoc @@ -0,0 +1,74 @@ +[role="xpack"] +[[security-api-get-role-mapping]] +=== Get role mappings API + +Retrieves role mappings. + +==== Request + +`GET /_xpack/security/role_mapping` + + +`GET /_xpack/security/role_mapping/` + +==== Description + +Role mappings define which roles are assigned to each user. For more information, +see {stack-ov}/mapping-roles.html[Mapping users and groups to roles]. + +==== Path Parameters + +`name`:: + (string) The distinct name that identifies the role mapping. The name is + used solely as an identifier to facilitate interaction via the API; it does + not affect the behavior of the mapping in any way. You can specify multiple + mapping names as a comma-separated list. If you do not specify this + parameter, the API returns information about all role mappings. + +//==== Request Body + +==== Results + +A successful call retrieves an object, where the keys are the +names of the request mappings, and the values are the JSON representation of +those mappings. For more information, see +<>. + +If there is no mapping with the requested name, the +response will have status code `404`. + + +==== Authorization + +To use this API, you must have at least the `manage_security` cluster privilege. + + +==== Examples + +The following example retrieves information about the `mapping1` role mapping: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/role_mapping/mapping1 +-------------------------------------------------- +// CONSOLE +// TEST[setup:role_mapping] + + +[source,js] +-------------------------------------------------- +{ + "mapping1": { + "enabled": true, + "roles": [ + "user" + ], + "rules": { + "field": { + "username": "*" + } + }, + "metadata": {} + } +} +-------------------------------------------------- +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/security/role-mapping-resources.asciidoc b/x-pack/docs/en/rest-api/security/role-mapping-resources.asciidoc new file mode 100644 index 000000000000..be4afc57a1a5 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/role-mapping-resources.asciidoc @@ -0,0 +1,89 @@ +[role="xpack"] +[[role-mapping-resources]] +=== Role mapping resources + +A role mapping resource has the following properties: + +`enabled`:: +(boolean) Mappings that have `enabled` set to `false` are ignored when role +mapping is performed. + +`metadata`:: +(object) Additional metadata that helps define which roles are assigned to each +user. Within the `metadata` object, keys beginning with `_` are reserved for +system usage. + +`roles`:: +(list) A list of roles that are granted to the users that match the role mapping +rules. + +`rules`:: +(object) The rules that determine which users should be matched by the mapping. +A rule is a logical condition that is expressed by using a JSON DSL. The DSL supports the following rule types: +`any`::: +(array of rules) If *any* of its children are true, it evaluates to `true`. +`all`::: +(array of rules) If *all* of its children are true, it evaluates to `true`. +`field`::: +(object) See <>. +`except`:: +(object) A single rule as an object. Only valid as a child of an `all` rule. If +its child is `false`, the `except` is `true`. + + +[float] +[[mapping-roles-rule-field]] +==== Field rules + +The `field` rule is the primary building block for a role mapping expression. +It takes a single object as its value and that object must contain a single +member with key _F_ and value _V_. The field rule looks up the value of _F_ +within the user object and then tests whether the user value _matches_ the +provided value _V_. + +The value specified in the field rule can be one of the following types: +[cols="2,5,3m"] +|======================= +| Type | Description | Example + +| Simple String | Exactly matches the provided value. | "esadmin" +| Wildcard String | Matches the provided value using a wildcard. | "*,dc=example,dc=com" +| Regular Expression | Matches the provided value using a + {ref}/query-dsl-regexp-query.html#regexp-syntax[Lucene regexp]. | "/.\*-admin[0-9]*/" +| Number | Matches an equivalent numerical value. | 7 +| Null | Matches a null or missing value. | null +| Array | Tests each element in the array in + accordance with the above definitions. + If _any_ of elements match, the match is successful. | ["admin", "operator"] +|======================= + +[float] +===== User fields + +The _user object_ against which rules are evaluated has the following fields: + +`username`:: +(string) The username by which {security} knows this user. For example, `"username": "jsmith"`. +`dn`:: +(string) The _Distinguished Name_ of the user. For example, `"dn": "cn=jsmith,ou=users,dc=example,dc=com",`. +`groups`:: +(array of strings) The groups to which the user belongs. For example, `"groups" : [ "cn=admin,ou=groups,dc=example,dc=com","cn=esusers,ou=groups,dc=example,dc=com ]`. +`metadata`:: +(object) Additional metadata for the user. For example, `"metadata": { "cn": "John Smith" }`. +`realm`:: +(object) The realm that authenticated the user. The only field in this object is the realm name. For example, `"realm": { "name": "ldap1" }`. + +The `groups` field is multi-valued; a user can belong to many groups. When a +`field` rule is applied against a multi-valued field, it is considered to match +if _at least one_ of the member values matches. For example, the following rule +matches any user who is a member of the `admin` group, regardless of any +other groups they belong to: + +[source, js] +------------------------------------------------------------ +{ "field" : { "groups" : "admin" } } +------------------------------------------------------------ +// NOTCONSOLE + +For additional realm-specific details, see +{stack-ov}/mapping-roles.html#ldap-role-mapping[Mapping Users and Groups to Roles]. diff --git a/x-pack/docs/en/rest-api/security/role-mapping.asciidoc b/x-pack/docs/en/rest-api/security/role-mapping.asciidoc deleted file mode 100644 index c8006346d4e8..000000000000 --- a/x-pack/docs/en/rest-api/security/role-mapping.asciidoc +++ /dev/null @@ -1,404 +0,0 @@ -[role="xpack"] -[[security-api-role-mapping]] -=== Role Mapping APIs - -The Role Mapping API enables you to add, remove, and retrieve role mappings. - -==== Request - -`GET /_xpack/security/role_mapping` + - -`GET /_xpack/security/role_mapping/` + - -`DELETE /_xpack/security/role_mapping/` + - -`POST /_xpack/security/role_mapping/` + - -`PUT /_xpack/security/role_mapping/` - -==== Description - -Role mappings have _rules_ that identify users and a list of _roles_ that are -granted to those users. - -NOTE: This API does not create roles. Rather, it maps users to existing roles. -Roles can be created by using <> or -{xpack-ref}/defining-roles.html#roles-management-file[roles files]. - -The role mapping rule is a logical condition that is expressed using a JSON DSL. -The DSL supports the following rule types: - -|======================= -| Type | Value Type (child) | Description - -| `any` | An array of rules | If *any* of its children are true, it - evaluates to `true`. -| `all` | An array of rules | If *all* of its children are true, it - evaluates to `true`. -| `field` | An object | See <> -| `except` | A single rule as an object | Only valid as a child of an `all` - rule. If its child is `false`, the - `except` is `true`. -|======================= - -[float] -[[mapping-roles-rule-field]] -===== The Field Rule - -The `field` rule is the primary building block for a role-mapping expression. -It takes a single object as its value and that object must contain a single -member with key _F_ and value _V_. The field rule looks up the value of _F_ -within the user object and then tests whether the user value _matches_ the -provided value _V_. - -The value specified in the field rule can be one of the following types: -[cols="2,5,3m"] -|======================= -| Type | Description | Example - -| Simple String | Exactly matches the provided value. | "esadmin" -| Wildcard String | Matches the provided value using a wildcard. | "*,dc=example,dc=com" -| Regular Expression | Matches the provided value using a - {ref}/query-dsl-regexp-query.html#regexp-syntax[Lucene regexp]. | "/.\*-admin[0-9]*/" -| Number | Matches an equivalent numerical value. | 7 -| Null | Matches a null or missing value. | null -| Array | Tests each element in the array in - accordance with the above definitions. - If _any_ of elements match, the match is successful. | ["admin", "operator"] -|======================= - -===== User Fields - -The _user object_ against which rules are evaluated has the following fields: -[cols="1s,,,m"] -|======================= -| Name | Type | Description | Example - -| username | string | The username by which {security} knows this user. | `"username": "jsmith"` -| dn | string | The _Distinguished Name_ of the user. | `"dn": "cn=jsmith,ou=users,dc=example,dc=com",` -| groups | array-of-string | The groups to which the user belongs. | `"groups" : [ "cn=admin,ou=groups,dc=example,dc=com", -"cn=esusers,ou=groups,dc=example,dc=com ]` -| metadata | object | Additional metadata for the user. | `"metadata": { "cn": "John Smith" }` -| realm | object | The realm that authenticated the user. The only field in this object is the realm name. | `"realm": { "name": "ldap1" }` -|======================= - -The `groups` field is multi-valued; a user can belong to many groups. When a -`field` rule is applied against a multi-valued field, it is considered to match -if _at least one_ of the member values matches. For example, the following rule -matches any user who is a member of the `admin` group, regardless of any -other groups they belong to: - -[source, js] ------------------------------------------------------------- -{ "field" : { "groups" : "admin" } } ------------------------------------------------------------- -// NOTCONSOLE - -For additional realm-specific details, see -{xpack-ref}/mapping-roles.html#ldap-role-mapping[Mapping Users and Groups to Roles]. - - -==== Path Parameters - -`name`:: - (string) The distinct name that identifies the role mapping. The name is - used solely as an identifier to facilitate interaction via the API; it does - not affect the behavior of the mapping in any way. If you do not specify this - parameter for the Get Role Mappings API, it returns information about all - role mappings. - - -==== Request Body - -The following parameters can be specified in the body of a PUT or POST request -and pertain to adding a role mapping: - -`enabled` (required):: -(boolean) Mappings that have `enabled` set to `false` are ignored when role -mapping is performed. - -`metadata`:: -(object) Additional metadata that helps define which roles are assigned to each -user. Within the `metadata` object, keys beginning with `_` are reserved for -system usage. - -`roles` (required):: -(list) A list of roles that are granted to the users that match the role-mapping -rules. - -`rules` (required):: -(object) The rules that determine which users should be matched by the mapping. -A rule is a logical condition that is expressed by using a JSON DSL. - - -==== Authorization - -To use this API, you must have at least the `manage_security` cluster privilege. - - -==== Examples - -[[security-api-put-role-mapping]] -To add a role mapping, submit a PUT or POST request to the `/_xpack/security/role_mapping/` endpoint. The following example assigns -the "user" role to all users: - -[source, js] ------------------------------------------------------------- -POST /_xpack/security/role_mapping/mapping1 -{ - "roles": [ "user"], - "enabled": true, <1> - "rules": { - "field" : { "username" : "*" } - }, - "metadata" : { <2> - "version" : 1 - } -} ------------------------------------------------------------- -// CONSOLE -<1> Mappings that have `enabled` set to `false` are ignored when role mapping - is performed. -<2> Metadata is optional. - -A successful call returns a JSON structure that shows whether the mapping has -been created or updated. - -[source,js] --------------------------------------------------- -{ - "role_mapping" : { - "created" : true <1> - } -} --------------------------------------------------- -// TESTRESPONSE -<1> When an existing mapping is updated, `created` is set to false. - -The following example assigns the "user" and "admin" roles to specific users: - -[source,js] --------------------------------------------------- -POST /_xpack/security/role_mapping/mapping2 -{ - "roles": [ "user", "admin" ], - "enabled": true, - "rules": { - "field" : { "username" : [ "esadmin01", "esadmin02" ] } - } -} --------------------------------------------------- -// CONSOLE - -The following example matches any user where either the username is `esadmin` -or the user is in the `cn=admin,dc=example,dc=com` group: - -[source, js] ------------------------------------------------------------- -POST /_xpack/security/role_mapping/mapping3 -{ - "roles": [ "superuser" ], - "enabled": true, - "rules": { - "any": [ - { - "field": { - "username": "esadmin" - } - }, - { - "field": { - "groups": "cn=admins,dc=example,dc=com" - } - } - ] - } -} ------------------------------------------------------------- -// CONSOLE - -The following example matches users who authenticated against a specific realm: -[source, js] ------------------------------------------------------------- -POST /_xpack/security/role_mapping/mapping4 -{ - "roles": [ "ldap-user" ], - "enabled": true, - "rules": { - "field" : { "realm.name" : "ldap1" } - } -} ------------------------------------------------------------- -// CONSOLE - -The following example matches users within a specific LDAP sub-tree: - -[source, js] ------------------------------------------------------------- -POST /_xpack/security/role_mapping/mapping5 -{ - "roles": [ "example-user" ], - "enabled": true, - "rules": { - "field" : { "dn" : "*,ou=subtree,dc=example,dc=com" } - } -} ------------------------------------------------------------- -// CONSOLE - -The following example matches users within a particular LDAP sub-tree in a -specific realm: - -[source, js] ------------------------------------------------------------- -POST /_xpack/security/role_mapping/mapping6 -{ - "roles": [ "ldap-example-user" ], - "enabled": true, - "rules": { - "all": [ - { "field" : { "dn" : "*,ou=subtree,dc=example,dc=com" } }, - { "field" : { "realm.name" : "ldap1" } } - ] - } -} ------------------------------------------------------------- -// CONSOLE - -The rules can be more complex and include wildcard matching. For example, the -following mapping matches any user where *all* of these conditions are met: - -- the _Distinguished Name_ matches the pattern `*,ou=admin,dc=example,dc=com`, - or the username is `es-admin`, or the username is `es-system` -- the user in in the `cn=people,dc=example,dc=com` group -- the user does not have a `terminated_date` - - -[source, js] ------------------------------------------------------------- -POST /_xpack/security/role_mapping/mapping7 -{ - "roles": [ "superuser" ], - "enabled": true, - "rules": { - "all": [ - { - "any": [ - { - "field": { - "dn": "*,ou=admin,dc=example,dc=com" - } - }, - { - "field": { - "username": [ "es-admin", "es-system" ] - } - } - ] - }, - { - "field": { - "groups": "cn=people,dc=example,dc=com" - } - }, - { - "except": { - "field": { - "metadata.terminated_date": null - } - } - } - ] - } -} ------------------------------------------------------------- -// CONSOLE - -[[security-api-get-role-mapping]] -To retrieve a role mapping, issue a GET request to the -`/_xpack/security/role_mapping/` endpoint: - -[source,js] --------------------------------------------------- -GET /_xpack/security/role_mapping/mapping7 --------------------------------------------------- -// CONSOLE -// TEST[continued] - -A successful call retrieves an object, where the keys are the -names of the request mappings, and the values are -the JSON representation of those mappings. -If there is no mapping with the requested name, the -response will have status code `404`. - -[source,js] --------------------------------------------------- -{ - "mapping7": { - "enabled": true, - "roles": [ - "superuser" - ], - "rules": { - "all": [ - { - "any": [ - { - "field": { - "dn": "*,ou=admin,dc=example,dc=com" - } - }, - { - "field": { - "username": [ - "es-admin", - "es-system" - ] - } - } - ] - }, - { - "field": { - "groups": "cn=people,dc=example,dc=com" - } - }, - { - "except": { - "field": { - "metadata.terminated_date": null - } - } - } - ] - }, - "metadata": {} - } -} --------------------------------------------------- -// TESTRESPONSE - -You can specify multiple mapping names as a comma-separated list. -To retrieve all mappings, omit the name entirely. - -[[security-api-delete-role-mapping]] -To delete a role mapping, submit a DELETE request to the -`/_xpack/security/role_mapping/` endpoint: - -[source,js] --------------------------------------------------- -DELETE /_xpack/security/role_mapping/mapping1 --------------------------------------------------- -// CONSOLE -// TEST[setup:role_mapping] - -If the mapping is successfully deleted, the request returns `{"found": true}`. -Otherwise, `found` is set to false. - -[source,js] --------------------------------------------------- -{ - "found" : true -} --------------------------------------------------- -// TESTRESPONSE diff --git a/x-pack/docs/en/security/authentication/configuring-active-directory-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-active-directory-realm.asciidoc index 6298bb8ef9f5..ba554eb8595d 100644 --- a/x-pack/docs/en/security/authentication/configuring-active-directory-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-active-directory-realm.asciidoc @@ -173,7 +173,7 @@ represent user roles for different systems in the organization. The `active_directory` realm enables you to map Active Directory users to roles via their Active Directory groups or other metadata. This role mapping can be -configured via the <> or by using +configured via the <> or by using a file stored on each node. When a user authenticates against an Active Directory realm, the privileges for that user are the union of all privileges defined by the roles to which the user is mapped. diff --git a/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc index e32c9eb5300b..d3572ae5e1b9 100644 --- a/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc @@ -133,7 +133,7 @@ supports both failover and load balancing modes of operation. See -- The `ldap` realm enables you to map LDAP users to to roles via their LDAP groups, or other metadata. This role mapping can be configured via the -{ref}/security-api-role-mapping.html[role-mapping API] or by using a file stored +{ref}/security-api-put-role-mapping.html[add role mapping API] or by using a file stored on each node. When a user authenticates with LDAP, the privileges for that user are the union of all privileges defined by the roles to which the user is mapped. diff --git a/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc index f66a82b06641..acaa8429d07f 100644 --- a/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc @@ -126,7 +126,7 @@ The `certificate_authorities` option can be used as an alternative to the + -- You map roles for PKI users through the -<> or by using a file stored on +<> or by using a file stored on each node. When a user authenticates against a PKI realm, the privileges for that user are the union of all privileges defined by the roles to which the user is mapped. diff --git a/x-pack/docs/en/security/authentication/saml-guide.asciidoc b/x-pack/docs/en/security/authentication/saml-guide.asciidoc index 7139f4f81987..633140f1238e 100644 --- a/x-pack/docs/en/security/authentication/saml-guide.asciidoc +++ b/x-pack/docs/en/security/authentication/saml-guide.asciidoc @@ -592,9 +592,9 @@ When a user authenticates using SAML, they are identified to the Elastic Stack, but this does not automatically grant them access to perform any actions or access any data. -Your SAML users cannot do anything until they are mapped to X-Pack Security +Your SAML users cannot do anything until they are mapped to {security} roles. This mapping is performed through the -{ref}/security-api-role-mapping.html[role-mapping API] +{ref}/security-api-put-role-mapping.html[add role mapping API]. This is an example of a simple role mapping that grants the `kibana_user` role to any user who authenticates against the `saml1` realm: @@ -626,7 +626,7 @@ mapping are derived from the SAML attributes as follows: - `metadata`: See <> For more information, see <> and -{ref}/security-api-role-mapping.html[Role Mapping APIs]. +{ref}/security-api.html#security-role-mapping-apis[role mapping APIs]. If your IdP has the ability to provide groups or roles to Service Providers, then you should map this SAML attribute to the `attributes.groups` setting in diff --git a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc index 36f3a1f27f34..ecafe2bd3ec9 100644 --- a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc @@ -28,7 +28,7 @@ you are able to map users to both API-managed roles and file-managed roles ==== Using the role mapping API You can define role-mappings through the -{ref}/security-api-role-mapping.html[role mapping API]. +{ref}/security-api-put-role-mapping.html[add role mapping API]. [[mapping-roles-file]] ==== Using role mapping files diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role_mapping.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role_mapping.json index 26c72666e8fa..4c1df6b99db7 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role_mapping.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.delete_role_mapping.json @@ -1,6 +1,6 @@ { "xpack.security.delete_role_mapping": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html#security-api-delete-role-mapping", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delete-role-mapping.html", "methods": [ "DELETE" ], "url": { "path": "/_xpack/security/role_mapping/{name}", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role_mapping.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role_mapping.json index 0bdeb54cfb67..7696f6671e48 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role_mapping.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.get_role_mapping.json @@ -1,6 +1,6 @@ { "xpack.security.get_role_mapping": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html#security-api-get-role-mapping", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-role-mapping.html", "methods": [ "GET" ], "url": { "path": "/_xpack/security/role_mapping/{name}", diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role_mapping.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role_mapping.json index 3f92cd130bab..98e723d80e9b 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role_mapping.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.put_role_mapping.json @@ -1,6 +1,6 @@ { "xpack.security.put_role_mapping": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-role-mapping.html#security-api-put-role-mapping", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-role-mapping.html", "methods": [ "PUT", "POST" ], "url": { "path": "/_xpack/security/role_mapping/{name}", From cd83ddceccbf8cfbcddd9e1a72db7b98101f90e9 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Mon, 20 Aug 2018 16:09:22 -0600 Subject: [PATCH 069/283] Fix assertion in AbstractSimpleTransportTestCase (#32991) This is a follow-up to #32956. That commit incorrectly used assertBusy which led to a possible race in the test. This commit fixes it. --- .../transport/AbstractSimpleTransportTestCase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java index 4e59aaecf8de..21dbc561c6b0 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java @@ -2650,7 +2650,7 @@ public void testChannelCloseWhileConnecting() { public void onConnectionOpened(final Transport.Connection connection) { closeConnectionChannel(connection); try { - assertBusy(connection::isClosed); + assertBusy(() -> assertTrue(connection.isClosed())); } catch (Exception e) { throw new AssertionError(e); } From ad0a965db9af15c49618a2f65c847ad4bbae1547 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 20 Aug 2018 22:07:16 -0400 Subject: [PATCH 070/283] Protect scheduler engine against throwing listeners (#32998) There are two problems with the scheduler engine today. Both relate to listeners that throw. The first problem is that any triggered listener that throws a plain old exception will cause no additional listeners to be triggered for the event, and will also cause the scheduler to never be invoked again. This leads to lost events and is bad. The second problem is that any triggered listener that throws an error of the fatal kind will not lead to that error because caught by the uncaught exception handler. This is because the triggered listener is executed as a future task under a scheduled thread pool executor. A throwable there goes caught by the JDK framework and set as the outcome on the future task. Since we never inspect these tasks for their outcomes, nor is there a good place to do this, we have to handle these errors ourselves. To do this, we catch them and dispatch them to the uncaught exception handler via a forked thread. This is similar to our handling in Netty. --- .../org/elasticsearch/ExceptionsHelper.java | 22 +-- .../xpack/core/scheduler/SchedulerEngine.java | 48 +++++- .../core/scheduler/SchedulerEngineTests.java | 159 ++++++++++++++++++ x-pack/qa/evil-tests/build.gradle | 9 + .../scheduler/EvilSchedulerEngineTests.java | 84 +++++++++ 5 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java create mode 100644 x-pack/qa/evil-tests/build.gradle create mode 100644 x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/core/scheduler/EvilSchedulerEngineTests.java diff --git a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java index 59eb8b60dadb..09347f519fb2 100644 --- a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java +++ b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java @@ -250,13 +250,13 @@ public static boolean reThrowIfNotNull(@Nullable Throwable e) { * @param throwable the throwable to test */ public static void dieOnError(Throwable throwable) { - final Optional maybeError = ExceptionsHelper.maybeError(throwable, logger); - if (maybeError.isPresent()) { + ExceptionsHelper.maybeError(throwable, logger).ifPresent(error -> { /* - * Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, Netty wraps too many - * invocations of user-code in try/catch blocks that swallow all throwables. This means that a rethrow here will not bubble up - * to where we want it to. So, we fork a thread and throw the exception from there where Netty can not get to it. We do not wrap - * the exception so as to not lose the original cause during exit. + * Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, sometimes the stack + * contains statements that catch any throwable (e.g., Netty, and the JDK futures framework). This means that a rethrow here + * will not bubble up to where we want it to. So, we fork a thread and throw the exception from there where we are sure the + * stack does not contain statements that catch any throwable. We do not wrap the exception so as to not lose the original cause + * during exit. */ try { // try to log the current stack trace @@ -264,12 +264,12 @@ public static void dieOnError(Throwable throwable) { logger.error("fatal error\n{}", formatted); } finally { new Thread( - () -> { - throw maybeError.get(); - }) - .start(); + () -> { + throw error; + }) + .start(); } - } + }); } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java index ffc0257313b3..71abb61bdcab 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java @@ -3,8 +3,13 @@ * 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.scheduler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -14,6 +19,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -89,13 +95,20 @@ public interface Schedule { } private final Map schedules = ConcurrentCollections.newConcurrentMap(); - private final ScheduledExecutorService scheduler; private final Clock clock; + private final ScheduledExecutorService scheduler; + private final Logger logger; private final List listeners = new CopyOnWriteArrayList<>(); - public SchedulerEngine(Settings settings, Clock clock) { - this.clock = clock; - this.scheduler = Executors.newScheduledThreadPool(1, EsExecutors.daemonThreadFactory(settings, "trigger_engine_scheduler")); + public SchedulerEngine(final Settings settings, final Clock clock) { + this(settings, clock, LogManager.getLogger(SchedulerEngine.class)); + } + + SchedulerEngine(final Settings settings, final Clock clock, final Logger logger) { + this.clock = Objects.requireNonNull(clock, "clock"); + this.scheduler = Executors.newScheduledThreadPool( + 1, EsExecutors.daemonThreadFactory(Objects.requireNonNull(settings, "settings"), "trigger_engine_scheduler")); + this.logger = Objects.requireNonNull(logger, "logger"); } public void register(Listener listener) { @@ -144,10 +157,15 @@ public int jobCount() { return schedules.size(); } - protected void notifyListeners(String name, long triggeredTime, long scheduledTime) { + protected void notifyListeners(final String name, final long triggeredTime, final long scheduledTime) { final Event event = new Event(name, triggeredTime, scheduledTime); - for (Listener listener : listeners) { - listener.triggered(event); + for (final Listener listener : listeners) { + try { + listener.triggered(event); + } catch (final Exception e) { + // do not allow exceptions to escape this method; we should continue to notify listeners and schedule the next run + logger.warn(new ParameterizedMessage("listener failed while handling triggered event [{}]", name), e); + } } } @@ -169,8 +187,20 @@ class ActiveSchedule implements Runnable { @Override public void run() { - long triggeredTime = clock.millis(); - notifyListeners(name, triggeredTime, scheduledTime); + final long triggeredTime = clock.millis(); + try { + notifyListeners(name, triggeredTime, scheduledTime); + } catch (final Throwable t) { + /* + * Allowing the throwable to escape here will lead to be it being caught in FutureTask#run and set as the outcome of this + * task; however, we never inspect the the outcomes of these scheduled tasks and so allowing the throwable to escape + * unhandled here could lead to use losing fatal errors. Instead, we rely on ExceptionsHelper#dieOnError to appropriately + * dispatch any error to the uncaught exception handler. We should never see an exception here as these do not escape from + * SchedulerEngine#notifyListeners. + */ + ExceptionsHelper.dieOnError(t); + throw t; + } scheduleNextRun(triggeredTime); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java new file mode 100644 index 000000000000..869a320fb638 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java @@ -0,0 +1,159 @@ +/* + * 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.scheduler; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.mockito.ArgumentCaptor; + +import java.time.Clock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class SchedulerEngineTests extends ESTestCase { + + public void testListenersThrowingExceptionsDoNotCauseOtherListenersToBeSkipped() throws InterruptedException { + final Logger mockLogger = mock(Logger.class); + final SchedulerEngine engine = new SchedulerEngine(Settings.EMPTY, Clock.systemUTC(), mockLogger); + try { + final List> listeners = new ArrayList<>(); + final int numberOfListeners = randomIntBetween(1, 32); + int numberOfFailingListeners = 0; + final CountDownLatch latch = new CountDownLatch(numberOfListeners); + for (int i = 0; i < numberOfListeners; i++) { + final AtomicBoolean trigger = new AtomicBoolean(); + final SchedulerEngine.Listener listener; + if (randomBoolean()) { + listener = event -> { + if (trigger.compareAndSet(false, true)) { + latch.countDown(); + } else { + fail("listener invoked twice"); + } + }; + } else { + numberOfFailingListeners++; + listener = event -> { + if (trigger.compareAndSet(false, true)) { + latch.countDown(); + throw new RuntimeException(getTestName()); + } else { + fail("listener invoked twice"); + } + }; + } + listeners.add(Tuple.tuple(listener, trigger)); + } + + // randomize the order and register the listeners + Collections.shuffle(listeners, random()); + listeners.stream().map(Tuple::v1).forEach(engine::register); + + final AtomicBoolean scheduled = new AtomicBoolean(); + engine.add(new SchedulerEngine.Job( + getTestName(), + (startTime, now) -> { + // only allow one triggering of the listeners + if (scheduled.compareAndSet(false, true)) { + return 0; + } else { + return -1; + } + })); + + latch.await(); + + // now check that every listener was invoked + assertTrue(listeners.stream().map(Tuple::v2).allMatch(AtomicBoolean::get)); + if (numberOfFailingListeners > 0) { + assertFailedListenerLogMessage(mockLogger, numberOfFailingListeners); + } + verifyNoMoreInteractions(mockLogger); + } finally { + engine.stop(); + } + } + + public void testListenersThrowingExceptionsDoNotCauseNextScheduledTaskToBeSkipped() throws InterruptedException { + final Logger mockLogger = mock(Logger.class); + final SchedulerEngine engine = new SchedulerEngine(Settings.EMPTY, Clock.systemUTC(), mockLogger); + try { + final List> listeners = new ArrayList<>(); + final int numberOfListeners = randomIntBetween(1, 32); + final int numberOfSchedules = randomIntBetween(1, 32); + final CountDownLatch listenersLatch = new CountDownLatch(numberOfSchedules * numberOfListeners); + for (int i = 0; i < numberOfListeners; i++) { + final AtomicInteger triggerCount = new AtomicInteger(); + final SchedulerEngine.Listener listener = event -> { + if (triggerCount.incrementAndGet() <= numberOfSchedules) { + listenersLatch.countDown(); + throw new RuntimeException(getTestName()); + } else { + fail("listener invoked more than [" + numberOfSchedules + "] times"); + } + }; + listeners.add(Tuple.tuple(listener, triggerCount)); + engine.register(listener); + } + + // latch for each invocation of nextScheduledTimeAfter, once for each scheduled run, and then a final time when we disable + final CountDownLatch latch = new CountDownLatch(1 + numberOfSchedules); + engine.add(new SchedulerEngine.Job( + getTestName(), + (startTime, now) -> { + if (latch.getCount() >= 2) { + latch.countDown(); + return 0; + } else if (latch.getCount() == 1) { + latch.countDown(); + return -1; + } else { + throw new AssertionError("nextScheduledTimeAfter invoked more than the expected number of times"); + } + })); + + listenersLatch.await(); + assertTrue(listeners.stream().map(Tuple::v2).allMatch(count -> count.get() == numberOfSchedules)); + latch.await(); + assertFailedListenerLogMessage(mockLogger, numberOfListeners * numberOfSchedules); + verifyNoMoreInteractions(mockLogger); + } finally { + engine.stop(); + } + } + + private void assertFailedListenerLogMessage(Logger mockLogger, int times) { + final ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(ParameterizedMessage.class); + final ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(mockLogger, times(times)).warn(messageCaptor.capture(), throwableCaptor.capture()); + for (final ParameterizedMessage message : messageCaptor.getAllValues()) { + assertThat(message.getFormat(), equalTo("listener failed while handling triggered event [{}]")); + assertThat(message.getParameters(), arrayWithSize(1)); + assertThat(message.getParameters()[0], equalTo(getTestName())); + } + for (final Throwable throwable : throwableCaptor.getAllValues()) { + assertThat(throwable, instanceOf(RuntimeException.class)); + assertThat(throwable.getMessage(), equalTo(getTestName())); + } + } + +} diff --git a/x-pack/qa/evil-tests/build.gradle b/x-pack/qa/evil-tests/build.gradle new file mode 100644 index 000000000000..9b6055ffad7d --- /dev/null +++ b/x-pack/qa/evil-tests/build.gradle @@ -0,0 +1,9 @@ +apply plugin: 'elasticsearch.standalone-test' + +dependencies { + testCompile project(path: xpackModule('core'), configuration: 'shadow') +} + +test { + systemProperty 'tests.security.manager', 'false' +} diff --git a/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/core/scheduler/EvilSchedulerEngineTests.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/core/scheduler/EvilSchedulerEngineTests.java new file mode 100644 index 000000000000..2dfd314ffb06 --- /dev/null +++ b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/core/scheduler/EvilSchedulerEngineTests.java @@ -0,0 +1,84 @@ +/* + * 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.scheduler; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +import java.time.Clock; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +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.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class EvilSchedulerEngineTests extends ESTestCase { + + public void testOutOfMemoryErrorWhileTriggeredIsRethrownAndIsUncaught() throws InterruptedException { + final AtomicReference maybeFatal = new AtomicReference<>(); + final CountDownLatch uncaughtLatuch = new CountDownLatch(1); + final Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + try { + /* + * We want to test that the out of memory error thrown from the scheduler engine goes uncaught on another thread; this gives us + * confidence that an error thrown during a triggered event will lead to the node being torn down. + */ + final AtomicReference maybeThread = new AtomicReference<>(); + Thread.setDefaultUncaughtExceptionHandler((t, e) -> { + maybeFatal.set(e); + maybeThread.set(Thread.currentThread()); + uncaughtLatuch.countDown(); + }); + final Logger mockLogger = mock(Logger.class); + final SchedulerEngine engine = new SchedulerEngine(Settings.EMPTY, Clock.systemUTC(), mockLogger); + try { + final AtomicBoolean trigger = new AtomicBoolean(); + engine.register(event -> { + if (trigger.compareAndSet(false, true)) { + throw new OutOfMemoryError("640K ought to be enough for anybody"); + } else { + fail("listener invoked twice"); + } + }); + final CountDownLatch schedulerLatch = new CountDownLatch(1); + engine.add(new SchedulerEngine.Job( + getTestName(), + (startTime, now) -> { + if (schedulerLatch.getCount() == 1) { + schedulerLatch.countDown(); + return 0; + } else { + throw new AssertionError("nextScheduledTimeAfter invoked more than the expected number of times"); + } + })); + + uncaughtLatuch.await(); + assertTrue(trigger.get()); + assertNotNull(maybeFatal.get()); + assertThat(maybeFatal.get(), instanceOf(OutOfMemoryError.class)); + assertThat(maybeFatal.get(), hasToString(containsString("640K ought to be enough for anybody"))); + assertNotNull(maybeThread.get()); + assertThat(maybeThread.get(), not(equalTo(Thread.currentThread()))); // the error should be rethrown on another thread + schedulerLatch.await(); + verifyNoMoreInteractions(mockLogger); // we never logged anything + } finally { + engine.stop(); + } + } finally { + // restore the uncaught exception handler + Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler); + } + } + +} From 6d62d6755aeded636d7c52bda7e0331a9aaa9f1e Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 20 Aug 2018 22:58:07 -0400 Subject: [PATCH 071/283] Fix typo in comment in scheduler engine This commit fixes a minor typo in a big block comment in SchedulerEngine.java. --- .../org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java index 71abb61bdcab..3f99818f31af 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java @@ -194,7 +194,7 @@ public void run() { /* * Allowing the throwable to escape here will lead to be it being caught in FutureTask#run and set as the outcome of this * task; however, we never inspect the the outcomes of these scheduled tasks and so allowing the throwable to escape - * unhandled here could lead to use losing fatal errors. Instead, we rely on ExceptionsHelper#dieOnError to appropriately + * unhandled here could lead to us losing fatal errors. Instead, we rely on ExceptionsHelper#dieOnError to appropriately * dispatch any error to the uncaught exception handler. We should never see an exception here as these do not escape from * SchedulerEngine#notifyListeners. */ From 8fc213f237ccffc12870a7a0d845c91d8ecbab3b Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 21 Aug 2018 05:05:32 +0200 Subject: [PATCH 072/283] INGEST: Move all Pipeline State into IngestService (#32617) * INGEST: Move all Pipeline State into IngestService * Moves all pipeline state into the ingest service * Retains the existing pipeline store and pipeline execution service as inner classes to make the review easier, they should be flattened out in the next step * All tests for these classes were copied (and adapted) to the ingest service tests * This is a refactoring step to enable a clean implementation of a pipeline processor (See #32473) --- .../action/bulk/TransportBulkAction.java | 2 +- .../ingest/DeletePipelineTransportAction.java | 22 +- .../ingest/GetPipelineTransportAction.java | 12 +- .../ingest/PutPipelineTransportAction.java | 24 +- .../ingest/SimulatePipelineRequest.java | 11 +- .../SimulatePipelineTransportAction.java | 13 +- .../elasticsearch/ingest/IngestService.java | 511 ++++++++++- .../org/elasticsearch/ingest/Pipeline.java | 44 +- .../ingest/PipelineExecutionService.java | 215 ----- .../elasticsearch/ingest/PipelineStore.java | 275 ------ .../java/org/elasticsearch/node/Node.java | 2 +- .../org/elasticsearch/node/NodeService.java | 6 +- .../bulk/TransportBulkActionIngestTests.java | 18 +- .../SimulatePipelineRequestParsingTests.java | 19 +- ...gestProcessorNotInstalledOnAllNodesIT.java | 4 +- .../ingest/IngestServiceTests.java | 864 +++++++++++++++++- .../ingest/PipelineExecutionServiceTests.java | 471 ---------- .../ingest/PipelineFactoryTests.java | 27 +- .../ingest/PipelineStoreTests.java | 377 -------- 19 files changed, 1437 insertions(+), 1480 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/ingest/PipelineExecutionService.java delete mode 100644 server/src/main/java/org/elasticsearch/ingest/PipelineStore.java delete mode 100644 server/src/test/java/org/elasticsearch/ingest/PipelineExecutionServiceTests.java delete mode 100644 server/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java index e3e94e823394..ea4a5086d7b9 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java @@ -521,7 +521,7 @@ private long relativeTime() { void processBulkIndexIngestRequest(Task task, BulkRequest original, ActionListener listener) { long ingestStartTimeInNanos = System.nanoTime(); BulkRequestModifier bulkRequestModifier = new BulkRequestModifier(original); - ingestService.getPipelineExecutionService().executeBulkRequest(() -> bulkRequestModifier, (indexRequest, exception) -> { + ingestService.executeBulkRequest(() -> bulkRequestModifier, (indexRequest, exception) -> { logger.debug(() -> new ParameterizedMessage("failed to execute pipeline [{}] for document [{}/{}/{}]", indexRequest.getPipeline(), indexRequest.index(), indexRequest.type(), indexRequest.id()), exception); bulkRequestModifier.markCurrentItemAsFailed(exception); diff --git a/server/src/main/java/org/elasticsearch/action/ingest/DeletePipelineTransportAction.java b/server/src/main/java/org/elasticsearch/action/ingest/DeletePipelineTransportAction.java index d3cd052ecad1..6b4c74fe56cd 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/DeletePipelineTransportAction.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/DeletePipelineTransportAction.java @@ -27,26 +27,23 @@ import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; -import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.ingest.PipelineStore; -import org.elasticsearch.node.NodeService; +import org.elasticsearch.ingest.IngestService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; public class DeletePipelineTransportAction extends TransportMasterNodeAction { - private final PipelineStore pipelineStore; - private final ClusterService clusterService; + private final IngestService ingestService; @Inject - public DeletePipelineTransportAction(Settings settings, ThreadPool threadPool, ClusterService clusterService, + public DeletePipelineTransportAction(Settings settings, ThreadPool threadPool, IngestService ingestService, TransportService transportService, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver, NodeService nodeService) { - super(settings, DeletePipelineAction.NAME, transportService, clusterService, threadPool, actionFilters, indexNameExpressionResolver, DeletePipelineRequest::new); - this.clusterService = clusterService; - this.pipelineStore = nodeService.getIngestService().getPipelineStore(); + IndexNameExpressionResolver indexNameExpressionResolver) { + super(settings, DeletePipelineAction.NAME, transportService, ingestService.getClusterService(), + threadPool, actionFilters, indexNameExpressionResolver, DeletePipelineRequest::new); + this.ingestService = ingestService; } @Override @@ -60,8 +57,9 @@ protected AcknowledgedResponse newResponse() { } @Override - protected void masterOperation(DeletePipelineRequest request, ClusterState state, ActionListener listener) throws Exception { - pipelineStore.delete(clusterService, request, listener); + protected void masterOperation(DeletePipelineRequest request, ClusterState state, + ActionListener listener) throws Exception { + ingestService.delete(request, listener); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/ingest/GetPipelineTransportAction.java b/server/src/main/java/org/elasticsearch/action/ingest/GetPipelineTransportAction.java index 191ed87a42cd..540f46982a56 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/GetPipelineTransportAction.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/GetPipelineTransportAction.java @@ -29,21 +29,17 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.ingest.PipelineStore; -import org.elasticsearch.node.NodeService; +import org.elasticsearch.ingest.IngestService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; public class GetPipelineTransportAction extends TransportMasterNodeReadAction { - - private final PipelineStore pipelineStore; - + @Inject public GetPipelineTransportAction(Settings settings, ThreadPool threadPool, ClusterService clusterService, TransportService transportService, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver, NodeService nodeService) { + IndexNameExpressionResolver indexNameExpressionResolver) { super(settings, GetPipelineAction.NAME, transportService, clusterService, threadPool, actionFilters, GetPipelineRequest::new, indexNameExpressionResolver); - this.pipelineStore = nodeService.getIngestService().getPipelineStore(); } @Override @@ -58,7 +54,7 @@ protected GetPipelineResponse newResponse() { @Override protected void masterOperation(GetPipelineRequest request, ClusterState state, ActionListener listener) throws Exception { - listener.onResponse(new GetPipelineResponse(pipelineStore.getPipelines(state, request.getIds()))); + listener.onResponse(new GetPipelineResponse(IngestService.getPipelines(state, request.getIds()))); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineTransportAction.java b/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineTransportAction.java index abe8f49272c7..38e1f2fb54b5 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineTransportAction.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineTransportAction.java @@ -32,12 +32,10 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.ingest.PipelineStore; +import org.elasticsearch.ingest.IngestService; import org.elasticsearch.ingest.IngestInfo; -import org.elasticsearch.node.NodeService; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -46,19 +44,19 @@ public class PutPipelineTransportAction extends TransportMasterNodeAction { - private final PipelineStore pipelineStore; - private final ClusterService clusterService; + private final IngestService ingestService; private final NodeClient client; @Inject - public PutPipelineTransportAction(Settings settings, ThreadPool threadPool, ClusterService clusterService, - TransportService transportService, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver, NodeService nodeService, - NodeClient client) { - super(settings, PutPipelineAction.NAME, transportService, clusterService, threadPool, actionFilters, indexNameExpressionResolver, PutPipelineRequest::new); - this.clusterService = clusterService; + public PutPipelineTransportAction(Settings settings, ThreadPool threadPool, TransportService transportService, + ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, + IngestService ingestService, NodeClient client) { + super( + settings, PutPipelineAction.NAME, transportService, ingestService.getClusterService(), + threadPool, actionFilters, indexNameExpressionResolver, PutPipelineRequest::new + ); this.client = client; - this.pipelineStore = nodeService.getIngestService().getPipelineStore(); + this.ingestService = ingestService; } @Override @@ -84,7 +82,7 @@ public void onResponse(NodesInfoResponse nodeInfos) { for (NodeInfo nodeInfo : nodeInfos.getNodes()) { ingestInfos.put(nodeInfo.getNode(), nodeInfo.getIngest()); } - pipelineStore.put(clusterService, ingestInfos, request, listener); + ingestService.putPipeline(ingestInfos, request, listener); } catch (Exception e) { onFailure(e); } diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java index 9a7d6bb7feea..8405bb85b4b1 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java @@ -32,8 +32,8 @@ import org.elasticsearch.index.VersionType; import org.elasticsearch.ingest.ConfigurationUtils; import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.ingest.IngestService; import org.elasticsearch.ingest.Pipeline; -import org.elasticsearch.ingest.PipelineStore; import java.io.IOException; import java.util.ArrayList; @@ -164,14 +164,13 @@ public boolean isVerbose() { } } - private static final Pipeline.Factory PIPELINE_FACTORY = new Pipeline.Factory(); static final String SIMULATED_PIPELINE_ID = "_simulate_pipeline"; - static Parsed parseWithPipelineId(String pipelineId, Map config, boolean verbose, PipelineStore pipelineStore) { + static Parsed parseWithPipelineId(String pipelineId, Map config, boolean verbose, IngestService ingestService) { if (pipelineId == null) { throw new IllegalArgumentException("param [pipeline] is null"); } - Pipeline pipeline = pipelineStore.get(pipelineId); + Pipeline pipeline = ingestService.getPipeline(pipelineId); if (pipeline == null) { throw new IllegalArgumentException("pipeline [" + pipelineId + "] does not exist"); } @@ -179,9 +178,9 @@ static Parsed parseWithPipelineId(String pipelineId, Map config, return new Parsed(pipeline, ingestDocumentList, verbose); } - static Parsed parse(Map config, boolean verbose, PipelineStore pipelineStore) throws Exception { + static Parsed parse(Map config, boolean verbose, IngestService pipelineStore) throws Exception { Map pipelineConfig = ConfigurationUtils.readMap(null, null, config, Fields.PIPELINE); - Pipeline pipeline = PIPELINE_FACTORY.create(SIMULATED_PIPELINE_ID, pipelineConfig, pipelineStore.getProcessorFactories()); + Pipeline pipeline = Pipeline.create(SIMULATED_PIPELINE_ID, pipelineConfig, pipelineStore.getProcessorFactories()); List ingestDocumentList = parseDocs(config); return new Parsed(pipeline, ingestDocumentList, verbose); } diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java index 2e898c1895f9..ad8577d5244d 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineTransportAction.java @@ -26,8 +26,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.ingest.PipelineStore; -import org.elasticsearch.node.NodeService; +import org.elasticsearch.ingest.IngestService; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -36,15 +35,15 @@ public class SimulatePipelineTransportAction extends HandledTransportAction { - private final PipelineStore pipelineStore; + private final IngestService ingestService; private final SimulateExecutionService executionService; @Inject public SimulatePipelineTransportAction(Settings settings, ThreadPool threadPool, TransportService transportService, - ActionFilters actionFilters, NodeService nodeService) { + ActionFilters actionFilters, IngestService ingestService) { super(settings, SimulatePipelineAction.NAME, transportService, actionFilters, (Writeable.Reader) SimulatePipelineRequest::new); - this.pipelineStore = nodeService.getIngestService().getPipelineStore(); + this.ingestService = ingestService; this.executionService = new SimulateExecutionService(threadPool); } @@ -55,9 +54,9 @@ protected void doExecute(Task task, SimulatePipelineRequest request, ActionListe final SimulatePipelineRequest.Parsed simulateRequest; try { if (request.getId() != null) { - simulateRequest = SimulatePipelineRequest.parseWithPipelineId(request.getId(), source, request.isVerbose(), pipelineStore); + simulateRequest = SimulatePipelineRequest.parseWithPipelineId(request.getId(), source, request.isVerbose(), ingestService); } else { - simulateRequest = SimulatePipelineRequest.parse(source, request.isVerbose(), pipelineStore); + simulateRequest = SimulatePipelineRequest.parse(source, request.isVerbose(), ingestService); } } catch (Exception e) { listener.onFailure(e); diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 01bc402e43ba..4ca06f63991a 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -22,14 +22,42 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.concurrent.ScheduledFuture; -import java.util.function.BiFunction; - -import org.elasticsearch.common.settings.Settings; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.ingest.DeletePipelineRequest; +import org.elasticsearch.action.ingest.PutPipelineRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.cluster.AckedClusterStateUpdateTask; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateApplier; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.metrics.CounterMetric; +import org.elasticsearch.common.metrics.MeanMetric; +import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.env.Environment; +import org.elasticsearch.gateway.GatewayService; +import org.elasticsearch.index.VersionType; import org.elasticsearch.index.analysis.AnalysisRegistry; import org.elasticsearch.plugins.IngestPlugin; import org.elasticsearch.script.ScriptService; @@ -38,20 +66,35 @@ /** * Holder class for several ingest related services. */ -public class IngestService { +public class IngestService implements ClusterStateApplier { public static final String NOOP_PIPELINE_NAME = "_none"; + private final ClusterService clusterService; private final PipelineStore pipelineStore; private final PipelineExecutionService pipelineExecutionService; - public IngestService(Settings settings, ThreadPool threadPool, + public IngestService(ClusterService clusterService, ThreadPool threadPool, Environment env, ScriptService scriptService, AnalysisRegistry analysisRegistry, List ingestPlugins) { - BiFunction> scheduler = - (delay, command) -> threadPool.schedule(TimeValue.timeValueMillis(delay), ThreadPool.Names.GENERIC, command); - Processor.Parameters parameters = new Processor.Parameters(env, scriptService, analysisRegistry, - threadPool.getThreadContext(), threadPool::relativeTimeInMillis, scheduler); + this.clusterService = clusterService; + this.pipelineStore = new PipelineStore( + processorFactories( + ingestPlugins, + new Processor.Parameters( + env, scriptService, analysisRegistry, + threadPool.getThreadContext(), threadPool::relativeTimeInMillis, + (delay, command) -> threadPool.schedule( + TimeValue.timeValueMillis(delay), ThreadPool.Names.GENERIC, command + ) + ) + ) + ); + this.pipelineExecutionService = new PipelineExecutionService(pipelineStore, threadPool); + } + + private static Map processorFactories(List ingestPlugins, + Processor.Parameters parameters) { Map processorFactories = new HashMap<>(); for (IngestPlugin ingestPlugin : ingestPlugins) { Map newProcessors = ingestPlugin.getProcessors(parameters); @@ -61,24 +104,458 @@ public IngestService(Settings settings, ThreadPool threadPool, } } } - this.pipelineStore = new PipelineStore(settings, Collections.unmodifiableMap(processorFactories)); - this.pipelineExecutionService = new PipelineExecutionService(pipelineStore, threadPool); + return Collections.unmodifiableMap(processorFactories); + } + + public ClusterService getClusterService() { + return clusterService; + } + + /** + * Deletes the pipeline specified by id in the request. + */ + public void delete(DeletePipelineRequest request, + ActionListener listener) { + clusterService.submitStateUpdateTask("delete-pipeline-" + request.getId(), + new AckedClusterStateUpdateTask(request, listener) { + + @Override + protected AcknowledgedResponse newResponse(boolean acknowledged) { + return new AcknowledgedResponse(acknowledged); + } + + @Override + public ClusterState execute(ClusterState currentState) { + return innerDelete(request, currentState); + } + }); + } + + static ClusterState innerDelete(DeletePipelineRequest request, ClusterState currentState) { + IngestMetadata currentIngestMetadata = currentState.metaData().custom(IngestMetadata.TYPE); + if (currentIngestMetadata == null) { + return currentState; + } + Map pipelines = currentIngestMetadata.getPipelines(); + Set toRemove = new HashSet<>(); + for (String pipelineKey : pipelines.keySet()) { + if (Regex.simpleMatch(request.getId(), pipelineKey)) { + toRemove.add(pipelineKey); + } + } + if (toRemove.isEmpty() && Regex.isMatchAllPattern(request.getId()) == false) { + throw new ResourceNotFoundException("pipeline [{}] is missing", request.getId()); + } else if (toRemove.isEmpty()) { + return currentState; + } + final Map pipelinesCopy = new HashMap<>(pipelines); + for (String key : toRemove) { + pipelinesCopy.remove(key); + } + ClusterState.Builder newState = ClusterState.builder(currentState); + newState.metaData(MetaData.builder(currentState.getMetaData()) + .putCustom(IngestMetadata.TYPE, new IngestMetadata(pipelinesCopy)) + .build()); + return newState.build(); + } + + /** + * @return pipeline configuration specified by id. If multiple ids or wildcards are specified multiple pipelines + * may be returned + */ + // Returning PipelineConfiguration instead of Pipeline, because Pipeline and Processor interface don't + // know how to serialize themselves. + public static List getPipelines(ClusterState clusterState, String... ids) { + IngestMetadata ingestMetadata = clusterState.getMetaData().custom(IngestMetadata.TYPE); + return innerGetPipelines(ingestMetadata, ids); + } + + static List innerGetPipelines(IngestMetadata ingestMetadata, String... ids) { + if (ingestMetadata == null) { + return Collections.emptyList(); + } + + // if we didn't ask for _any_ ID, then we get them all (this is the same as if they ask for '*') + if (ids.length == 0) { + return new ArrayList<>(ingestMetadata.getPipelines().values()); + } + + List result = new ArrayList<>(ids.length); + for (String id : ids) { + if (Regex.isSimpleMatchPattern(id)) { + for (Map.Entry entry : ingestMetadata.getPipelines().entrySet()) { + if (Regex.simpleMatch(id, entry.getKey())) { + result.add(entry.getValue()); + } + } + } else { + PipelineConfiguration pipeline = ingestMetadata.getPipelines().get(id); + if (pipeline != null) { + result.add(pipeline); + } + } + } + return result; + } + + public void executeBulkRequest(Iterable> actionRequests, BiConsumer itemFailureHandler, + Consumer completionHandler) { + pipelineExecutionService.executeBulkRequest(actionRequests, itemFailureHandler, completionHandler); } - public PipelineStore getPipelineStore() { - return pipelineStore; + public IngestStats stats() { + return pipelineExecutionService.stats(); } - public PipelineExecutionService getPipelineExecutionService() { - return pipelineExecutionService; + /** + * Stores the specified pipeline definition in the request. + */ + public void putPipeline(Map ingestInfos, PutPipelineRequest request, + ActionListener listener) throws Exception { + pipelineStore.put(clusterService, ingestInfos, request, listener); + } + + /** + * Returns the pipeline by the specified id + */ + public Pipeline getPipeline(String id) { + return pipelineStore.get(id); + } + + public Map getProcessorFactories() { + return pipelineStore.getProcessorFactories(); } public IngestInfo info() { - Map processorFactories = pipelineStore.getProcessorFactories(); + Map processorFactories = getProcessorFactories(); List processorInfoList = new ArrayList<>(processorFactories.size()); for (Map.Entry entry : processorFactories.entrySet()) { processorInfoList.add(new ProcessorInfo(entry.getKey())); } return new IngestInfo(processorInfoList); } + + Map pipelines() { + return pipelineStore.pipelines; + } + + void validatePipeline(Map ingestInfos, PutPipelineRequest request) throws Exception { + pipelineStore.validatePipeline(ingestInfos, request); + } + + void updatePipelineStats(IngestMetadata ingestMetadata) { + pipelineExecutionService.updatePipelineStats(ingestMetadata); + } + + @Override + public void applyClusterState(final ClusterChangedEvent event) { + ClusterState state = event.state(); + pipelineStore.innerUpdatePipelines(event.previousState(), state); + IngestMetadata ingestMetadata = state.getMetaData().custom(IngestMetadata.TYPE); + if (ingestMetadata != null) { + pipelineExecutionService.updatePipelineStats(ingestMetadata); + } + } + + public static final class PipelineStore { + + private final Map processorFactories; + + // Ideally this should be in IngestMetadata class, but we don't have the processor factories around there. + // We know of all the processor factories when a node with all its plugin have been initialized. Also some + // processor factories rely on other node services. Custom metadata is statically registered when classes + // are loaded, so in the cluster state we just save the pipeline config and here we keep the actual pipelines around. + volatile Map pipelines = new HashMap<>(); + + private PipelineStore(Map processorFactories) { + this.processorFactories = processorFactories; + } + + void innerUpdatePipelines(ClusterState previousState, ClusterState state) { + if (state.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) { + return; + } + + IngestMetadata ingestMetadata = state.getMetaData().custom(IngestMetadata.TYPE); + IngestMetadata previousIngestMetadata = previousState.getMetaData().custom(IngestMetadata.TYPE); + if (Objects.equals(ingestMetadata, previousIngestMetadata)) { + return; + } + + Map pipelines = new HashMap<>(); + List exceptions = new ArrayList<>(); + for (PipelineConfiguration pipeline : ingestMetadata.getPipelines().values()) { + try { + pipelines.put(pipeline.getId(), Pipeline.create(pipeline.getId(), pipeline.getConfigAsMap(), processorFactories)); + } catch (ElasticsearchParseException e) { + pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), e)); + exceptions.add(e); + } catch (Exception e) { + ElasticsearchParseException parseException = new ElasticsearchParseException( + "Error updating pipeline with id [" + pipeline.getId() + "]", e); + pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), parseException)); + exceptions.add(parseException); + } + } + this.pipelines = Collections.unmodifiableMap(pipelines); + ExceptionsHelper.rethrowAndSuppress(exceptions); + } + + private static Pipeline substitutePipeline(String id, ElasticsearchParseException e) { + String tag = e.getHeaderKeys().contains("processor_tag") ? e.getHeader("processor_tag").get(0) : null; + String type = e.getHeaderKeys().contains("processor_type") ? e.getHeader("processor_type").get(0) : "unknown"; + String errorMessage = "pipeline with id [" + id + "] could not be loaded, caused by [" + e.getDetailedMessage() + "]"; + Processor failureProcessor = new AbstractProcessor(tag) { + @Override + public void execute(IngestDocument ingestDocument) { + throw new IllegalStateException(errorMessage); + } + + @Override + public String getType() { + return type; + } + }; + String description = "this is a place holder pipeline, because pipeline with id [" + id + "] could not be loaded"; + return new Pipeline(id, description, null, new CompoundProcessor(failureProcessor)); + } + + /** + * Stores the specified pipeline definition in the request. + */ + public void put(ClusterService clusterService, Map ingestInfos, PutPipelineRequest request, + ActionListener listener) throws Exception { + // validates the pipeline and processor configuration before submitting a cluster update task: + validatePipeline(ingestInfos, request); + clusterService.submitStateUpdateTask("put-pipeline-" + request.getId(), + new AckedClusterStateUpdateTask(request, listener) { + + @Override + protected AcknowledgedResponse newResponse(boolean acknowledged) { + return new AcknowledgedResponse(acknowledged); + } + + @Override + public ClusterState execute(ClusterState currentState) { + return innerPut(request, currentState); + } + }); + } + + void validatePipeline(Map ingestInfos, PutPipelineRequest request) throws Exception { + if (ingestInfos.isEmpty()) { + throw new IllegalStateException("Ingest info is empty"); + } + + Map pipelineConfig = XContentHelper.convertToMap(request.getSource(), false, request.getXContentType()).v2(); + Pipeline pipeline = Pipeline.create(request.getId(), pipelineConfig, processorFactories); + List exceptions = new ArrayList<>(); + for (Processor processor : pipeline.flattenAllProcessors()) { + for (Map.Entry entry : ingestInfos.entrySet()) { + if (entry.getValue().containsProcessor(processor.getType()) == false) { + String message = "Processor type [" + processor.getType() + "] is not installed on node [" + entry.getKey() + "]"; + exceptions.add( + ConfigurationUtils.newConfigurationException(processor.getType(), processor.getTag(), null, message) + ); + } + } + } + ExceptionsHelper.rethrowAndSuppress(exceptions); + } + + static ClusterState innerPut(PutPipelineRequest request, ClusterState currentState) { + IngestMetadata currentIngestMetadata = currentState.metaData().custom(IngestMetadata.TYPE); + Map pipelines; + if (currentIngestMetadata != null) { + pipelines = new HashMap<>(currentIngestMetadata.getPipelines()); + } else { + pipelines = new HashMap<>(); + } + + pipelines.put(request.getId(), new PipelineConfiguration(request.getId(), request.getSource(), request.getXContentType())); + ClusterState.Builder newState = ClusterState.builder(currentState); + newState.metaData(MetaData.builder(currentState.getMetaData()) + .putCustom(IngestMetadata.TYPE, new IngestMetadata(pipelines)) + .build()); + return newState.build(); + } + + /** + * Returns the pipeline by the specified id + */ + public Pipeline get(String id) { + return pipelines.get(id); + } + + public Map getProcessorFactories() { + return processorFactories; + } + } + + private static final class PipelineExecutionService { + + private final PipelineStore store; + private final ThreadPool threadPool; + + private final StatsHolder totalStats = new StatsHolder(); + private volatile Map statsHolderPerPipeline = Collections.emptyMap(); + + PipelineExecutionService(PipelineStore store, ThreadPool threadPool) { + this.store = store; + this.threadPool = threadPool; + } + + void executeBulkRequest(Iterable> actionRequests, + BiConsumer itemFailureHandler, + Consumer completionHandler) { + threadPool.executor(ThreadPool.Names.WRITE).execute(new AbstractRunnable() { + + @Override + public void onFailure(Exception e) { + completionHandler.accept(e); + } + + @Override + protected void doRun() { + for (DocWriteRequest actionRequest : actionRequests) { + IndexRequest indexRequest = null; + if (actionRequest instanceof IndexRequest) { + indexRequest = (IndexRequest) actionRequest; + } else if (actionRequest instanceof UpdateRequest) { + UpdateRequest updateRequest = (UpdateRequest) actionRequest; + indexRequest = updateRequest.docAsUpsert() ? updateRequest.doc() : updateRequest.upsertRequest(); + } + if (indexRequest == null) { + continue; + } + String pipeline = indexRequest.getPipeline(); + if (NOOP_PIPELINE_NAME.equals(pipeline) == false) { + try { + innerExecute(indexRequest, getPipeline(indexRequest.getPipeline())); + //this shouldn't be needed here but we do it for consistency with index api + // which requires it to prevent double execution + indexRequest.setPipeline(NOOP_PIPELINE_NAME); + } catch (Exception e) { + itemFailureHandler.accept(indexRequest, e); + } + } + } + completionHandler.accept(null); + } + }); + } + + IngestStats stats() { + Map statsHolderPerPipeline = this.statsHolderPerPipeline; + + Map statsPerPipeline = new HashMap<>(statsHolderPerPipeline.size()); + for (Map.Entry entry : statsHolderPerPipeline.entrySet()) { + statsPerPipeline.put(entry.getKey(), entry.getValue().createStats()); + } + + return new IngestStats(totalStats.createStats(), statsPerPipeline); + } + + void updatePipelineStats(IngestMetadata ingestMetadata) { + boolean changed = false; + Map newStatsPerPipeline = new HashMap<>(statsHolderPerPipeline); + Iterator iterator = newStatsPerPipeline.keySet().iterator(); + while (iterator.hasNext()) { + String pipeline = iterator.next(); + if (ingestMetadata.getPipelines().containsKey(pipeline) == false) { + iterator.remove(); + changed = true; + } + } + for (String pipeline : ingestMetadata.getPipelines().keySet()) { + if (newStatsPerPipeline.containsKey(pipeline) == false) { + newStatsPerPipeline.put(pipeline, new StatsHolder()); + changed = true; + } + } + + if (changed) { + statsHolderPerPipeline = Collections.unmodifiableMap(newStatsPerPipeline); + } + } + + private void innerExecute(IndexRequest indexRequest, Pipeline pipeline) throws Exception { + if (pipeline.getProcessors().isEmpty()) { + return; + } + + long startTimeInNanos = System.nanoTime(); + // the pipeline specific stat holder may not exist and that is fine: + // (e.g. the pipeline may have been removed while we're ingesting a document + Optional pipelineStats = Optional.ofNullable(statsHolderPerPipeline.get(pipeline.getId())); + try { + totalStats.preIngest(); + pipelineStats.ifPresent(StatsHolder::preIngest); + String index = indexRequest.index(); + String type = indexRequest.type(); + String id = indexRequest.id(); + String routing = indexRequest.routing(); + Long version = indexRequest.version(); + VersionType versionType = indexRequest.versionType(); + Map sourceAsMap = indexRequest.sourceAsMap(); + IngestDocument ingestDocument = new IngestDocument(index, type, id, routing, version, versionType, sourceAsMap); + pipeline.execute(ingestDocument); + + Map metadataMap = ingestDocument.extractMetadata(); + //it's fine to set all metadata fields all the time, as ingest document holds their starting values + //before ingestion, which might also get modified during ingestion. + indexRequest.index((String) metadataMap.get(IngestDocument.MetaData.INDEX)); + indexRequest.type((String) metadataMap.get(IngestDocument.MetaData.TYPE)); + indexRequest.id((String) metadataMap.get(IngestDocument.MetaData.ID)); + indexRequest.routing((String) metadataMap.get(IngestDocument.MetaData.ROUTING)); + indexRequest.version(((Number) metadataMap.get(IngestDocument.MetaData.VERSION)).longValue()); + if (metadataMap.get(IngestDocument.MetaData.VERSION_TYPE) != null) { + indexRequest.versionType(VersionType.fromString((String) metadataMap.get(IngestDocument.MetaData.VERSION_TYPE))); + } + indexRequest.source(ingestDocument.getSourceAndMetadata()); + } catch (Exception e) { + totalStats.ingestFailed(); + pipelineStats.ifPresent(StatsHolder::ingestFailed); + throw e; + } finally { + long ingestTimeInMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeInNanos); + totalStats.postIngest(ingestTimeInMillis); + pipelineStats.ifPresent(statsHolder -> statsHolder.postIngest(ingestTimeInMillis)); + } + } + + private Pipeline getPipeline(String pipelineId) { + Pipeline pipeline = store.get(pipelineId); + if (pipeline == null) { + throw new IllegalArgumentException("pipeline with id [" + pipelineId + "] does not exist"); + } + return pipeline; + } + + private static class StatsHolder { + + private final MeanMetric ingestMetric = new MeanMetric(); + private final CounterMetric ingestCurrent = new CounterMetric(); + private final CounterMetric ingestFailed = new CounterMetric(); + + void preIngest() { + ingestCurrent.inc(); + } + + void postIngest(long ingestTimeInMillis) { + ingestCurrent.dec(); + ingestMetric.inc(ingestTimeInMillis); + } + + void ingestFailed() { + ingestFailed.inc(); + } + + IngestStats.Stats createStats() { + return new IngestStats.Stats(ingestMetric.count(), ingestMetric.sum(), ingestCurrent.count(), ingestFailed.count()); + } + + } + + } } diff --git a/server/src/main/java/org/elasticsearch/ingest/Pipeline.java b/server/src/main/java/org/elasticsearch/ingest/Pipeline.java index 1b0553a54902..37dd3f52cb7d 100644 --- a/server/src/main/java/org/elasticsearch/ingest/Pipeline.java +++ b/server/src/main/java/org/elasticsearch/ingest/Pipeline.java @@ -51,6 +51,27 @@ public Pipeline(String id, @Nullable String description, @Nullable Integer versi this.version = version; } + public static Pipeline create(String id, Map config, + Map processorFactories) throws Exception { + String description = ConfigurationUtils.readOptionalStringProperty(null, null, config, DESCRIPTION_KEY); + Integer version = ConfigurationUtils.readIntProperty(null, null, config, VERSION_KEY, null); + List> processorConfigs = ConfigurationUtils.readList(null, null, config, PROCESSORS_KEY); + List processors = ConfigurationUtils.readProcessorConfigs(processorConfigs, processorFactories); + List> onFailureProcessorConfigs = + ConfigurationUtils.readOptionalList(null, null, config, ON_FAILURE_KEY); + List onFailureProcessors = ConfigurationUtils.readProcessorConfigs(onFailureProcessorConfigs, processorFactories); + if (config.isEmpty() == false) { + throw new ElasticsearchParseException("pipeline [" + id + + "] doesn't support one or more provided configuration parameters " + Arrays.toString(config.keySet().toArray())); + } + if (onFailureProcessorConfigs != null && onFailureProcessors.isEmpty()) { + throw new ElasticsearchParseException("pipeline [" + id + "] cannot have an empty on_failure option defined"); + } + CompoundProcessor compoundProcessor = new CompoundProcessor(false, Collections.unmodifiableList(processors), + Collections.unmodifiableList(onFailureProcessors)); + return new Pipeline(id, description, version, compoundProcessor); + } + /** * Modifies the data of a document to be indexed based on the processor this pipeline holds */ @@ -113,27 +134,4 @@ public List flattenAllProcessors() { return compoundProcessor.flattenProcessors(); } - public static final class Factory { - - public Pipeline create(String id, Map config, Map processorFactories) throws Exception { - String description = ConfigurationUtils.readOptionalStringProperty(null, null, config, DESCRIPTION_KEY); - Integer version = ConfigurationUtils.readIntProperty(null, null, config, VERSION_KEY, null); - List> processorConfigs = ConfigurationUtils.readList(null, null, config, PROCESSORS_KEY); - List processors = ConfigurationUtils.readProcessorConfigs(processorConfigs, processorFactories); - List> onFailureProcessorConfigs = - ConfigurationUtils.readOptionalList(null, null, config, ON_FAILURE_KEY); - List onFailureProcessors = ConfigurationUtils.readProcessorConfigs(onFailureProcessorConfigs, processorFactories); - if (config.isEmpty() == false) { - throw new ElasticsearchParseException("pipeline [" + id + - "] doesn't support one or more provided configuration parameters " + Arrays.toString(config.keySet().toArray())); - } - if (onFailureProcessorConfigs != null && onFailureProcessors.isEmpty()) { - throw new ElasticsearchParseException("pipeline [" + id + "] cannot have an empty on_failure option defined"); - } - CompoundProcessor compoundProcessor = new CompoundProcessor(false, Collections.unmodifiableList(processors), - Collections.unmodifiableList(onFailureProcessors)); - return new Pipeline(id, description, version, compoundProcessor); - } - - } } diff --git a/server/src/main/java/org/elasticsearch/ingest/PipelineExecutionService.java b/server/src/main/java/org/elasticsearch/ingest/PipelineExecutionService.java deleted file mode 100644 index 56d44ee88812..000000000000 --- a/server/src/main/java/org/elasticsearch/ingest/PipelineExecutionService.java +++ /dev/null @@ -1,215 +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.ingest; - -import org.elasticsearch.action.DocWriteRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.cluster.ClusterChangedEvent; -import org.elasticsearch.cluster.ClusterStateApplier; -import org.elasticsearch.common.metrics.CounterMetric; -import org.elasticsearch.common.metrics.MeanMetric; -import org.elasticsearch.common.util.concurrent.AbstractRunnable; -import org.elasticsearch.index.VersionType; -import org.elasticsearch.threadpool.ThreadPool; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -public class PipelineExecutionService implements ClusterStateApplier { - - private final PipelineStore store; - private final ThreadPool threadPool; - - private final StatsHolder totalStats = new StatsHolder(); - private volatile Map statsHolderPerPipeline = Collections.emptyMap(); - - public PipelineExecutionService(PipelineStore store, ThreadPool threadPool) { - this.store = store; - this.threadPool = threadPool; - } - - public void executeBulkRequest(Iterable> actionRequests, - BiConsumer itemFailureHandler, - Consumer completionHandler) { - threadPool.executor(ThreadPool.Names.WRITE).execute(new AbstractRunnable() { - - @Override - public void onFailure(Exception e) { - completionHandler.accept(e); - } - - @Override - protected void doRun() throws Exception { - for (DocWriteRequest actionRequest : actionRequests) { - IndexRequest indexRequest = null; - if (actionRequest instanceof IndexRequest) { - indexRequest = (IndexRequest) actionRequest; - } else if (actionRequest instanceof UpdateRequest) { - UpdateRequest updateRequest = (UpdateRequest) actionRequest; - indexRequest = updateRequest.docAsUpsert() ? updateRequest.doc() : updateRequest.upsertRequest(); - } - if (indexRequest == null) { - continue; - } - String pipeline = indexRequest.getPipeline(); - if (IngestService.NOOP_PIPELINE_NAME.equals(pipeline) == false) { - try { - innerExecute(indexRequest, getPipeline(indexRequest.getPipeline())); - //this shouldn't be needed here but we do it for consistency with index api - // which requires it to prevent double execution - indexRequest.setPipeline(IngestService.NOOP_PIPELINE_NAME); - } catch (Exception e) { - itemFailureHandler.accept(indexRequest, e); - } - } - } - completionHandler.accept(null); - } - }); - } - - public IngestStats stats() { - Map statsHolderPerPipeline = this.statsHolderPerPipeline; - - Map statsPerPipeline = new HashMap<>(statsHolderPerPipeline.size()); - for (Map.Entry entry : statsHolderPerPipeline.entrySet()) { - statsPerPipeline.put(entry.getKey(), entry.getValue().createStats()); - } - - return new IngestStats(totalStats.createStats(), statsPerPipeline); - } - - @Override - public void applyClusterState(ClusterChangedEvent event) { - IngestMetadata ingestMetadata = event.state().getMetaData().custom(IngestMetadata.TYPE); - if (ingestMetadata != null) { - updatePipelineStats(ingestMetadata); - } - } - - void updatePipelineStats(IngestMetadata ingestMetadata) { - boolean changed = false; - Map newStatsPerPipeline = new HashMap<>(statsHolderPerPipeline); - Iterator iterator = newStatsPerPipeline.keySet().iterator(); - while (iterator.hasNext()) { - String pipeline = iterator.next(); - if (ingestMetadata.getPipelines().containsKey(pipeline) == false) { - iterator.remove(); - changed = true; - } - } - for (String pipeline : ingestMetadata.getPipelines().keySet()) { - if (newStatsPerPipeline.containsKey(pipeline) == false) { - newStatsPerPipeline.put(pipeline, new StatsHolder()); - changed = true; - } - } - - if (changed) { - statsHolderPerPipeline = Collections.unmodifiableMap(newStatsPerPipeline); - } - } - - private void innerExecute(IndexRequest indexRequest, Pipeline pipeline) throws Exception { - if (pipeline.getProcessors().isEmpty()) { - return; - } - - long startTimeInNanos = System.nanoTime(); - // the pipeline specific stat holder may not exist and that is fine: - // (e.g. the pipeline may have been removed while we're ingesting a document - Optional pipelineStats = Optional.ofNullable(statsHolderPerPipeline.get(pipeline.getId())); - try { - totalStats.preIngest(); - pipelineStats.ifPresent(StatsHolder::preIngest); - String index = indexRequest.index(); - String type = indexRequest.type(); - String id = indexRequest.id(); - String routing = indexRequest.routing(); - Long version = indexRequest.version(); - VersionType versionType = indexRequest.versionType(); - Map sourceAsMap = indexRequest.sourceAsMap(); - IngestDocument ingestDocument = new IngestDocument(index, type, id, routing, version, versionType, sourceAsMap); - pipeline.execute(ingestDocument); - - Map metadataMap = ingestDocument.extractMetadata(); - //it's fine to set all metadata fields all the time, as ingest document holds their starting values - //before ingestion, which might also get modified during ingestion. - indexRequest.index((String) metadataMap.get(IngestDocument.MetaData.INDEX)); - indexRequest.type((String) metadataMap.get(IngestDocument.MetaData.TYPE)); - indexRequest.id((String) metadataMap.get(IngestDocument.MetaData.ID)); - indexRequest.routing((String) metadataMap.get(IngestDocument.MetaData.ROUTING)); - indexRequest.version(((Number) metadataMap.get(IngestDocument.MetaData.VERSION)).longValue()); - if (metadataMap.get(IngestDocument.MetaData.VERSION_TYPE) != null) { - indexRequest.versionType(VersionType.fromString((String) metadataMap.get(IngestDocument.MetaData.VERSION_TYPE))); - } - indexRequest.source(ingestDocument.getSourceAndMetadata()); - } catch (Exception e) { - totalStats.ingestFailed(); - pipelineStats.ifPresent(StatsHolder::ingestFailed); - throw e; - } finally { - long ingestTimeInMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeInNanos); - totalStats.postIngest(ingestTimeInMillis); - pipelineStats.ifPresent(statsHolder -> statsHolder.postIngest(ingestTimeInMillis)); - } - } - - private Pipeline getPipeline(String pipelineId) { - Pipeline pipeline = store.get(pipelineId); - if (pipeline == null) { - throw new IllegalArgumentException("pipeline with id [" + pipelineId + "] does not exist"); - } - return pipeline; - } - - static class StatsHolder { - - private final MeanMetric ingestMetric = new MeanMetric(); - private final CounterMetric ingestCurrent = new CounterMetric(); - private final CounterMetric ingestFailed = new CounterMetric(); - - void preIngest() { - ingestCurrent.inc(); - } - - void postIngest(long ingestTimeInMillis) { - ingestCurrent.dec(); - ingestMetric.inc(ingestTimeInMillis); - } - - void ingestFailed() { - ingestFailed.inc(); - } - - IngestStats.Stats createStats() { - return new IngestStats.Stats(ingestMetric.count(), ingestMetric.sum(), ingestCurrent.count(), ingestFailed.count()); - } - - } - -} diff --git a/server/src/main/java/org/elasticsearch/ingest/PipelineStore.java b/server/src/main/java/org/elasticsearch/ingest/PipelineStore.java deleted file mode 100644 index 9fceaf1a9a57..000000000000 --- a/server/src/main/java/org/elasticsearch/ingest/PipelineStore.java +++ /dev/null @@ -1,275 +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.ingest; - -import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.ExceptionsHelper; -import org.elasticsearch.ResourceNotFoundException; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ingest.DeletePipelineRequest; -import org.elasticsearch.action.ingest.PutPipelineRequest; -import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.cluster.AckedClusterStateUpdateTask; -import org.elasticsearch.cluster.ClusterChangedEvent; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.ClusterStateApplier; -import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.component.AbstractComponent; -import org.elasticsearch.common.regex.Regex; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.gateway.GatewayService; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -public class PipelineStore extends AbstractComponent implements ClusterStateApplier { - - private final Pipeline.Factory factory = new Pipeline.Factory(); - private final Map processorFactories; - - // Ideally this should be in IngestMetadata class, but we don't have the processor factories around there. - // We know of all the processor factories when a node with all its plugin have been initialized. Also some - // processor factories rely on other node services. Custom metadata is statically registered when classes - // are loaded, so in the cluster state we just save the pipeline config and here we keep the actual pipelines around. - volatile Map pipelines = new HashMap<>(); - - public PipelineStore(Settings settings, Map processorFactories) { - super(settings); - this.processorFactories = processorFactories; - } - - @Override - public void applyClusterState(ClusterChangedEvent event) { - innerUpdatePipelines(event.previousState(), event.state()); - } - - void innerUpdatePipelines(ClusterState previousState, ClusterState state) { - if (state.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) { - return; - } - - IngestMetadata ingestMetadata = state.getMetaData().custom(IngestMetadata.TYPE); - IngestMetadata previousIngestMetadata = previousState.getMetaData().custom(IngestMetadata.TYPE); - if (Objects.equals(ingestMetadata, previousIngestMetadata)) { - return; - } - - Map pipelines = new HashMap<>(); - List exceptions = new ArrayList<>(); - for (PipelineConfiguration pipeline : ingestMetadata.getPipelines().values()) { - try { - pipelines.put(pipeline.getId(), factory.create(pipeline.getId(), pipeline.getConfigAsMap(), processorFactories)); - } catch (ElasticsearchParseException e) { - pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), e)); - exceptions.add(e); - } catch (Exception e) { - ElasticsearchParseException parseException = new ElasticsearchParseException( - "Error updating pipeline with id [" + pipeline.getId() + "]", e); - pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), parseException)); - exceptions.add(parseException); - } - } - this.pipelines = Collections.unmodifiableMap(pipelines); - ExceptionsHelper.rethrowAndSuppress(exceptions); - } - - private Pipeline substitutePipeline(String id, ElasticsearchParseException e) { - String tag = e.getHeaderKeys().contains("processor_tag") ? e.getHeader("processor_tag").get(0) : null; - String type = e.getHeaderKeys().contains("processor_type") ? e.getHeader("processor_type").get(0) : "unknown"; - String errorMessage = "pipeline with id [" + id + "] could not be loaded, caused by [" + e.getDetailedMessage() + "]"; - Processor failureProcessor = new AbstractProcessor(tag) { - @Override - public void execute(IngestDocument ingestDocument) { - throw new IllegalStateException(errorMessage); - } - - @Override - public String getType() { - return type; - } - }; - String description = "this is a place holder pipeline, because pipeline with id [" + id + "] could not be loaded"; - return new Pipeline(id, description, null, new CompoundProcessor(failureProcessor)); - } - - /** - * Deletes the pipeline specified by id in the request. - */ - public void delete(ClusterService clusterService, DeletePipelineRequest request, ActionListener listener) { - clusterService.submitStateUpdateTask("delete-pipeline-" + request.getId(), - new AckedClusterStateUpdateTask(request, listener) { - - @Override - protected AcknowledgedResponse newResponse(boolean acknowledged) { - return new AcknowledgedResponse(acknowledged); - } - - @Override - public ClusterState execute(ClusterState currentState) throws Exception { - return innerDelete(request, currentState); - } - }); - } - - ClusterState innerDelete(DeletePipelineRequest request, ClusterState currentState) { - IngestMetadata currentIngestMetadata = currentState.metaData().custom(IngestMetadata.TYPE); - if (currentIngestMetadata == null) { - return currentState; - } - Map pipelines = currentIngestMetadata.getPipelines(); - Set toRemove = new HashSet<>(); - for (String pipelineKey : pipelines.keySet()) { - if (Regex.simpleMatch(request.getId(), pipelineKey)) { - toRemove.add(pipelineKey); - } - } - if (toRemove.isEmpty() && Regex.isMatchAllPattern(request.getId()) == false) { - throw new ResourceNotFoundException("pipeline [{}] is missing", request.getId()); - } else if (toRemove.isEmpty()) { - return currentState; - } - final Map pipelinesCopy = new HashMap<>(pipelines); - for (String key : toRemove) { - pipelinesCopy.remove(key); - } - ClusterState.Builder newState = ClusterState.builder(currentState); - newState.metaData(MetaData.builder(currentState.getMetaData()) - .putCustom(IngestMetadata.TYPE, new IngestMetadata(pipelinesCopy)) - .build()); - return newState.build(); - } - - /** - * Stores the specified pipeline definition in the request. - */ - public void put(ClusterService clusterService, Map ingestInfos, PutPipelineRequest request, - ActionListener listener) throws Exception { - // validates the pipeline and processor configuration before submitting a cluster update task: - validatePipeline(ingestInfos, request); - clusterService.submitStateUpdateTask("put-pipeline-" + request.getId(), - new AckedClusterStateUpdateTask(request, listener) { - - @Override - protected AcknowledgedResponse newResponse(boolean acknowledged) { - return new AcknowledgedResponse(acknowledged); - } - - @Override - public ClusterState execute(ClusterState currentState) throws Exception { - return innerPut(request, currentState); - } - }); - } - - void validatePipeline(Map ingestInfos, PutPipelineRequest request) throws Exception { - if (ingestInfos.isEmpty()) { - throw new IllegalStateException("Ingest info is empty"); - } - - Map pipelineConfig = XContentHelper.convertToMap(request.getSource(), false, request.getXContentType()).v2(); - Pipeline pipeline = factory.create(request.getId(), pipelineConfig, processorFactories); - List exceptions = new ArrayList<>(); - for (Processor processor : pipeline.flattenAllProcessors()) { - for (Map.Entry entry : ingestInfos.entrySet()) { - if (entry.getValue().containsProcessor(processor.getType()) == false) { - String message = "Processor type [" + processor.getType() + "] is not installed on node [" + entry.getKey() + "]"; - exceptions.add(ConfigurationUtils.newConfigurationException(processor.getType(), processor.getTag(), null, message)); - } - } - } - ExceptionsHelper.rethrowAndSuppress(exceptions); - } - - ClusterState innerPut(PutPipelineRequest request, ClusterState currentState) { - IngestMetadata currentIngestMetadata = currentState.metaData().custom(IngestMetadata.TYPE); - Map pipelines; - if (currentIngestMetadata != null) { - pipelines = new HashMap<>(currentIngestMetadata.getPipelines()); - } else { - pipelines = new HashMap<>(); - } - - pipelines.put(request.getId(), new PipelineConfiguration(request.getId(), request.getSource(), request.getXContentType())); - ClusterState.Builder newState = ClusterState.builder(currentState); - newState.metaData(MetaData.builder(currentState.getMetaData()) - .putCustom(IngestMetadata.TYPE, new IngestMetadata(pipelines)) - .build()); - return newState.build(); - } - - /** - * Returns the pipeline by the specified id - */ - public Pipeline get(String id) { - return pipelines.get(id); - } - - public Map getProcessorFactories() { - return processorFactories; - } - - /** - * @return pipeline configuration specified by id. If multiple ids or wildcards are specified multiple pipelines - * may be returned - */ - // Returning PipelineConfiguration instead of Pipeline, because Pipeline and Processor interface don't - // know how to serialize themselves. - public List getPipelines(ClusterState clusterState, String... ids) { - IngestMetadata ingestMetadata = clusterState.getMetaData().custom(IngestMetadata.TYPE); - return innerGetPipelines(ingestMetadata, ids); - } - - List innerGetPipelines(IngestMetadata ingestMetadata, String... ids) { - if (ingestMetadata == null) { - return Collections.emptyList(); - } - - // if we didn't ask for _any_ ID, then we get them all (this is the same as if they ask for '*') - if (ids.length == 0) { - return new ArrayList<>(ingestMetadata.getPipelines().values()); - } - - List result = new ArrayList<>(ids.length); - for (String id : ids) { - if (Regex.isSimpleMatchPattern(id)) { - for (Map.Entry entry : ingestMetadata.getPipelines().entrySet()) { - if (Regex.simpleMatch(id, entry.getKey())) { - result.add(entry.getValue()); - } - } - } else { - PipelineConfiguration pipeline = ingestMetadata.getPipelines().get(id); - if (pipeline != null) { - result.add(pipeline); - } - } - } - return result; - } -} diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index c65488bd08eb..9dfd8d2a382a 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -352,7 +352,7 @@ protected Node(final Environment environment, Collection final ClusterService clusterService = new ClusterService(settings, settingsModule.getClusterSettings(), threadPool); clusterService.addStateApplier(scriptModule.getScriptService()); resourcesToClose.add(clusterService); - final IngestService ingestService = new IngestService(settings, threadPool, this.environment, + final IngestService ingestService = new IngestService(clusterService, threadPool, this.environment, scriptModule.getScriptService(), analysisModule.getAnalysisRegistry(), pluginsService.filterPlugins(IngestPlugin.class)); final DiskThresholdMonitor listener = new DiskThresholdMonitor(settings, clusterService::state, clusterService.getClusterSettings(), client); diff --git a/server/src/main/java/org/elasticsearch/node/NodeService.java b/server/src/main/java/org/elasticsearch/node/NodeService.java index 0e19b5a65022..207886c5cf26 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeService.java +++ b/server/src/main/java/org/elasticsearch/node/NodeService.java @@ -37,7 +37,6 @@ import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.ingest.IngestService; import org.elasticsearch.monitor.MonitorService; -import org.elasticsearch.node.ResponseCollectorService; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.script.ScriptService; import org.elasticsearch.threadpool.ThreadPool; @@ -83,8 +82,7 @@ public class NodeService extends AbstractComponent implements Closeable { this.scriptService = scriptService; this.responseCollectorService = responseCollectorService; this.searchTransportService = searchTransportService; - clusterService.addStateApplier(ingestService.getPipelineStore()); - clusterService.addStateApplier(ingestService.getPipelineExecutionService()); + clusterService.addStateApplier(ingestService); } public NodeInfo info(boolean settings, boolean os, boolean process, boolean jvm, boolean threadPool, @@ -120,7 +118,7 @@ public NodeStats stats(CommonStatsFlags indices, boolean os, boolean process, bo circuitBreaker ? circuitBreakerService.stats() : null, script ? scriptService.stats() : null, discoveryStats ? discovery.stats() : null, - ingest ? ingestService.getPipelineExecutionService().stats() : null, + ingest ? ingestService.stats() : null, adaptiveSelection ? responseCollectorService.getAdaptiveStats(searchTransportService.getPendingSearchRequests()) : null ); } diff --git a/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java b/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java index 4c0dacc8a6e7..8b68d2b6bb9b 100644 --- a/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java +++ b/server/src/test/java/org/elasticsearch/action/bulk/TransportBulkActionIngestTests.java @@ -45,7 +45,6 @@ import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.ingest.IngestService; -import org.elasticsearch.ingest.PipelineExecutionService; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; @@ -90,9 +89,6 @@ public class TransportBulkActionIngestTests extends ESTestCase { ClusterService clusterService; IngestService ingestService; - /** The ingest execution service we can capture calls to */ - PipelineExecutionService executionService; - /** Arguments to callbacks we want to capture, but which require generics, so we must use @Captor */ @Captor ArgumentCaptor> failureHandler; @@ -207,8 +203,6 @@ public void setupAction() { }).when(clusterService).addStateApplier(any(ClusterStateApplier.class)); // setup the mocked ingest service for capturing calls ingestService = mock(IngestService.class); - executionService = mock(PipelineExecutionService.class); - when(ingestService.getPipelineExecutionService()).thenReturn(executionService); action = new TestTransportBulkAction(); singleItemBulkWriteAction = new TestSingleItemBulkWriteAction(action); reset(transportService); // call on construction of action @@ -265,7 +259,7 @@ public void testIngestLocal() throws Exception { assertFalse(action.isExecuted); // haven't executed yet assertFalse(responseCalled.get()); assertFalse(failureCalled.get()); - verify(executionService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); + verify(ingestService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); completionHandler.getValue().accept(exception); assertTrue(failureCalled.get()); @@ -299,7 +293,7 @@ public void testSingleItemBulkActionIngestLocal() throws Exception { assertFalse(action.isExecuted); // haven't executed yet assertFalse(responseCalled.get()); assertFalse(failureCalled.get()); - verify(executionService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); + verify(ingestService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); completionHandler.getValue().accept(exception); assertTrue(failureCalled.get()); @@ -331,7 +325,7 @@ public void testIngestForward() throws Exception { action.execute(null, bulkRequest, listener); // should not have executed ingest locally - verify(executionService, never()).executeBulkRequest(any(), any(), any()); + verify(ingestService, never()).executeBulkRequest(any(), any(), any()); // but instead should have sent to a remote node with the transport service ArgumentCaptor node = ArgumentCaptor.forClass(DiscoveryNode.class); verify(transportService).sendRequest(node.capture(), eq(BulkAction.NAME), any(), remoteResponseHandler.capture()); @@ -375,7 +369,7 @@ public void testSingleItemBulkActionIngestForward() throws Exception { singleItemBulkWriteAction.execute(null, indexRequest, listener); // should not have executed ingest locally - verify(executionService, never()).executeBulkRequest(any(), any(), any()); + verify(ingestService, never()).executeBulkRequest(any(), any(), any()); // but instead should have sent to a remote node with the transport service ArgumentCaptor node = ArgumentCaptor.forClass(DiscoveryNode.class); verify(transportService).sendRequest(node.capture(), eq(BulkAction.NAME), any(), remoteResponseHandler.capture()); @@ -423,7 +417,7 @@ public void testUseDefaultPipeline() throws Exception { assertFalse(action.isExecuted); // haven't executed yet assertFalse(responseCalled.get()); assertFalse(failureCalled.get()); - verify(executionService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); + verify(ingestService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); completionHandler.getValue().accept(exception); assertTrue(failureCalled.get()); @@ -455,7 +449,7 @@ public void testCreateIndexBeforeRunPipeline() throws Exception { assertFalse(action.isExecuted); // haven't executed yet assertFalse(responseCalled.get()); assertFalse(failureCalled.get()); - verify(executionService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); + verify(ingestService).executeBulkRequest(bulkDocsItr.capture(), failureHandler.capture(), completionHandler.capture()); completionHandler.getValue().accept(exception); assertTrue(failureCalled.get()); diff --git a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java index b0c6d717bb38..1711d1689108 100644 --- a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java +++ b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java @@ -31,8 +31,8 @@ import org.elasticsearch.index.VersionType; import org.elasticsearch.ingest.CompoundProcessor; import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.ingest.IngestService; import org.elasticsearch.ingest.Pipeline; -import org.elasticsearch.ingest.PipelineStore; import org.elasticsearch.ingest.Processor; import org.elasticsearch.ingest.TestProcessor; import org.elasticsearch.test.ESTestCase; @@ -53,7 +53,7 @@ public class SimulatePipelineRequestParsingTests extends ESTestCase { - private PipelineStore store; + private IngestService ingestService; @Before public void init() throws IOException { @@ -62,9 +62,9 @@ public void init() throws IOException { Pipeline pipeline = new Pipeline(SIMULATED_PIPELINE_ID, null, null, pipelineCompoundProcessor); Map registry = Collections.singletonMap("mock_processor", (factories, tag, config) -> processor); - store = mock(PipelineStore.class); - when(store.get(SIMULATED_PIPELINE_ID)).thenReturn(pipeline); - when(store.getProcessorFactories()).thenReturn(registry); + ingestService = mock(IngestService.class); + when(ingestService.getPipeline(SIMULATED_PIPELINE_ID)).thenReturn(pipeline); + when(ingestService.getProcessorFactories()).thenReturn(registry); } public void testParseUsingPipelineStore() throws Exception { @@ -94,7 +94,8 @@ public void testParseUsingPipelineStore() throws Exception { expectedDocs.add(expectedDoc); } - SimulatePipelineRequest.Parsed actualRequest = SimulatePipelineRequest.parseWithPipelineId(SIMULATED_PIPELINE_ID, requestContent, false, store); + SimulatePipelineRequest.Parsed actualRequest = + SimulatePipelineRequest.parseWithPipelineId(SIMULATED_PIPELINE_ID, requestContent, false, ingestService); assertThat(actualRequest.isVerbose(), equalTo(false)); assertThat(actualRequest.getDocuments().size(), equalTo(numDocs)); Iterator> expectedDocsIterator = expectedDocs.iterator(); @@ -182,7 +183,7 @@ public void testParseWithProvidedPipeline() throws Exception { requestContent.put(Fields.PIPELINE, pipelineConfig); - SimulatePipelineRequest.Parsed actualRequest = SimulatePipelineRequest.parse(requestContent, false, store); + SimulatePipelineRequest.Parsed actualRequest = SimulatePipelineRequest.parse(requestContent, false, ingestService); assertThat(actualRequest.isVerbose(), equalTo(false)); assertThat(actualRequest.getDocuments().size(), equalTo(numDocs)); Iterator> expectedDocsIterator = expectedDocs.iterator(); @@ -208,7 +209,7 @@ public void testNullPipelineId() { List> docs = new ArrayList<>(); requestContent.put(Fields.DOCS, docs); Exception e = expectThrows(IllegalArgumentException.class, - () -> SimulatePipelineRequest.parseWithPipelineId(null, requestContent, false, store)); + () -> SimulatePipelineRequest.parseWithPipelineId(null, requestContent, false, ingestService)); assertThat(e.getMessage(), equalTo("param [pipeline] is null")); } @@ -218,7 +219,7 @@ public void testNonExistentPipelineId() { List> docs = new ArrayList<>(); requestContent.put(Fields.DOCS, docs); Exception e = expectThrows(IllegalArgumentException.class, - () -> SimulatePipelineRequest.parseWithPipelineId(pipelineId, requestContent, false, store)); + () -> SimulatePipelineRequest.parseWithPipelineId(pipelineId, requestContent, false, ingestService)); assertThat(e.getMessage(), equalTo("pipeline [" + pipelineId + "] does not exist")); } } diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestProcessorNotInstalledOnAllNodesIT.java b/server/src/test/java/org/elasticsearch/ingest/IngestProcessorNotInstalledOnAllNodesIT.java index 338e5b662c5d..4c2352bfebe7 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestProcessorNotInstalledOnAllNodesIT.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestProcessorNotInstalledOnAllNodesIT.java @@ -97,12 +97,12 @@ public void testFailStartNode() throws Exception { AcknowledgedResponse response = client().admin().cluster().preparePutPipeline("_id", pipelineSource, XContentType.JSON).get(); assertThat(response.isAcknowledged(), is(true)); - Pipeline pipeline = internalCluster().getInstance(NodeService.class, node1).getIngestService().getPipelineStore().get("_id"); + Pipeline pipeline = internalCluster().getInstance(NodeService.class, node1).getIngestService().getPipeline("_id"); assertThat(pipeline, notNullValue()); installPlugin = false; String node2 = internalCluster().startNode(); - pipeline = internalCluster().getInstance(NodeService.class, node2).getIngestService().getPipelineStore().get("_id"); + pipeline = internalCluster().getInstance(NodeService.class, node2).getIngestService().getPipeline("_id"); assertNotNull(pipeline); assertThat(pipeline.getId(), equalTo("_id")); diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java index c0353acb7f9d..10516dc0d012 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java @@ -21,16 +21,69 @@ import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; import java.util.Map; - -import org.elasticsearch.common.settings.Settings; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.Version; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.delete.DeleteRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.ingest.DeletePipelineRequest; +import org.elasticsearch.action.ingest.PutPipelineRequest; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.client.Requests; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.VersionType; import org.elasticsearch.plugins.IngestPlugin; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; -import org.mockito.Mockito; +import org.hamcrest.CustomTypeSafeMatcher; +import org.mockito.ArgumentMatcher; +import org.mockito.invocation.InvocationOnMock; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class IngestServiceTests extends ESTestCase { - private final IngestPlugin DUMMY_PLUGIN = new IngestPlugin() { + + private static final IngestPlugin DUMMY_PLUGIN = new IngestPlugin() { @Override public Map getProcessors(Processor.Parameters parameters) { return Collections.singletonMap("foo", (factories, tag, config) -> null); @@ -38,19 +91,812 @@ public Map getProcessors(Processor.Parameters paramet }; public void testIngestPlugin() { - ThreadPool tp = Mockito.mock(ThreadPool.class); - IngestService ingestService = new IngestService(Settings.EMPTY, tp, null, null, + ThreadPool tp = mock(ThreadPool.class); + IngestService ingestService = new IngestService(mock(ClusterService.class), tp, null, null, null, Collections.singletonList(DUMMY_PLUGIN)); - Map factories = ingestService.getPipelineStore().getProcessorFactories(); + Map factories = ingestService.getProcessorFactories(); assertTrue(factories.containsKey("foo")); assertEquals(1, factories.size()); } public void testIngestPluginDuplicate() { - ThreadPool tp = Mockito.mock(ThreadPool.class); + ThreadPool tp = mock(ThreadPool.class); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> - new IngestService(Settings.EMPTY, tp, null, null, + new IngestService(mock(ClusterService.class), tp, null, null, null, Arrays.asList(DUMMY_PLUGIN, DUMMY_PLUGIN))); assertTrue(e.getMessage(), e.getMessage().contains("already registered")); } + + public void testExecuteIndexPipelineDoesNotExist() { + ThreadPool threadPool = mock(ThreadPool.class); + final ExecutorService executorService = EsExecutors.newDirectExecutorService(); + when(threadPool.executor(anyString())).thenReturn(executorService); + IngestService ingestService = new IngestService(mock(ClusterService.class), threadPool, null, null, + null, Collections.singletonList(DUMMY_PLUGIN)); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + + final SetOnce failure = new SetOnce<>(); + final BiConsumer failureHandler = (request, e) -> { + failure.set(true); + assertThat(request, sameInstance(indexRequest)); + assertThat(e, instanceOf(IllegalArgumentException.class)); + assertThat(e.getMessage(), equalTo("pipeline with id [_id] does not exist")); + }; + + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + + assertTrue(failure.get()); + verify(completionHandler, times(1)).accept(null); + } + + public void testUpdatePipelines() { + IngestService ingestService = createWithProcessors(); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + ClusterState previousClusterState = clusterState; + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.pipelines().size(), is(0)); + + PipelineConfiguration pipeline = new PipelineConfiguration( + "_id",new BytesArray("{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON + ); + IngestMetadata ingestMetadata = new IngestMetadata(Collections.singletonMap("_id", pipeline)); + clusterState = ClusterState.builder(clusterState) + .metaData(MetaData.builder().putCustom(IngestMetadata.TYPE, ingestMetadata)) + .build(); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.pipelines().size(), is(1)); + assertThat(ingestService.pipelines().get("_id").getId(), equalTo("_id")); + assertThat(ingestService.pipelines().get("_id").getDescription(), nullValue()); + assertThat(ingestService.pipelines().get("_id").getProcessors().size(), equalTo(1)); + assertThat(ingestService.pipelines().get("_id").getProcessors().get(0).getType(), equalTo("set")); + } + + public void testDelete() { + IngestService ingestService = createWithProcessors(); + PipelineConfiguration config = new PipelineConfiguration( + "_id",new BytesArray("{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON + ); + IngestMetadata ingestMetadata = new IngestMetadata(Collections.singletonMap("_id", config)); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + ClusterState previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).metaData(MetaData.builder() + .putCustom(IngestMetadata.TYPE, ingestMetadata)).build(); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.getPipeline("_id"), notNullValue()); + + // Delete pipeline: + DeletePipelineRequest deleteRequest = new DeletePipelineRequest("_id"); + previousClusterState = clusterState; + clusterState = IngestService.innerDelete(deleteRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.getPipeline("_id"), nullValue()); + + // Delete existing pipeline: + try { + IngestService.innerDelete(deleteRequest, clusterState); + fail("exception expected"); + } catch (ResourceNotFoundException e) { + assertThat(e.getMessage(), equalTo("pipeline [_id] is missing")); + } + } + + public void testValidateNoIngestInfo() throws Exception { + IngestService ingestService = createWithProcessors(); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", new BytesArray( + "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON); + Exception e = expectThrows(IllegalStateException.class, () -> ingestService.validatePipeline(emptyMap(), putRequest)); + assertEquals("Ingest info is empty", e.getMessage()); + + DiscoveryNode discoveryNode = new DiscoveryNode("_node_id", buildNewFakeTransportAddress(), + emptyMap(), emptySet(), Version.CURRENT); + IngestInfo ingestInfo = new IngestInfo(Collections.singletonList(new ProcessorInfo("set"))); + ingestService.validatePipeline(Collections.singletonMap(discoveryNode, ingestInfo), putRequest); + } + + public void testCrud() throws Exception { + IngestService ingestService = createWithProcessors(); + String id = "_id"; + Pipeline pipeline = ingestService.getPipeline(id); + assertThat(pipeline, nullValue()); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + + PutPipelineRequest putRequest = new PutPipelineRequest(id, + new BytesArray("{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON); + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + pipeline = ingestService.getPipeline(id); + assertThat(pipeline, notNullValue()); + assertThat(pipeline.getId(), equalTo(id)); + assertThat(pipeline.getDescription(), nullValue()); + assertThat(pipeline.getProcessors().size(), equalTo(1)); + assertThat(pipeline.getProcessors().get(0).getType(), equalTo("set")); + + DeletePipelineRequest deleteRequest = new DeletePipelineRequest(id); + previousClusterState = clusterState; + clusterState = IngestService.innerDelete(deleteRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + pipeline = ingestService.getPipeline(id); + assertThat(pipeline, nullValue()); + } + + public void testPut() { + IngestService ingestService = createWithProcessors(); + String id = "_id"; + Pipeline pipeline = ingestService.getPipeline(id); + assertThat(pipeline, nullValue()); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + + // add a new pipeline: + PutPipelineRequest putRequest = new PutPipelineRequest(id, new BytesArray("{\"processors\": []}"), XContentType.JSON); + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + pipeline = ingestService.getPipeline(id); + assertThat(pipeline, notNullValue()); + assertThat(pipeline.getId(), equalTo(id)); + assertThat(pipeline.getDescription(), nullValue()); + assertThat(pipeline.getProcessors().size(), equalTo(0)); + + // overwrite existing pipeline: + putRequest = + new PutPipelineRequest(id, new BytesArray("{\"processors\": [], \"description\": \"_description\"}"), XContentType.JSON); + previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + pipeline = ingestService.getPipeline(id); + assertThat(pipeline, notNullValue()); + assertThat(pipeline.getId(), equalTo(id)); + assertThat(pipeline.getDescription(), equalTo("_description")); + assertThat(pipeline.getProcessors().size(), equalTo(0)); + } + + public void testPutWithErrorResponse() { + IngestService ingestService = createWithProcessors(); + String id = "_id"; + Pipeline pipeline = ingestService.getPipeline(id); + assertThat(pipeline, nullValue()); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + + PutPipelineRequest putRequest = + new PutPipelineRequest(id, new BytesArray("{\"description\": \"empty processors\"}"), XContentType.JSON); + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + try { + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + fail("should fail"); + } catch (ElasticsearchParseException e) { + assertThat(e.getMessage(), equalTo("[processors] required property is missing")); + } + pipeline = ingestService.getPipeline(id); + assertNotNull(pipeline); + assertThat(pipeline.getId(), equalTo("_id")); + assertThat(pipeline.getDescription(), equalTo("this is a place holder pipeline, because pipeline with" + + " id [_id] could not be loaded")); + assertThat(pipeline.getProcessors().size(), equalTo(1)); + assertNull(pipeline.getProcessors().get(0).getTag()); + assertThat(pipeline.getProcessors().get(0).getType(), equalTo("unknown")); + } + + public void testDeleteUsingWildcard() { + IngestService ingestService = createWithProcessors(); + HashMap pipelines = new HashMap<>(); + BytesArray definition = new BytesArray( + "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}" + ); + pipelines.put("p1", new PipelineConfiguration("p1", definition, XContentType.JSON)); + pipelines.put("p2", new PipelineConfiguration("p2", definition, XContentType.JSON)); + pipelines.put("q1", new PipelineConfiguration("q1", definition, XContentType.JSON)); + IngestMetadata ingestMetadata = new IngestMetadata(pipelines); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + ClusterState previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).metaData(MetaData.builder() + .putCustom(IngestMetadata.TYPE, ingestMetadata)).build(); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.getPipeline("p1"), notNullValue()); + assertThat(ingestService.getPipeline("p2"), notNullValue()); + assertThat(ingestService.getPipeline("q1"), notNullValue()); + + // Delete pipeline matching wildcard + DeletePipelineRequest deleteRequest = new DeletePipelineRequest("p*"); + previousClusterState = clusterState; + clusterState = IngestService.innerDelete(deleteRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.getPipeline("p1"), nullValue()); + assertThat(ingestService.getPipeline("p2"), nullValue()); + assertThat(ingestService.getPipeline("q1"), notNullValue()); + + // Exception if we used name which does not exist + try { + IngestService.innerDelete(new DeletePipelineRequest("unknown"), clusterState); + fail("exception expected"); + } catch (ResourceNotFoundException e) { + assertThat(e.getMessage(), equalTo("pipeline [unknown] is missing")); + } + + // match all wildcard works on last remaining pipeline + DeletePipelineRequest matchAllDeleteRequest = new DeletePipelineRequest("*"); + previousClusterState = clusterState; + clusterState = IngestService.innerDelete(matchAllDeleteRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.getPipeline("p1"), nullValue()); + assertThat(ingestService.getPipeline("p2"), nullValue()); + assertThat(ingestService.getPipeline("q1"), nullValue()); + + // match all wildcard does not throw exception if none match + IngestService.innerDelete(matchAllDeleteRequest, clusterState); + } + + public void testDeleteWithExistingUnmatchedPipelines() { + IngestService ingestService = createWithProcessors(); + HashMap pipelines = new HashMap<>(); + BytesArray definition = new BytesArray( + "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}" + ); + pipelines.put("p1", new PipelineConfiguration("p1", definition, XContentType.JSON)); + IngestMetadata ingestMetadata = new IngestMetadata(pipelines); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + ClusterState previousClusterState = clusterState; + clusterState = ClusterState.builder(clusterState).metaData(MetaData.builder() + .putCustom(IngestMetadata.TYPE, ingestMetadata)).build(); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + assertThat(ingestService.getPipeline("p1"), notNullValue()); + + DeletePipelineRequest deleteRequest = new DeletePipelineRequest("z*"); + try { + IngestService.innerDelete(deleteRequest, clusterState); + fail("exception expected"); + } catch (ResourceNotFoundException e) { + assertThat(e.getMessage(), equalTo("pipeline [z*] is missing")); + } + } + + public void testGetPipelines() { + Map configs = new HashMap<>(); + configs.put("_id1", new PipelineConfiguration( + "_id1", new BytesArray("{\"processors\": []}"), XContentType.JSON + )); + configs.put("_id2", new PipelineConfiguration( + "_id2", new BytesArray("{\"processors\": []}"), XContentType.JSON + )); + + assertThat(IngestService.innerGetPipelines(null, "_id1").isEmpty(), is(true)); + + IngestMetadata ingestMetadata = new IngestMetadata(configs); + List pipelines = IngestService.innerGetPipelines(ingestMetadata, "_id1"); + assertThat(pipelines.size(), equalTo(1)); + assertThat(pipelines.get(0).getId(), equalTo("_id1")); + + pipelines = IngestService.innerGetPipelines(ingestMetadata, "_id1", "_id2"); + assertThat(pipelines.size(), equalTo(2)); + assertThat(pipelines.get(0).getId(), equalTo("_id1")); + assertThat(pipelines.get(1).getId(), equalTo("_id2")); + + pipelines = IngestService.innerGetPipelines(ingestMetadata, "_id*"); + pipelines.sort(Comparator.comparing(PipelineConfiguration::getId)); + assertThat(pipelines.size(), equalTo(2)); + assertThat(pipelines.get(0).getId(), equalTo("_id1")); + assertThat(pipelines.get(1).getId(), equalTo("_id2")); + + // get all variants: (no IDs or '*') + pipelines = IngestService.innerGetPipelines(ingestMetadata); + pipelines.sort(Comparator.comparing(PipelineConfiguration::getId)); + assertThat(pipelines.size(), equalTo(2)); + assertThat(pipelines.get(0).getId(), equalTo("_id1")); + assertThat(pipelines.get(1).getId(), equalTo("_id2")); + + pipelines = IngestService.innerGetPipelines(ingestMetadata, "*"); + pipelines.sort(Comparator.comparing(PipelineConfiguration::getId)); + assertThat(pipelines.size(), equalTo(2)); + assertThat(pipelines.get(0).getId(), equalTo("_id1")); + assertThat(pipelines.get(1).getId(), equalTo("_id2")); + } + + public void testValidate() throws Exception { + IngestService ingestService = createWithProcessors(); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", new BytesArray( + "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\", \"tag\": \"tag1\"}}," + + "{\"remove\" : {\"field\": \"_field\", \"tag\": \"tag2\"}}]}"), + XContentType.JSON); + + DiscoveryNode node1 = new DiscoveryNode("_node_id1", buildNewFakeTransportAddress(), + emptyMap(), emptySet(), Version.CURRENT); + DiscoveryNode node2 = new DiscoveryNode("_node_id2", buildNewFakeTransportAddress(), + emptyMap(), emptySet(), Version.CURRENT); + Map ingestInfos = new HashMap<>(); + ingestInfos.put(node1, new IngestInfo(Arrays.asList(new ProcessorInfo("set"), new ProcessorInfo("remove")))); + ingestInfos.put(node2, new IngestInfo(Arrays.asList(new ProcessorInfo("set")))); + + ElasticsearchParseException e = + expectThrows(ElasticsearchParseException.class, () -> ingestService.validatePipeline(ingestInfos, putRequest)); + assertEquals("Processor type [remove] is not installed on node [" + node2 + "]", e.getMessage()); + assertEquals("remove", e.getMetadata("es.processor_type").get(0)); + assertEquals("tag2", e.getMetadata("es.processor_tag").get(0)); + + ingestInfos.put(node2, new IngestInfo(Arrays.asList(new ProcessorInfo("set"), new ProcessorInfo("remove")))); + ingestService.validatePipeline(ingestInfos, putRequest); + } + + public void testExecuteIndexPipelineExistsButFailedParsing() { + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> new AbstractProcessor("mock") { + @Override + public void execute(IngestDocument ingestDocument) { + throw new IllegalStateException("error"); + } + + @Override + public String getType() { + return null; + } + } + )); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + String id = "_id"; + PutPipelineRequest putRequest = new PutPipelineRequest(id, + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final SetOnce failure = new SetOnce<>(); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline(id); + final BiConsumer failureHandler = (request, e) -> { + assertThat(e.getCause(), instanceOf(IllegalArgumentException.class)); + assertThat(e.getCause().getCause(), instanceOf(IllegalStateException.class)); + assertThat(e.getCause().getCause().getMessage(), equalTo("error")); + failure.set(true); + }; + + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + + assertTrue(failure.get()); + verify(completionHandler, times(1)).accept(null); + } + + public void testExecuteBulkPipelineDoesNotExist() { + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> mock(CompoundProcessor.class))); + + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + + BulkRequest bulkRequest = new BulkRequest(); + + IndexRequest indexRequest1 = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + bulkRequest.add(indexRequest1); + IndexRequest indexRequest2 = + new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("does_not_exist"); + bulkRequest.add(indexRequest2); + @SuppressWarnings("unchecked") + BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(bulkRequest.requests(), failureHandler, completionHandler); + verify(failureHandler, times(1)).accept( + argThat(new CustomTypeSafeMatcher("failure handler was not called with the expected arguments") { + @Override + protected boolean matchesSafely(IndexRequest item) { + return item == indexRequest2; + } + + }), + argThat(new CustomTypeSafeMatcher("failure handler was not called with the expected arguments") { + @Override + protected boolean matchesSafely(IllegalArgumentException iae) { + return "pipeline with id [does_not_exist] does not exist".equals(iae.getMessage()); + } + }) + ); + verify(completionHandler, times(1)).accept(null); + } + + public void testExecuteSuccess() { + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> mock(CompoundProcessor.class))); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + @SuppressWarnings("unchecked") + final BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + verify(failureHandler, never()).accept(any(), any()); + verify(completionHandler, times(1)).accept(null); + } + + public void testExecuteEmptyPipeline() throws Exception { + IngestService ingestService = createWithProcessors(emptyMap()); + PutPipelineRequest putRequest = + new PutPipelineRequest("_id", new BytesArray("{\"processors\": [], \"description\": \"_description\"}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + @SuppressWarnings("unchecked") + final BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + verify(failureHandler, never()).accept(any(), any()); + verify(completionHandler, times(1)).accept(null); + } + + public void testExecutePropagateAllMetaDataUpdates() throws Exception { + final CompoundProcessor processor = mock(CompoundProcessor.class); + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> processor)); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final long newVersion = randomLong(); + final String versionType = randomFrom("internal", "external", "external_gt", "external_gte"); + doAnswer((InvocationOnMock invocationOnMock) -> { + IngestDocument ingestDocument = (IngestDocument) invocationOnMock.getArguments()[0]; + for (IngestDocument.MetaData metaData : IngestDocument.MetaData.values()) { + if (metaData == IngestDocument.MetaData.VERSION) { + ingestDocument.setFieldValue(metaData.getFieldName(), newVersion); + } else if (metaData == IngestDocument.MetaData.VERSION_TYPE) { + ingestDocument.setFieldValue(metaData.getFieldName(), versionType); + } else { + ingestDocument.setFieldValue(metaData.getFieldName(), "update" + metaData.getFieldName()); + } + } + return null; + }).when(processor).execute(any()); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + @SuppressWarnings("unchecked") + final BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + verify(processor).execute(any()); + verify(failureHandler, never()).accept(any(), any()); + verify(completionHandler, times(1)).accept(null); + assertThat(indexRequest.index(), equalTo("update_index")); + assertThat(indexRequest.type(), equalTo("update_type")); + assertThat(indexRequest.id(), equalTo("update_id")); + assertThat(indexRequest.routing(), equalTo("update_routing")); + assertThat(indexRequest.version(), equalTo(newVersion)); + assertThat(indexRequest.versionType(), equalTo(VersionType.fromString(versionType))); + } + + public void testExecuteFailure() throws Exception { + final CompoundProcessor processor = mock(CompoundProcessor.class); + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> processor)); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + doThrow(new RuntimeException()) + .when(processor) + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap())); + @SuppressWarnings("unchecked") + final BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap())); + verify(failureHandler, times(1)).accept(eq(indexRequest), any(RuntimeException.class)); + verify(completionHandler, times(1)).accept(null); + } + + public void testExecuteSuccessWithOnFailure() throws Exception { + final Processor processor = mock(Processor.class); + when(processor.getType()).thenReturn("mock_processor_type"); + when(processor.getTag()).thenReturn("mock_processor_tag"); + final Processor onFailureProcessor = mock(Processor.class); + final CompoundProcessor compoundProcessor = new CompoundProcessor( + false, Collections.singletonList(processor), Collections.singletonList(new CompoundProcessor(onFailureProcessor))); + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> compoundProcessor)); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + doThrow(new RuntimeException()).when(processor).execute(eqIndexTypeId(emptyMap())); + @SuppressWarnings("unchecked") + final BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + verify(failureHandler, never()).accept(eq(indexRequest), any(ElasticsearchException.class)); + verify(completionHandler, times(1)).accept(null); + } + + public void testExecuteFailureWithNestedOnFailure() throws Exception { + final Processor processor = mock(Processor.class); + final Processor onFailureProcessor = mock(Processor.class); + final Processor onFailureOnFailureProcessor = mock(Processor.class); + final List processors = Collections.singletonList(onFailureProcessor); + final List onFailureProcessors = Collections.singletonList(onFailureOnFailureProcessor); + final CompoundProcessor compoundProcessor = new CompoundProcessor( + false, + Collections.singletonList(processor), + Collections.singletonList(new CompoundProcessor(false, processors, onFailureProcessors))); + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> compoundProcessor)); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); + doThrow(new RuntimeException()) + .when(onFailureOnFailureProcessor) + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap())); + doThrow(new RuntimeException()) + .when(onFailureProcessor) + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap())); + doThrow(new RuntimeException()) + .when(processor) + .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap())); + @SuppressWarnings("unchecked") + final BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + final Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), emptyMap())); + verify(failureHandler, times(1)).accept(eq(indexRequest), any(RuntimeException.class)); + verify(completionHandler, times(1)).accept(null); + } + + public void testBulkRequestExecutionWithFailures() throws Exception { + BulkRequest bulkRequest = new BulkRequest(); + String pipelineId = "_id"; + + int numRequest = scaledRandomIntBetween(8, 64); + int numIndexRequests = 0; + for (int i = 0; i < numRequest; i++) { + DocWriteRequest request; + if (randomBoolean()) { + if (randomBoolean()) { + request = new DeleteRequest("_index", "_type", "_id"); + } else { + request = new UpdateRequest("_index", "_type", "_id"); + } + } else { + IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").setPipeline(pipelineId); + indexRequest.source(Requests.INDEX_CONTENT_TYPE, "field1", "value1"); + request = indexRequest; + numIndexRequests++; + } + bulkRequest.add(request); + } + + CompoundProcessor processor = mock(CompoundProcessor.class); + when(processor.getProcessors()).thenReturn(Collections.singletonList(mock(Processor.class))); + Exception error = new RuntimeException(); + doThrow(error).when(processor).execute(any()); + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> processor)); + PutPipelineRequest putRequest = new PutPipelineRequest("_id", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + + @SuppressWarnings("unchecked") + BiConsumer requestItemErrorHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(bulkRequest.requests(), requestItemErrorHandler, completionHandler); + + verify(requestItemErrorHandler, times(numIndexRequests)).accept(any(IndexRequest.class), argThat(new ArgumentMatcher() { + @Override + public boolean matches(final Object o) { + return ((Exception)o).getCause().getCause().equals(error); + } + })); + verify(completionHandler, times(1)).accept(null); + } + + public void testBulkRequestExecution() { + BulkRequest bulkRequest = new BulkRequest(); + String pipelineId = "_id"; + + int numRequest = scaledRandomIntBetween(8, 64); + for (int i = 0; i < numRequest; i++) { + IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").setPipeline(pipelineId); + indexRequest.source(Requests.INDEX_CONTENT_TYPE, "field1", "value1"); + bulkRequest.add(indexRequest); + } + + IngestService ingestService = createWithProcessors(emptyMap()); + PutPipelineRequest putRequest = + new PutPipelineRequest("_id", new BytesArray("{\"processors\": [], \"description\": \"_description\"}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + + @SuppressWarnings("unchecked") + BiConsumer requestItemErrorHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") + Consumer completionHandler = mock(Consumer.class); + ingestService.executeBulkRequest(bulkRequest.requests(), requestItemErrorHandler, completionHandler); + + verify(requestItemErrorHandler, never()).accept(any(), any()); + verify(completionHandler, times(1)).accept(null); + } + + public void testStats() { + final Processor processor = mock(Processor.class); + IngestService ingestService = createWithProcessors(Collections.singletonMap( + "mock", (factories, tag, config) -> processor)); + final IngestStats initialStats = ingestService.stats(); + assertThat(initialStats.getStatsPerPipeline().size(), equalTo(0)); + assertThat(initialStats.getTotalStats().getIngestCount(), equalTo(0L)); + assertThat(initialStats.getTotalStats().getIngestCurrent(), equalTo(0L)); + assertThat(initialStats.getTotalStats().getIngestFailedCount(), equalTo(0L)); + assertThat(initialStats.getTotalStats().getIngestTimeInMillis(), equalTo(0L)); + + PutPipelineRequest putRequest = new PutPipelineRequest("_id1", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty + ClusterState previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + putRequest = new PutPipelineRequest("_id2", + new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); + previousClusterState = clusterState; + clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); + final Map configurationMap = new HashMap<>(); + configurationMap.put("_id1", new PipelineConfiguration("_id1", new BytesArray("{}"), XContentType.JSON)); + configurationMap.put("_id2", new PipelineConfiguration("_id2", new BytesArray("{}"), XContentType.JSON)); + ingestService.updatePipelineStats(new IngestMetadata(configurationMap)); + + @SuppressWarnings("unchecked") final BiConsumer failureHandler = mock(BiConsumer.class); + @SuppressWarnings("unchecked") final Consumer completionHandler = mock(Consumer.class); + + final IndexRequest indexRequest = new IndexRequest("_index"); + indexRequest.setPipeline("_id1"); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + final IngestStats afterFirstRequestStats = ingestService.stats(); + assertThat(afterFirstRequestStats.getStatsPerPipeline().size(), equalTo(2)); + assertThat(afterFirstRequestStats.getStatsPerPipeline().get("_id1").getIngestCount(), equalTo(1L)); + assertThat(afterFirstRequestStats.getStatsPerPipeline().get("_id2").getIngestCount(), equalTo(0L)); + assertThat(afterFirstRequestStats.getTotalStats().getIngestCount(), equalTo(1L)); + + indexRequest.setPipeline("_id2"); + ingestService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); + final IngestStats afterSecondRequestStats = ingestService.stats(); + assertThat(afterSecondRequestStats.getStatsPerPipeline().size(), equalTo(2)); + assertThat(afterSecondRequestStats.getStatsPerPipeline().get("_id1").getIngestCount(), equalTo(1L)); + assertThat(afterSecondRequestStats.getStatsPerPipeline().get("_id2").getIngestCount(), equalTo(1L)); + assertThat(afterSecondRequestStats.getTotalStats().getIngestCount(), equalTo(2L)); + } + + // issue: https://github.com/elastic/elasticsearch/issues/18126 + public void testUpdatingStatsWhenRemovingPipelineWorks() { + IngestService ingestService = createWithProcessors(); + Map configurationMap = new HashMap<>(); + configurationMap.put("_id1", new PipelineConfiguration("_id1", new BytesArray("{}"), XContentType.JSON)); + configurationMap.put("_id2", new PipelineConfiguration("_id2", new BytesArray("{}"), XContentType.JSON)); + ingestService.updatePipelineStats(new IngestMetadata(configurationMap)); + assertThat(ingestService.stats().getStatsPerPipeline(), hasKey("_id1")); + assertThat(ingestService.stats().getStatsPerPipeline(), hasKey("_id2")); + + configurationMap = new HashMap<>(); + configurationMap.put("_id3", new PipelineConfiguration("_id3", new BytesArray("{}"), XContentType.JSON)); + ingestService.updatePipelineStats(new IngestMetadata(configurationMap)); + assertThat(ingestService.stats().getStatsPerPipeline(), not(hasKey("_id1"))); + assertThat(ingestService.stats().getStatsPerPipeline(), not(hasKey("_id2"))); + } + + private IngestDocument eqIndexTypeId(final Map source) { + return argThat(new IngestDocumentMatcher("_index", "_type", "_id", source)); + } + + private IngestDocument eqIndexTypeId(final Long version, final VersionType versionType, final Map source) { + return argThat(new IngestDocumentMatcher("_index", "_type", "_id", version, versionType, source)); + } + + private static IngestService createWithProcessors() { + Map processors = new HashMap<>(); + processors.put("set", (factories, tag, config) -> { + String field = (String) config.remove("field"); + String value = (String) config.remove("value"); + return new Processor() { + @Override + public void execute(IngestDocument ingestDocument) { + ingestDocument.setFieldValue(field, value); + } + + @Override + public String getType() { + return "set"; + } + + @Override + public String getTag() { + return tag; + } + }; + }); + processors.put("remove", (factories, tag, config) -> { + String field = (String) config.remove("field"); + return new Processor() { + @Override + public void execute(IngestDocument ingestDocument) { + ingestDocument.removeField(field); + } + + @Override + public String getType() { + return "remove"; + } + + @Override + public String getTag() { + return tag; + } + }; + }); + return createWithProcessors(processors); + } + + private static IngestService createWithProcessors(Map processors) { + ThreadPool threadPool = mock(ThreadPool.class); + final ExecutorService executorService = EsExecutors.newDirectExecutorService(); + when(threadPool.executor(anyString())).thenReturn(executorService); + return new IngestService(mock(ClusterService.class), threadPool, null, null, + null, Collections.singletonList(new IngestPlugin() { + @Override + public Map getProcessors(final Processor.Parameters parameters) { + return processors; + } + })); + } + + private class IngestDocumentMatcher extends ArgumentMatcher { + + private final IngestDocument ingestDocument; + + IngestDocumentMatcher(String index, String type, String id, Map source) { + this.ingestDocument = new IngestDocument(index, type, id, null, null, null, source); + } + + IngestDocumentMatcher(String index, String type, String id, Long version, VersionType versionType, Map source) { + this.ingestDocument = new IngestDocument(index, type, id, null, version, versionType, source); + } + + @Override + public boolean matches(Object o) { + if (o.getClass() == IngestDocument.class) { + IngestDocument otherIngestDocument = (IngestDocument) o; + //ingest metadata will not be the same (timestamp differs every time) + return Objects.equals(ingestDocument.getSourceAndMetadata(), otherIngestDocument.getSourceAndMetadata()); + } + return false; + } + } } diff --git a/server/src/test/java/org/elasticsearch/ingest/PipelineExecutionServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/PipelineExecutionServiceTests.java deleted file mode 100644 index 15a23421da26..000000000000 --- a/server/src/test/java/org/elasticsearch/ingest/PipelineExecutionServiceTests.java +++ /dev/null @@ -1,471 +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.ingest; - -import org.apache.lucene.util.SetOnce; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.DocWriteRequest; -import org.elasticsearch.action.bulk.BulkRequest; -import org.elasticsearch.action.delete.DeleteRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.update.UpdateRequest; -import org.elasticsearch.client.Requests; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.util.concurrent.EsExecutors; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.index.VersionType; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.ThreadPool; -import org.hamcrest.CustomTypeSafeMatcher; -import org.junit.Before; -import org.mockito.ArgumentMatcher; -import org.mockito.invocation.InvocationOnMock; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ExecutorService; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.argThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class PipelineExecutionServiceTests extends ESTestCase { - - private final Integer version = randomBoolean() ? randomInt() : null; - private PipelineStore store; - private PipelineExecutionService executionService; - - @Before - public void setup() { - store = mock(PipelineStore.class); - ThreadPool threadPool = mock(ThreadPool.class); - final ExecutorService executorService = EsExecutors.newDirectExecutorService(); - when(threadPool.executor(anyString())).thenReturn(executorService); - executionService = new PipelineExecutionService(store, threadPool); - } - - public void testExecuteIndexPipelineDoesNotExist() { - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - - final SetOnce failure = new SetOnce<>(); - final BiConsumer failureHandler = (request, e) -> { - failure.set(true); - assertThat(request, sameInstance(indexRequest)); - assertThat(e, instanceOf(IllegalArgumentException.class)); - assertThat(e.getMessage(), equalTo("pipeline with id [_id] does not exist")); - }; - - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - - assertTrue(failure.get()); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecuteIndexPipelineExistsButFailedParsing() { - when(store.get("_id")).thenReturn(new Pipeline("_id", "stub", null, - new CompoundProcessor(new AbstractProcessor("mock") { - @Override - public void execute(IngestDocument ingestDocument) { - throw new IllegalStateException("error"); - } - - @Override - public String getType() { - return null; - } - }))); - - final SetOnce failure = new SetOnce<>(); - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - final BiConsumer failureHandler = (request, e) -> { - assertThat(e.getCause(), instanceOf(IllegalArgumentException.class)); - assertThat(e.getCause().getCause(), instanceOf(IllegalStateException.class)); - assertThat(e.getCause().getCause().getMessage(), equalTo("error")); - failure.set(true); - }; - - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - - assertTrue(failure.get()); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecuteBulkPipelineDoesNotExist() { - CompoundProcessor processor = mock(CompoundProcessor.class); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, processor)); - BulkRequest bulkRequest = new BulkRequest(); - - IndexRequest indexRequest1 = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - bulkRequest.add(indexRequest1); - IndexRequest indexRequest2 = - new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("does_not_exist"); - bulkRequest.add(indexRequest2); - @SuppressWarnings("unchecked") - BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(bulkRequest.requests(), failureHandler, completionHandler); - verify(failureHandler, times(1)).accept( - argThat(new CustomTypeSafeMatcher("failure handler was not called with the expected arguments") { - @Override - protected boolean matchesSafely(IndexRequest item) { - return item == indexRequest2; - } - - }), - argThat(new CustomTypeSafeMatcher("failure handler was not called with the expected arguments") { - @Override - protected boolean matchesSafely(IllegalArgumentException iae) { - return "pipeline with id [does_not_exist] does not exist".equals(iae.getMessage()); - } - }) - ); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecuteSuccess() { - final CompoundProcessor processor = mock(CompoundProcessor.class); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, processor)); - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - verify(failureHandler, never()).accept(any(), any()); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecuteEmptyPipeline() throws Exception { - final CompoundProcessor processor = mock(CompoundProcessor.class); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, processor)); - when(processor.getProcessors()).thenReturn(Collections.emptyList()); - - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - verify(processor, never()).execute(any()); - verify(failureHandler, never()).accept(any(), any()); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecutePropagateAllMetaDataUpdates() throws Exception { - final CompoundProcessor processor = mock(CompoundProcessor.class); - when(processor.getProcessors()).thenReturn(Collections.singletonList(mock(Processor.class))); - final long newVersion = randomLong(); - final String versionType = randomFrom("internal", "external", "external_gt", "external_gte"); - doAnswer((InvocationOnMock invocationOnMock) -> { - IngestDocument ingestDocument = (IngestDocument) invocationOnMock.getArguments()[0]; - for (IngestDocument.MetaData metaData : IngestDocument.MetaData.values()) { - if (metaData == IngestDocument.MetaData.VERSION) { - ingestDocument.setFieldValue(metaData.getFieldName(), newVersion); - } else if (metaData == IngestDocument.MetaData.VERSION_TYPE) { - ingestDocument.setFieldValue(metaData.getFieldName(), versionType); - } else { - ingestDocument.setFieldValue(metaData.getFieldName(), "update" + metaData.getFieldName()); - } - } - return null; - }).when(processor).execute(any()); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, processor)); - - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - verify(processor).execute(any()); - verify(failureHandler, never()).accept(any(), any()); - verify(completionHandler, times(1)).accept(null); - assertThat(indexRequest.index(), equalTo("update_index")); - assertThat(indexRequest.type(), equalTo("update_type")); - assertThat(indexRequest.id(), equalTo("update_id")); - assertThat(indexRequest.routing(), equalTo("update_routing")); - assertThat(indexRequest.version(), equalTo(newVersion)); - assertThat(indexRequest.versionType(), equalTo(VersionType.fromString(versionType))); - } - - public void testExecuteFailure() throws Exception { - final CompoundProcessor processor = mock(CompoundProcessor.class); - when(processor.getProcessors()).thenReturn(Collections.singletonList(mock(Processor.class))); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, processor)); - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - doThrow(new RuntimeException()) - .when(processor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - verify(failureHandler, times(1)).accept(eq(indexRequest), any(RuntimeException.class)); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecuteSuccessWithOnFailure() throws Exception { - final Processor processor = mock(Processor.class); - when(processor.getType()).thenReturn("mock_processor_type"); - when(processor.getTag()).thenReturn("mock_processor_tag"); - final Processor onFailureProcessor = mock(Processor.class); - final CompoundProcessor compoundProcessor = new CompoundProcessor( - false, Collections.singletonList(processor), Collections.singletonList(new CompoundProcessor(onFailureProcessor))); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, compoundProcessor)); - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - doThrow(new RuntimeException()).when(processor).execute(eqIndexTypeId(Collections.emptyMap())); - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - verify(failureHandler, never()).accept(eq(indexRequest), any(ElasticsearchException.class)); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecuteFailureWithOnFailure() throws Exception { - final Processor processor = mock(Processor.class); - final Processor onFailureProcessor = mock(Processor.class); - final CompoundProcessor compoundProcessor = new CompoundProcessor( - false, Collections.singletonList(processor), Collections.singletonList(new CompoundProcessor(onFailureProcessor))); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, compoundProcessor)); - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - doThrow(new RuntimeException()) - .when(processor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - doThrow(new RuntimeException()) - .when(onFailureProcessor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - verify(failureHandler, times(1)).accept(eq(indexRequest), any(RuntimeException.class)); - verify(completionHandler, times(1)).accept(null); - } - - public void testExecuteFailureWithNestedOnFailure() throws Exception { - final Processor processor = mock(Processor.class); - final Processor onFailureProcessor = mock(Processor.class); - final Processor onFailureOnFailureProcessor = mock(Processor.class); - final List processors = Collections.singletonList(onFailureProcessor); - final List onFailureProcessors = Collections.singletonList(onFailureOnFailureProcessor); - final CompoundProcessor compoundProcessor = new CompoundProcessor( - false, - Collections.singletonList(processor), - Collections.singletonList(new CompoundProcessor(false, processors, onFailureProcessors))); - when(store.get("_id")).thenReturn(new Pipeline("_id", "_description", version, compoundProcessor)); - final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(Collections.emptyMap()).setPipeline("_id"); - doThrow(new RuntimeException()) - .when(onFailureOnFailureProcessor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - doThrow(new RuntimeException()) - .when(onFailureProcessor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - doThrow(new RuntimeException()) - .when(processor) - .execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - verify(processor).execute(eqIndexTypeId(indexRequest.version(), indexRequest.versionType(), Collections.emptyMap())); - verify(failureHandler, times(1)).accept(eq(indexRequest), any(RuntimeException.class)); - verify(completionHandler, times(1)).accept(null); - } - - public void testBulkRequestExecutionWithFailures() throws Exception { - BulkRequest bulkRequest = new BulkRequest(); - String pipelineId = "_id"; - - int numRequest = scaledRandomIntBetween(8, 64); - int numIndexRequests = 0; - for (int i = 0; i < numRequest; i++) { - DocWriteRequest request; - if (randomBoolean()) { - if (randomBoolean()) { - request = new DeleteRequest("_index", "_type", "_id"); - } else { - request = new UpdateRequest("_index", "_type", "_id"); - } - } else { - IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").setPipeline(pipelineId); - indexRequest.source(Requests.INDEX_CONTENT_TYPE, "field1", "value1"); - request = indexRequest; - numIndexRequests++; - } - bulkRequest.add(request); - } - - CompoundProcessor processor = mock(CompoundProcessor.class); - when(processor.getProcessors()).thenReturn(Collections.singletonList(mock(Processor.class))); - Exception error = new RuntimeException(); - doThrow(error).when(processor).execute(any()); - when(store.get(pipelineId)).thenReturn(new Pipeline(pipelineId, null, version, processor)); - - @SuppressWarnings("unchecked") - BiConsumer requestItemErrorHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(bulkRequest.requests(), requestItemErrorHandler, completionHandler); - - verify(requestItemErrorHandler, times(numIndexRequests)).accept(any(IndexRequest.class), eq(error)); - verify(completionHandler, times(1)).accept(null); - } - - public void testBulkRequestExecution() { - BulkRequest bulkRequest = new BulkRequest(); - String pipelineId = "_id"; - - int numRequest = scaledRandomIntBetween(8, 64); - for (int i = 0; i < numRequest; i++) { - IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").setPipeline(pipelineId); - indexRequest.source(Requests.INDEX_CONTENT_TYPE, "field1", "value1"); - bulkRequest.add(indexRequest); - } - - when(store.get(pipelineId)).thenReturn(new Pipeline(pipelineId, null, version, new CompoundProcessor())); - - @SuppressWarnings("unchecked") - BiConsumer requestItemErrorHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - Consumer completionHandler = mock(Consumer.class); - executionService.executeBulkRequest(bulkRequest.requests(), requestItemErrorHandler, completionHandler); - - verify(requestItemErrorHandler, never()).accept(any(), any()); - verify(completionHandler, times(1)).accept(null); - } - - public void testStats() { - final IngestStats initialStats = executionService.stats(); - assertThat(initialStats.getStatsPerPipeline().size(), equalTo(0)); - assertThat(initialStats.getTotalStats().getIngestCount(), equalTo(0L)); - assertThat(initialStats.getTotalStats().getIngestCurrent(), equalTo(0L)); - assertThat(initialStats.getTotalStats().getIngestFailedCount(), equalTo(0L)); - assertThat(initialStats.getTotalStats().getIngestTimeInMillis(), equalTo(0L)); - - when(store.get("_id1")).thenReturn(new Pipeline("_id1", null, version, new CompoundProcessor(mock(Processor.class)))); - when(store.get("_id2")).thenReturn(new Pipeline("_id2", null, null, new CompoundProcessor(mock(Processor.class)))); - - final Map configurationMap = new HashMap<>(); - configurationMap.put("_id1", new PipelineConfiguration("_id1", new BytesArray("{}"), XContentType.JSON)); - configurationMap.put("_id2", new PipelineConfiguration("_id2", new BytesArray("{}"), XContentType.JSON)); - executionService.updatePipelineStats(new IngestMetadata(configurationMap)); - - @SuppressWarnings("unchecked") - final BiConsumer failureHandler = mock(BiConsumer.class); - @SuppressWarnings("unchecked") - final Consumer completionHandler = mock(Consumer.class); - - final IndexRequest indexRequest = new IndexRequest("_index"); - indexRequest.setPipeline("_id1"); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - final IngestStats afterFirstRequestStats = executionService.stats(); - assertThat(afterFirstRequestStats.getStatsPerPipeline().size(), equalTo(2)); - assertThat(afterFirstRequestStats.getStatsPerPipeline().get("_id1").getIngestCount(), equalTo(1L)); - assertThat(afterFirstRequestStats.getStatsPerPipeline().get("_id2").getIngestCount(), equalTo(0L)); - assertThat(afterFirstRequestStats.getTotalStats().getIngestCount(), equalTo(1L)); - - indexRequest.setPipeline("_id2"); - executionService.executeBulkRequest(Collections.singletonList(indexRequest), failureHandler, completionHandler); - final IngestStats afterSecondRequestStats = executionService.stats(); - assertThat(afterSecondRequestStats.getStatsPerPipeline().size(), equalTo(2)); - assertThat(afterSecondRequestStats.getStatsPerPipeline().get("_id1").getIngestCount(), equalTo(1L)); - assertThat(afterSecondRequestStats.getStatsPerPipeline().get("_id2").getIngestCount(), equalTo(1L)); - assertThat(afterSecondRequestStats.getTotalStats().getIngestCount(), equalTo(2L)); - } - - // issue: https://github.com/elastic/elasticsearch/issues/18126 - public void testUpdatingStatsWhenRemovingPipelineWorks() { - Map configurationMap = new HashMap<>(); - configurationMap.put("_id1", new PipelineConfiguration("_id1", new BytesArray("{}"), XContentType.JSON)); - configurationMap.put("_id2", new PipelineConfiguration("_id2", new BytesArray("{}"), XContentType.JSON)); - executionService.updatePipelineStats(new IngestMetadata(configurationMap)); - assertThat(executionService.stats().getStatsPerPipeline(), hasKey("_id1")); - assertThat(executionService.stats().getStatsPerPipeline(), hasKey("_id2")); - - configurationMap = new HashMap<>(); - configurationMap.put("_id3", new PipelineConfiguration("_id3", new BytesArray("{}"), XContentType.JSON)); - executionService.updatePipelineStats(new IngestMetadata(configurationMap)); - assertThat(executionService.stats().getStatsPerPipeline(), not(hasKey("_id1"))); - assertThat(executionService.stats().getStatsPerPipeline(), not(hasKey("_id2"))); - } - - private IngestDocument eqIndexTypeId(final Map source) { - return argThat(new IngestDocumentMatcher("_index", "_type", "_id", source)); - } - - private IngestDocument eqIndexTypeId(final Long version, final VersionType versionType, final Map source) { - return argThat(new IngestDocumentMatcher("_index", "_type", "_id", version, versionType, source)); - } - - private class IngestDocumentMatcher extends ArgumentMatcher { - - private final IngestDocument ingestDocument; - - IngestDocumentMatcher(String index, String type, String id, Map source) { - this.ingestDocument = new IngestDocument(index, type, id, null, null, null, source); - } - - IngestDocumentMatcher(String index, String type, String id, Long version, VersionType versionType, Map source) { - this.ingestDocument = new IngestDocument(index, type, id, null, version, versionType, source); - } - - @Override - public boolean matches(Object o) { - if (o.getClass() == IngestDocument.class) { - IngestDocument otherIngestDocument = (IngestDocument) o; - //ingest metadata will not be the same (timestamp differs every time) - return Objects.equals(ingestDocument.getSourceAndMetadata(), otherIngestDocument.getSourceAndMetadata()); - } - return false; - } - } -} diff --git a/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java b/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java index 461873a3fe3d..cafdbcfb4469 100644 --- a/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java @@ -47,9 +47,8 @@ public void testCreate() throws Exception { pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Arrays.asList(Collections.singletonMap("test", processorConfig0), Collections.singletonMap("test", processorConfig1))); - Pipeline.Factory factory = new Pipeline.Factory(); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Pipeline pipeline = factory.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -64,9 +63,8 @@ public void testCreateWithNoProcessorsField() throws Exception { Map pipelineConfig = new HashMap<>(); pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); - Pipeline.Factory factory = new Pipeline.Factory(); try { - factory.create("_id", pipelineConfig, Collections.emptyMap()); + Pipeline.create("_id", pipelineConfig, Collections.emptyMap()); fail("should fail, missing required [processors] field"); } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[processors] required property is missing")); @@ -78,8 +76,7 @@ public void testCreateWithEmptyProcessorsField() throws Exception { pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.emptyList()); - Pipeline.Factory factory = new Pipeline.Factory(); - Pipeline pipeline = factory.create("_id", pipelineConfig, null); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, null); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -93,9 +90,8 @@ public void testCreateWithPipelineOnFailure() throws Exception { pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); pipelineConfig.put(Pipeline.ON_FAILURE_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Pipeline.Factory factory = new Pipeline.Factory(); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Pipeline pipeline = factory.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -112,9 +108,8 @@ public void testCreateWithPipelineEmptyOnFailure() throws Exception { pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); pipelineConfig.put(Pipeline.ON_FAILURE_KEY, Collections.emptyList()); - Pipeline.Factory factory = new Pipeline.Factory(); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Exception e = expectThrows(ElasticsearchParseException.class, () -> factory.create("_id", pipelineConfig, processorRegistry)); + Exception e = expectThrows(ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry)); assertThat(e.getMessage(), equalTo("pipeline [_id] cannot have an empty on_failure option defined")); } @@ -125,9 +120,8 @@ public void testCreateWithPipelineEmptyOnFailureInProcessor() throws Exception { pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Pipeline.Factory factory = new Pipeline.Factory(); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Exception e = expectThrows(ElasticsearchParseException.class, () -> factory.create("_id", pipelineConfig, processorRegistry)); + Exception e = expectThrows(ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry)); assertThat(e.getMessage(), equalTo("[on_failure] processors list cannot be empty")); } @@ -136,14 +130,13 @@ public void testCreateWithPipelineIgnoreFailure() throws Exception { processorConfig.put("ignore_failure", true); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Pipeline.Factory factory = new Pipeline.Factory(); Map pipelineConfig = new HashMap<>(); pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Pipeline pipeline = factory.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -162,9 +155,8 @@ public void testCreateUnusedProcessorOptions() throws Exception { pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Pipeline.Factory factory = new Pipeline.Factory(); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Exception e = expectThrows(ElasticsearchParseException.class, () -> factory.create("_id", pipelineConfig, processorRegistry)); + Exception e = expectThrows(ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry)); assertThat(e.getMessage(), equalTo("processor [test] doesn't support one or more provided configuration parameters [unused]")); } @@ -176,9 +168,8 @@ public void testCreateProcessorsWithOnFailureProperties() throws Exception { pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Pipeline.Factory factory = new Pipeline.Factory(); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Pipeline pipeline = factory.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); diff --git a/server/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java b/server/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java deleted file mode 100644 index d0ce465fc9ef..000000000000 --- a/server/src/test/java/org/elasticsearch/ingest/PipelineStoreTests.java +++ /dev/null @@ -1,377 +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.ingest; - -import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.ResourceNotFoundException; -import org.elasticsearch.Version; -import org.elasticsearch.action.ingest.DeletePipelineRequest; -import org.elasticsearch.action.ingest.PutPipelineRequest; -import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.metadata.MetaData; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.test.ESTestCase; -import org.junit.Before; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static java.util.Collections.emptyMap; -import static java.util.Collections.emptySet; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; - -public class PipelineStoreTests extends ESTestCase { - - private PipelineStore store; - - @Before - public void init() throws Exception { - Map processorFactories = new HashMap<>(); - processorFactories.put("set", (factories, tag, config) -> { - String field = (String) config.remove("field"); - String value = (String) config.remove("value"); - return new Processor() { - @Override - public void execute(IngestDocument ingestDocument) throws Exception { - ingestDocument.setFieldValue(field, value); - } - - @Override - public String getType() { - return "set"; - } - - @Override - public String getTag() { - return tag; - } - }; - }); - processorFactories.put("remove", (factories, tag, config) -> { - String field = (String) config.remove("field"); - return new Processor() { - @Override - public void execute(IngestDocument ingestDocument) throws Exception { - ingestDocument.removeField(field); - } - - @Override - public String getType() { - return "remove"; - } - - @Override - public String getTag() { - return tag; - } - }; - }); - store = new PipelineStore(Settings.EMPTY, processorFactories); - } - - public void testUpdatePipelines() { - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - ClusterState previousClusterState = clusterState; - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.pipelines.size(), is(0)); - - PipelineConfiguration pipeline = new PipelineConfiguration( - "_id",new BytesArray("{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON - ); - IngestMetadata ingestMetadata = new IngestMetadata(Collections.singletonMap("_id", pipeline)); - clusterState = ClusterState.builder(clusterState) - .metaData(MetaData.builder().putCustom(IngestMetadata.TYPE, ingestMetadata)) - .build(); - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.pipelines.size(), is(1)); - assertThat(store.pipelines.get("_id").getId(), equalTo("_id")); - assertThat(store.pipelines.get("_id").getDescription(), nullValue()); - assertThat(store.pipelines.get("_id").getProcessors().size(), equalTo(1)); - assertThat(store.pipelines.get("_id").getProcessors().get(0).getType(), equalTo("set")); - } - - public void testPut() { - String id = "_id"; - Pipeline pipeline = store.get(id); - assertThat(pipeline, nullValue()); - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - - // add a new pipeline: - PutPipelineRequest putRequest = new PutPipelineRequest(id, new BytesArray("{\"processors\": []}"), XContentType.JSON); - ClusterState previousClusterState = clusterState; - clusterState = store.innerPut(putRequest, clusterState); - store.innerUpdatePipelines(previousClusterState, clusterState); - pipeline = store.get(id); - assertThat(pipeline, notNullValue()); - assertThat(pipeline.getId(), equalTo(id)); - assertThat(pipeline.getDescription(), nullValue()); - assertThat(pipeline.getProcessors().size(), equalTo(0)); - - // overwrite existing pipeline: - putRequest = - new PutPipelineRequest(id, new BytesArray("{\"processors\": [], \"description\": \"_description\"}"), XContentType.JSON); - previousClusterState = clusterState; - clusterState = store.innerPut(putRequest, clusterState); - store.innerUpdatePipelines(previousClusterState, clusterState); - pipeline = store.get(id); - assertThat(pipeline, notNullValue()); - assertThat(pipeline.getId(), equalTo(id)); - assertThat(pipeline.getDescription(), equalTo("_description")); - assertThat(pipeline.getProcessors().size(), equalTo(0)); - } - - public void testPutWithErrorResponse() { - String id = "_id"; - Pipeline pipeline = store.get(id); - assertThat(pipeline, nullValue()); - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - - PutPipelineRequest putRequest = - new PutPipelineRequest(id, new BytesArray("{\"description\": \"empty processors\"}"), XContentType.JSON); - ClusterState previousClusterState = clusterState; - clusterState = store.innerPut(putRequest, clusterState); - try { - store.innerUpdatePipelines(previousClusterState, clusterState); - fail("should fail"); - } catch (ElasticsearchParseException e) { - assertThat(e.getMessage(), equalTo("[processors] required property is missing")); - } - pipeline = store.get(id); - assertNotNull(pipeline); - assertThat(pipeline.getId(), equalTo("_id")); - assertThat(pipeline.getDescription(), equalTo("this is a place holder pipeline, because pipeline with" + - " id [_id] could not be loaded")); - assertThat(pipeline.getProcessors().size(), equalTo(1)); - assertNull(pipeline.getProcessors().get(0).getTag()); - assertThat(pipeline.getProcessors().get(0).getType(), equalTo("unknown")); - } - - public void testDelete() { - PipelineConfiguration config = new PipelineConfiguration( - "_id",new BytesArray("{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON - ); - IngestMetadata ingestMetadata = new IngestMetadata(Collections.singletonMap("_id", config)); - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - ClusterState previousClusterState = clusterState; - clusterState = ClusterState.builder(clusterState).metaData(MetaData.builder() - .putCustom(IngestMetadata.TYPE, ingestMetadata)).build(); - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.get("_id"), notNullValue()); - - // Delete pipeline: - DeletePipelineRequest deleteRequest = new DeletePipelineRequest("_id"); - previousClusterState = clusterState; - clusterState = store.innerDelete(deleteRequest, clusterState); - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.get("_id"), nullValue()); - - // Delete existing pipeline: - try { - store.innerDelete(deleteRequest, clusterState); - fail("exception expected"); - } catch (ResourceNotFoundException e) { - assertThat(e.getMessage(), equalTo("pipeline [_id] is missing")); - } - } - - public void testDeleteUsingWildcard() { - HashMap pipelines = new HashMap<>(); - BytesArray definition = new BytesArray( - "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}" - ); - pipelines.put("p1", new PipelineConfiguration("p1", definition, XContentType.JSON)); - pipelines.put("p2", new PipelineConfiguration("p2", definition, XContentType.JSON)); - pipelines.put("q1", new PipelineConfiguration("q1", definition, XContentType.JSON)); - IngestMetadata ingestMetadata = new IngestMetadata(pipelines); - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - ClusterState previousClusterState = clusterState; - clusterState = ClusterState.builder(clusterState).metaData(MetaData.builder() - .putCustom(IngestMetadata.TYPE, ingestMetadata)).build(); - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.get("p1"), notNullValue()); - assertThat(store.get("p2"), notNullValue()); - assertThat(store.get("q1"), notNullValue()); - - // Delete pipeline matching wildcard - DeletePipelineRequest deleteRequest = new DeletePipelineRequest("p*"); - previousClusterState = clusterState; - clusterState = store.innerDelete(deleteRequest, clusterState); - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.get("p1"), nullValue()); - assertThat(store.get("p2"), nullValue()); - assertThat(store.get("q1"), notNullValue()); - - // Exception if we used name which does not exist - try { - store.innerDelete(new DeletePipelineRequest("unknown"), clusterState); - fail("exception expected"); - } catch (ResourceNotFoundException e) { - assertThat(e.getMessage(), equalTo("pipeline [unknown] is missing")); - } - - // match all wildcard works on last remaining pipeline - DeletePipelineRequest matchAllDeleteRequest = new DeletePipelineRequest("*"); - previousClusterState = clusterState; - clusterState = store.innerDelete(matchAllDeleteRequest, clusterState); - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.get("p1"), nullValue()); - assertThat(store.get("p2"), nullValue()); - assertThat(store.get("q1"), nullValue()); - - // match all wildcard does not throw exception if none match - store.innerDelete(matchAllDeleteRequest, clusterState); - } - - public void testDeleteWithExistingUnmatchedPipelines() { - HashMap pipelines = new HashMap<>(); - BytesArray definition = new BytesArray( - "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}" - ); - pipelines.put("p1", new PipelineConfiguration("p1", definition, XContentType.JSON)); - IngestMetadata ingestMetadata = new IngestMetadata(pipelines); - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - ClusterState previousClusterState = clusterState; - clusterState = ClusterState.builder(clusterState).metaData(MetaData.builder() - .putCustom(IngestMetadata.TYPE, ingestMetadata)).build(); - store.innerUpdatePipelines(previousClusterState, clusterState); - assertThat(store.get("p1"), notNullValue()); - - DeletePipelineRequest deleteRequest = new DeletePipelineRequest("z*"); - try { - store.innerDelete(deleteRequest, clusterState); - fail("exception expected"); - } catch (ResourceNotFoundException e) { - assertThat(e.getMessage(), equalTo("pipeline [z*] is missing")); - } - } - - public void testGetPipelines() { - Map configs = new HashMap<>(); - configs.put("_id1", new PipelineConfiguration( - "_id1", new BytesArray("{\"processors\": []}"), XContentType.JSON - )); - configs.put("_id2", new PipelineConfiguration( - "_id2", new BytesArray("{\"processors\": []}"), XContentType.JSON - )); - - assertThat(store.innerGetPipelines(null, "_id1").isEmpty(), is(true)); - - IngestMetadata ingestMetadata = new IngestMetadata(configs); - List pipelines = store.innerGetPipelines(ingestMetadata, "_id1"); - assertThat(pipelines.size(), equalTo(1)); - assertThat(pipelines.get(0).getId(), equalTo("_id1")); - - pipelines = store.innerGetPipelines(ingestMetadata, "_id1", "_id2"); - assertThat(pipelines.size(), equalTo(2)); - assertThat(pipelines.get(0).getId(), equalTo("_id1")); - assertThat(pipelines.get(1).getId(), equalTo("_id2")); - - pipelines = store.innerGetPipelines(ingestMetadata, "_id*"); - pipelines.sort((o1, o2) -> o1.getId().compareTo(o2.getId())); - assertThat(pipelines.size(), equalTo(2)); - assertThat(pipelines.get(0).getId(), equalTo("_id1")); - assertThat(pipelines.get(1).getId(), equalTo("_id2")); - - // get all variants: (no IDs or '*') - pipelines = store.innerGetPipelines(ingestMetadata); - pipelines.sort((o1, o2) -> o1.getId().compareTo(o2.getId())); - assertThat(pipelines.size(), equalTo(2)); - assertThat(pipelines.get(0).getId(), equalTo("_id1")); - assertThat(pipelines.get(1).getId(), equalTo("_id2")); - - pipelines = store.innerGetPipelines(ingestMetadata, "*"); - pipelines.sort((o1, o2) -> o1.getId().compareTo(o2.getId())); - assertThat(pipelines.size(), equalTo(2)); - assertThat(pipelines.get(0).getId(), equalTo("_id1")); - assertThat(pipelines.get(1).getId(), equalTo("_id2")); - } - - public void testCrud() throws Exception { - String id = "_id"; - Pipeline pipeline = store.get(id); - assertThat(pipeline, nullValue()); - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty - - PutPipelineRequest putRequest = new PutPipelineRequest(id, - new BytesArray("{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON); - ClusterState previousClusterState = clusterState; - clusterState = store.innerPut(putRequest, clusterState); - store.innerUpdatePipelines(previousClusterState, clusterState); - pipeline = store.get(id); - assertThat(pipeline, notNullValue()); - assertThat(pipeline.getId(), equalTo(id)); - assertThat(pipeline.getDescription(), nullValue()); - assertThat(pipeline.getProcessors().size(), equalTo(1)); - assertThat(pipeline.getProcessors().get(0).getType(), equalTo("set")); - - DeletePipelineRequest deleteRequest = new DeletePipelineRequest(id); - previousClusterState = clusterState; - clusterState = store.innerDelete(deleteRequest, clusterState); - store.innerUpdatePipelines(previousClusterState, clusterState); - pipeline = store.get(id); - assertThat(pipeline, nullValue()); - } - - public void testValidate() throws Exception { - PutPipelineRequest putRequest = new PutPipelineRequest("_id", new BytesArray( - "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\", \"tag\": \"tag1\"}}," + - "{\"remove\" : {\"field\": \"_field\", \"tag\": \"tag2\"}}]}"), - XContentType.JSON); - - DiscoveryNode node1 = new DiscoveryNode("_node_id1", buildNewFakeTransportAddress(), - emptyMap(), emptySet(), Version.CURRENT); - DiscoveryNode node2 = new DiscoveryNode("_node_id2", buildNewFakeTransportAddress(), - emptyMap(), emptySet(), Version.CURRENT); - Map ingestInfos = new HashMap<>(); - ingestInfos.put(node1, new IngestInfo(Arrays.asList(new ProcessorInfo("set"), new ProcessorInfo("remove")))); - ingestInfos.put(node2, new IngestInfo(Arrays.asList(new ProcessorInfo("set")))); - - ElasticsearchParseException e = - expectThrows(ElasticsearchParseException.class, () -> store.validatePipeline(ingestInfos, putRequest)); - assertEquals("Processor type [remove] is not installed on node [" + node2 + "]", e.getMessage()); - assertEquals("remove", e.getMetadata("es.processor_type").get(0)); - assertEquals("tag2", e.getMetadata("es.processor_tag").get(0)); - - ingestInfos.put(node2, new IngestInfo(Arrays.asList(new ProcessorInfo("set"), new ProcessorInfo("remove")))); - store.validatePipeline(ingestInfos, putRequest); - } - - public void testValidateNoIngestInfo() throws Exception { - PutPipelineRequest putRequest = new PutPipelineRequest("_id", new BytesArray( - "{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON); - Exception e = expectThrows(IllegalStateException.class, () -> store.validatePipeline(Collections.emptyMap(), putRequest)); - assertEquals("Ingest info is empty", e.getMessage()); - - DiscoveryNode discoveryNode = new DiscoveryNode("_node_id", buildNewFakeTransportAddress(), - emptyMap(), emptySet(), Version.CURRENT); - IngestInfo ingestInfo = new IngestInfo(Collections.singletonList(new ProcessorInfo("set"))); - store.validatePipeline(Collections.singletonMap(discoveryNode, ingestInfo), putRequest); - } -} From b08d02e3b73851b8d4d8446b13c521da6e06b4f0 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 20 Aug 2018 23:33:18 -0400 Subject: [PATCH 073/283] Implement CCR licensing (#33002) This commit implements licensing for CCR. CCR will require a platinum license, and administrative endpoints will be disabled when a license is non-compliant. --- x-pack/plugin/ccr/build.gradle | 6 +- .../build.gradle | 39 +++++ .../xpack/ccr/CcrMultiClusterLicenseIT.java | 56 +++++++ .../plugin/ccr/qa/multi-cluster/build.gradle | 2 + .../java/org/elasticsearch/xpack/ccr/Ccr.java | 33 ++++ .../xpack/ccr/CcrLicenseChecker.java | 141 ++++++++++++++++++ .../action/CreateAndFollowIndexAction.java | 82 ++++++---- .../xpack/ccr/action/FollowIndexAction.java | 96 +++++++----- .../ccr/action/TransportCcrStatsAction.java | 22 ++- .../elasticsearch/xpack/ccr/CcrLicenseIT.java | 112 ++++++++++++++ .../org/elasticsearch/xpack/ccr/CcrTests.java | 6 +- .../ccr/IncompatibleLicenseLocalStateCcr.java | 30 ++++ .../xpack/ccr/LocalStateCcr.java | 9 +- .../license/RemoteClusterLicenseChecker.java | 7 +- .../license/XPackLicenseState.java | 38 ++++- .../RemoteClusterLicenseCheckerTests.java | 10 +- .../action/TransportStartDatafeedAction.java | 2 +- 17 files changed, 603 insertions(+), 88 deletions(-) create mode 100644 x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle create mode 100644 x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/src/test/java/org/elasticsearch/xpack/ccr/CcrMultiClusterLicenseIT.java create mode 100644 x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java create mode 100644 x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java create mode 100644 x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IncompatibleLicenseLocalStateCcr.java diff --git a/x-pack/plugin/ccr/build.gradle b/x-pack/plugin/ccr/build.gradle index a430745db06f..8be8a4ba4b93 100644 --- a/x-pack/plugin/ccr/build.gradle +++ b/x-pack/plugin/ccr/build.gradle @@ -33,7 +33,11 @@ task internalClusterTest(type: RandomizedTestingTask, } internalClusterTest.mustRunAfter test -check.dependsOn(internalClusterTest, 'qa:multi-cluster:followClusterTest', 'qa:multi-cluster-with-security:followClusterTest') +check.dependsOn( + internalClusterTest, + 'qa:multi-cluster:followClusterTest', + 'qa:multi-cluster-with-incompatible-license:followClusterTest', + 'qa:multi-cluster-with-security:followClusterTest') dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle new file mode 100644 index 000000000000..e9e57762c37e --- /dev/null +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle @@ -0,0 +1,39 @@ +import org.elasticsearch.gradle.test.RestIntegTestTask + +apply plugin: 'elasticsearch.standalone-test' + +dependencies { + testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile project(path: xpackModule('ccr'), configuration: 'runtime') +} + +task leaderClusterTest(type: RestIntegTestTask) { + mustRunAfter(precommit) +} + +leaderClusterTestCluster { + numNodes = 1 + clusterName = 'leader-cluster' +} + +leaderClusterTestRunner { + systemProperty 'tests.is_leader_cluster', 'true' +} + +task followClusterTest(type: RestIntegTestTask) {} + +followClusterTestCluster { + dependsOn leaderClusterTestRunner + numNodes = 1 + clusterName = 'follow-cluster' + setting 'xpack.license.self_generated.type', 'trial' + setting 'search.remote.leader_cluster.seeds', "\"${-> leaderClusterTest.nodes.get(0).transportUri()}\"" +} + +followClusterTestRunner { + systemProperty 'tests.is_leader_cluster', 'false' + systemProperty 'tests.leader_host', "${-> leaderClusterTest.nodes.get(0).httpUri()}" + finalizedBy 'leaderClusterTestCluster#stop' +} + +test.enabled = false diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/src/test/java/org/elasticsearch/xpack/ccr/CcrMultiClusterLicenseIT.java b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/src/test/java/org/elasticsearch/xpack/ccr/CcrMultiClusterLicenseIT.java new file mode 100644 index 000000000000..06d9f91c7abb --- /dev/null +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/src/test/java/org/elasticsearch/xpack/ccr/CcrMultiClusterLicenseIT.java @@ -0,0 +1,56 @@ +/* + * 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; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.Booleans; +import org.elasticsearch.test.rest.ESRestTestCase; + +import java.util.Locale; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasToString; + +public class CcrMultiClusterLicenseIT extends ESRestTestCase { + + private final boolean runningAgainstLeaderCluster = Booleans.parseBoolean(System.getProperty("tests.is_leader_cluster")); + + @Override + protected boolean preserveClusterUponCompletion() { + return true; + } + + public void testFollowIndex() { + if (runningAgainstLeaderCluster == false) { + final Request request = new Request("POST", "/follower/_ccr/follow"); + request.setJsonEntity("{\"leader_index\": \"leader_cluster:leader\"}"); + assertLicenseIncompatible(request); + } + } + + public void testCreateAndFollowIndex() { + if (runningAgainstLeaderCluster == false) { + final Request request = new Request("POST", "/follower/_ccr/create_and_follow"); + request.setJsonEntity("{\"leader_index\": \"leader_cluster:leader\"}"); + assertLicenseIncompatible(request); + } + } + + private static void assertLicenseIncompatible(final Request request) { + final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request)); + final String expected = String.format( + Locale.ROOT, + "can not fetch remote index [%s] metadata as the remote cluster [%s] is not licensed for [ccr]; " + + "the license mode [BASIC] on cluster [%s] does not enable [ccr]", + "leader_cluster:leader", + "leader_cluster", + "leader_cluster"); + assertThat(e, hasToString(containsString(expected))); + } + +} diff --git a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle index 2ee0f9c1b45c..537584f7b59f 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle +++ b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle @@ -14,6 +14,7 @@ task leaderClusterTest(type: RestIntegTestTask) { leaderClusterTestCluster { numNodes = 1 clusterName = 'leader-cluster' + setting 'xpack.license.self_generated.type', 'trial' } leaderClusterTestRunner { @@ -26,6 +27,7 @@ followClusterTestCluster { dependsOn leaderClusterTestRunner numNodes = 1 clusterName = 'follow-cluster' + setting 'xpack.license.self_generated.type', 'trial' setting 'search.remote.leader_cluster.seeds', "\"${-> leaderClusterTest.nodes.get(0).transportUri()}\"" } 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 dedf3ea0f755..d76af9f3c535 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 @@ -20,6 +20,8 @@ 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.XPackLicenseState; @@ -31,10 +33,12 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ExecutorBuilder; import org.elasticsearch.threadpool.FixedExecutorBuilder; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.ccr.action.CcrStatsAction; import org.elasticsearch.xpack.ccr.action.CreateAndFollowIndexAction; import org.elasticsearch.xpack.ccr.action.FollowIndexAction; @@ -54,8 +58,10 @@ import org.elasticsearch.xpack.core.XPackPlugin; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; @@ -72,15 +78,42 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E private final boolean enabled; private final Settings settings; + private final CcrLicenseChecker ccrLicenseChecker; /** * Construct an instance of the CCR container with the specified settings. * * @param settings the settings */ + @SuppressWarnings("unused") // constructed reflectively by the plugin infrastructure public Ccr(final Settings settings) { + this(settings, new CcrLicenseChecker()); + } + + /** + * Construct an instance of the CCR container with the specified settings and license checker. + * + * @param settings the settings + * @param ccrLicenseChecker the CCR license checker + */ + Ccr(final Settings settings, final CcrLicenseChecker ccrLicenseChecker) { this.settings = settings; this.enabled = CCR_ENABLED_SETTING.get(settings); + this.ccrLicenseChecker = Objects.requireNonNull(ccrLicenseChecker); + } + + @Override + public Collection createComponents( + final Client client, + final ClusterService clusterService, + final ThreadPool threadPool, + final ResourceWatcherService resourceWatcherService, + final ScriptService scriptService, + final NamedXContentRegistry xContentRegistry, + final Environment environment, + final NodeEnvironment nodeEnvironment, + final NamedWriteableRegistry namedWriteableRegistry) { + return Collections.singleton(ccrLicenseChecker); } @Override 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 new file mode 100644 index 000000000000..cefa490f4f7e --- /dev/null +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java @@ -0,0 +1,141 @@ +/* + * 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; + +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.client.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.license.RemoteClusterLicenseChecker; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xpack.core.XPackPlugin; + +import java.util.Collections; +import java.util.Locale; +import java.util.Objects; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +/** + * Encapsulates licensing checking for CCR. + */ +public final class CcrLicenseChecker { + + private final BooleanSupplier isCcrAllowed; + + /** + * Constructs a CCR license checker with the default rule based on the license state for checking if CCR is allowed. + */ + CcrLicenseChecker() { + this(XPackPlugin.getSharedLicenseState()::isCcrAllowed); + } + + /** + * Constructs a CCR license checker with the specified boolean supplier. + * + * @param isCcrAllowed a boolean supplier that should return true if CCR is allowed and false otherwise + */ + CcrLicenseChecker(final BooleanSupplier isCcrAllowed) { + this.isCcrAllowed = Objects.requireNonNull(isCcrAllowed); + } + + /** + * Returns whether or not CCR is allowed. + * + * @return true if CCR is allowed, otherwise false + */ + public boolean isCcrAllowed() { + return isCcrAllowed.getAsBoolean(); + } + + /** + * 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 {@link ActionListener#onFailure(Exception)} method + * of the specified listener 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 listener the listener + * @param leaderIndexMetadataConsumer the leader index metadata consumer + * @param the type of response the listener is waiting for + */ + public void checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( + final Client client, + final String clusterAlias, + final String leaderIndex, + final ActionListener listener, + final Consumer leaderIndexMetadataConsumer) { + // we have to check the license on the remote cluster + new RemoteClusterLicenseChecker(client, XPackLicenseState::isCcrAllowedForOperationMode).checkRemoteClusterLicenses( + Collections.singletonList(clusterAlias), + new ActionListener() { + + @Override + public void onResponse(final RemoteClusterLicenseChecker.LicenseCheck licenseCheck) { + if (licenseCheck.isSuccess()) { + final Client remoteClient = client.getRemoteClusterClient(clusterAlias); + final ClusterStateRequest clusterStateRequest = new ClusterStateRequest(); + clusterStateRequest.clear(); + clusterStateRequest.metaData(true); + clusterStateRequest.indices(leaderIndex); + final ActionListener clusterStateListener = ActionListener.wrap( + r -> { + final ClusterState remoteClusterState = r.getState(); + final IndexMetaData leaderIndexMetadata = + remoteClusterState.getMetaData().index(leaderIndex); + leaderIndexMetadataConsumer.accept(leaderIndexMetadata); + }, + listener::onFailure); + // following an index in remote cluster, so use remote client to fetch leader index metadata + remoteClient.admin().cluster().state(clusterStateRequest, clusterStateListener); + } else { + listener.onFailure(incompatibleRemoteLicense(leaderIndex, licenseCheck)); + } + } + + @Override + public void onFailure(final Exception e) { + listener.onFailure(unknownRemoteLicense(leaderIndex, clusterAlias, e)); + } + + }); + } + + private static ElasticsearchStatusException incompatibleRemoteLicense( + final String leaderIndex, final RemoteClusterLicenseChecker.LicenseCheck licenseCheck) { + final String clusterAlias = licenseCheck.remoteClusterLicenseInfo().clusterAlias(); + final String message = String.format( + Locale.ROOT, + "can not fetch remote index [%s:%s] metadata as the remote cluster [%s] is not licensed for [ccr]; %s", + clusterAlias, + leaderIndex, + clusterAlias, + RemoteClusterLicenseChecker.buildErrorMessage( + "ccr", + licenseCheck.remoteClusterLicenseInfo(), + RemoteClusterLicenseChecker::isLicensePlatinumOrTrial)); + return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST); + } + + private static ElasticsearchStatusException unknownRemoteLicense( + final String leaderIndex, final String clusterAlias, final Exception cause) { + final String message = String.format( + Locale.ROOT, + "can not fetch remote index [%s:%s] metadata as the license state of the remote cluster [%s] could not be determined", + clusterAlias, + leaderIndex, + clusterAlias); + return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST, cause); + } + +} 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 index 1ce915fb1926..2e36bca29322 100644 --- 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 @@ -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 com.carrotsearch.hppc.cursors.ObjectObjectCursor; @@ -11,7 +12,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.ActiveShardsObserver; @@ -36,10 +36,12 @@ 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; @@ -185,16 +187,25 @@ public static class TransportAction extends TransportMasterNodeAction listener) throws Exception { - String[] indices = new String[]{request.getFollowRequest().getLeaderIndex()}; - Map> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false); - if (remoteClusterIndices.containsKey(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { - // Following an index in local cluster, so use local cluster state to fetch leader IndexMetaData: - IndexMetaData leaderIndexMetadata = state.getMetaData().index(request.getFollowRequest().getLeaderIndex()); - createFollowIndex(leaderIndexMetadata, request, listener); + protected void masterOperation( + final Request request, final ClusterState state, final ActionListener listener) throws Exception { + if (ccrLicenseChecker.isCcrAllowed()) { + 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); + } } else { - // Following an index in remote cluster, so use remote client to fetch leader IndexMetaData: - assert remoteClusterIndices.size() == 1; - Map.Entry> entry = remoteClusterIndices.entrySet().iterator().next(); - assert entry.getValue().size() == 1; - String clusterNameAlias = entry.getKey(); - String leaderIndex = entry.getValue().get(0); - - Client remoteClient = client.getRemoteClusterClient(clusterNameAlias); - ClusterStateRequest clusterStateRequest = new ClusterStateRequest(); - clusterStateRequest.clear(); - clusterStateRequest.metaData(true); - clusterStateRequest.indices(leaderIndex); - remoteClient.admin().cluster().state(clusterStateRequest, ActionListener.wrap(r -> { - ClusterState remoteClusterState = r.getState(); - IndexMetaData leaderIndexMetadata = remoteClusterState.getMetaData().index(leaderIndex); - createFollowIndex(leaderIndexMetadata, request, listener); - }, listener::onFailure)); + listener.onFailure(LicenseUtils.newComplianceException("ccr")); } } - private void createFollowIndex(IndexMetaData leaderIndexMetaData, Request request, ActionListener 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, + 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")); 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 index 5f33bcd6fcd0..179c4f1c4389 100644 --- 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 @@ -10,7 +10,6 @@ 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; @@ -40,6 +39,7 @@ 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; @@ -47,6 +47,7 @@ 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; @@ -293,11 +294,19 @@ public static class TransportAction extends HandledTransportAction listener) { - ClusterState localClusterState = clusterService.state(); - IndexMetaData followIndexMetadata = localClusterState.getMetaData().index(request.followerIndex); - - String[] indices = new String[]{request.leaderIndex}; - Map> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false); - if (remoteClusterIndices.containsKey(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { - // Following an index in local cluster, so use local cluster state to fetch leader IndexMetaData: - IndexMetaData leaderIndexMetadata = localClusterState.getMetaData().index(request.leaderIndex); - try { - start(request, null, leaderIndexMetadata, followIndexMetadata, listener); - } catch (IOException e) { - listener.onFailure(e); - return; + protected void doExecute(final Task task, final Request request, final ActionListener listener) { + if (ccrLicenseChecker.isCcrAllowed()) { + 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); } } else { - // Following an index in remote cluster, so use remote client to fetch leader IndexMetaData: - assert remoteClusterIndices.size() == 1; - Map.Entry> entry = remoteClusterIndices.entrySet().iterator().next(); - assert entry.getValue().size() == 1; - String clusterNameAlias = entry.getKey(); - String leaderIndex = entry.getValue().get(0); - - Client remoteClient = client.getRemoteClusterClient(clusterNameAlias); - ClusterStateRequest clusterStateRequest = new ClusterStateRequest(); - clusterStateRequest.clear(); - clusterStateRequest.metaData(true); - clusterStateRequest.indices(leaderIndex); - remoteClient.admin().cluster().state(clusterStateRequest, ActionListener.wrap(r -> { - ClusterState remoteClusterState = r.getState(); - IndexMetaData leaderIndexMetadata = remoteClusterState.getMetaData().index(leaderIndex); - start(request, clusterNameAlias, leaderIndexMetadata, followIndexMetadata, listener); - }, listener::onFailure)); + listener.onFailure(LicenseUtils.newComplianceException("ccr")); + } + } + + 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, + 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 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 8949739e76d9..33873201f5fb 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 @@ -17,14 +17,17 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.LicenseUtils; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.ccr.Ccr; +import org.elasticsearch.xpack.ccr.CcrLicenseChecker; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; @@ -34,6 +37,7 @@ public class TransportCcrStatsAction extends TransportTasksAction< CcrStatsAction.TasksResponse, CcrStatsAction.TaskResponse> { private final IndexNameExpressionResolver resolver; + private final CcrLicenseChecker ccrLicenseChecker; @Inject public TransportCcrStatsAction( @@ -41,7 +45,8 @@ public TransportCcrStatsAction( final ClusterService clusterService, final TransportService transportService, final ActionFilters actionFilters, - final IndexNameExpressionResolver resolver) { + final IndexNameExpressionResolver resolver, + final CcrLicenseChecker ccrLicenseChecker) { super( settings, CcrStatsAction.NAME, @@ -51,7 +56,20 @@ public TransportCcrStatsAction( CcrStatsAction.TasksRequest::new, CcrStatsAction.TasksResponse::new, Ccr.CCR_THREAD_POOL_NAME); - this.resolver = resolver; + this.resolver = Objects.requireNonNull(resolver); + this.ccrLicenseChecker = Objects.requireNonNull(ccrLicenseChecker); + } + + @Override + protected void doExecute( + final Task task, + final CcrStatsAction.TasksRequest request, + final ActionListener listener) { + if (ccrLicenseChecker.isCcrAllowed()) { + super.doExecute(task, request, listener); + } else { + listener.onFailure(LicenseUtils.newComplianceException("ccr")); + } } @Override 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 new file mode 100644 index 000000000000..87772d0c1507 --- /dev/null +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrLicenseIT.java @@ -0,0 +1,112 @@ +/* + * 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; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +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.ccr.action.ShardFollowNodeTask; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class CcrLicenseIT extends ESSingleNodeTestCase { + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(IncompatibleLicenseLocalStateCcr.class); + } + + public void testThatFollowingIndexIsUnavailableWithIncompatibleLicense() throws InterruptedException { + final FollowIndexAction.Request followRequest = getFollowRequest(); + final CountDownLatch latch = new CountDownLatch(1); + client().execute( + FollowIndexAction.INSTANCE, + followRequest, + new ActionListener() { + @Override + public void onResponse(final FollowIndexAction.Response response) { + fail(); + } + + @Override + public void onFailure(final Exception e) { + assertIncompatibleLicense(e); + latch.countDown(); + } + }); + latch.await(); + } + + public void testThatCreateAndFollowingIndexIsUnavailableWithIncompatibleLicense() throws InterruptedException { + final FollowIndexAction.Request followRequest = getFollowRequest(); + final CreateAndFollowIndexAction.Request createAndFollowRequest = new CreateAndFollowIndexAction.Request(followRequest); + final CountDownLatch latch = new CountDownLatch(1); + client().execute( + CreateAndFollowIndexAction.INSTANCE, + createAndFollowRequest, + new ActionListener() { + @Override + public void onResponse(final CreateAndFollowIndexAction.Response response) { + fail(); + } + + @Override + public void onFailure(final Exception e) { + assertIncompatibleLicense(e); + latch.countDown(); + } + }); + latch.await(); + } + + public void testThatCcrStatsAreUnavailableWithIncompatibleLicense() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + client().execute(CcrStatsAction.INSTANCE, new CcrStatsAction.TasksRequest(), new ActionListener() { + @Override + public void onResponse(final CcrStatsAction.TasksResponse tasksResponse) { + fail(); + } + + @Override + public void onFailure(final Exception e) { + assertIncompatibleLicense(e); + latch.countDown(); + } + }); + + latch.await(); + } + + private void assertIncompatibleLicense(final Exception e) { + assertThat(e, instanceOf(ElasticsearchSecurityException.class)); + assertThat(e.getMessage(), equalTo("current license is non-compliant for [ccr]")); + } + + private FollowIndexAction.Request getFollowRequest() { + 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, + TimeValue.timeValueMillis(10), + TimeValue.timeValueMillis(10)); + } + +} diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrTests.java index c9a862954566..0a9ca00590b3 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/CcrTests.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; import org.elasticsearch.Version; @@ -40,9 +41,8 @@ public void testGetEngineFactory() throws IOException { .numberOfShards(1) .numberOfReplicas(0) .build(); - final Ccr ccr = new Ccr(Settings.EMPTY); - final Optional engineFactory = - ccr.getEngineFactory(new IndexSettings(indexMetaData, Settings.EMPTY)); + final Ccr ccr = new Ccr(Settings.EMPTY, new CcrLicenseChecker(() -> true)); + final Optional engineFactory = ccr.getEngineFactory(new IndexSettings(indexMetaData, Settings.EMPTY)); if (value != null && value) { assertTrue(engineFactory.isPresent()); assertThat(engineFactory.get(), instanceOf(FollowingEngineFactory.class)); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IncompatibleLicenseLocalStateCcr.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IncompatibleLicenseLocalStateCcr.java new file mode 100644 index 000000000000..c4b765d3c65e --- /dev/null +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/IncompatibleLicenseLocalStateCcr.java @@ -0,0 +1,30 @@ +/* + * 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; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; + +import java.nio.file.Path; + +public class IncompatibleLicenseLocalStateCcr extends LocalStateCompositeXPackPlugin { + + public IncompatibleLicenseLocalStateCcr(final Settings settings, final Path configPath) throws Exception { + super(settings, configPath); + + plugins.add(new Ccr(settings, new CcrLicenseChecker(() -> false)) { + + @Override + protected XPackLicenseState getLicenseState() { + return IncompatibleLicenseLocalStateCcr.this.getLicenseState(); + } + + }); + } + +} diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/LocalStateCcr.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/LocalStateCcr.java index b705ce53e7e1..cfc30b8dfac4 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/LocalStateCcr.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/LocalStateCcr.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; import org.elasticsearch.common.settings.Settings; @@ -15,14 +16,16 @@ public class LocalStateCcr extends LocalStateCompositeXPackPlugin { public LocalStateCcr(final Settings settings, final Path configPath) throws Exception { super(settings, configPath); - LocalStateCcr thisVar = this; - plugins.add(new Ccr(settings){ + plugins.add(new Ccr(settings, new CcrLicenseChecker(() -> true)) { + @Override protected XPackLicenseState getLicenseState() { - return thisVar.getLicenseState(); + return LocalStateCcr.this.getLicenseState(); } + }); } + } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java index 043224e357b9..e7460d5a2eb3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/RemoteClusterLicenseChecker.java @@ -119,7 +119,7 @@ private LicenseCheck(final RemoteClusterLicenseInfo remoteClusterLicenseInfo) { } private final Client client; - private final Predicate predicate; + private final Predicate predicate; /** * Constructs a remote cluster license checker with the specified license predicate for checking license compatibility. The predicate @@ -128,7 +128,7 @@ private LicenseCheck(final RemoteClusterLicenseInfo remoteClusterLicenseInfo) { * @param client the client * @param predicate the license predicate */ - public RemoteClusterLicenseChecker(final Client client, final Predicate predicate) { + public RemoteClusterLicenseChecker(final Client client, final Predicate predicate) { this.client = client; this.predicate = predicate; } @@ -159,7 +159,8 @@ public void checkRemoteClusterLicenses(final List clusterAliases, final @Override public void onResponse(final XPackInfoResponse xPackInfoResponse) { final XPackInfoResponse.LicenseInfo licenseInfo = xPackInfoResponse.getLicenseInfo(); - if ((licenseInfo.getStatus() == LicenseStatus.ACTIVE) == false || predicate.test(licenseInfo) == false) { + if ((licenseInfo.getStatus() == LicenseStatus.ACTIVE) == false + || predicate.test(License.OperationMode.resolve(licenseInfo.getMode())) == false) { listener.onResponse(LicenseCheck.failure(new RemoteClusterLicenseInfo(clusterAlias.get(), licenseInfo))); return; } 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 722c9d0e711a..39ab0b29834a 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 @@ -514,13 +514,13 @@ public boolean isGraphAllowed() { * {@code false}. */ public boolean isMachineLearningAllowed() { - // status is volatile - Status localStatus = status; - OperationMode operationMode = localStatus.mode; - - boolean licensed = operationMode == OperationMode.TRIAL || operationMode == OperationMode.PLATINUM; + // one-time volatile read as status could be updated on us while performing this check + final Status currentStatus = status; + return currentStatus.active && isMachineLearningAllowedForOperationMode(currentStatus.mode); + } - return licensed && localStatus.active; + public static boolean isMachineLearningAllowedForOperationMode(final OperationMode operationMode) { + return isPlatinumOrTrialOperationMode(operationMode); } /** @@ -612,4 +612,30 @@ public boolean isSecurityEnabled() { final OperationMode mode = status.mode; return mode == OperationMode.TRIAL ? (isSecurityExplicitlyEnabled || isSecurityEnabledByTrialVersion) : isSecurityEnabled; } + + /** + * Determine if cross-cluster replication should be enabled. + *

+ * Cross-cluster replication is only disabled when the license has expired or if the mode is not: + *

    + *
  • {@link OperationMode#PLATINUM}
  • + *
  • {@link OperationMode#TRIAL}
  • + *
+ * + * @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 + final Status currentStatus = status; + return currentStatus.active && isCcrAllowedForOperationMode(currentStatus.mode); + } + + public static boolean isCcrAllowedForOperationMode(final OperationMode operationMode) { + return isPlatinumOrTrialOperationMode(operationMode); + } + + public static boolean isPlatinumOrTrialOperationMode(final OperationMode operationMode) { + return operationMode == OperationMode.PLATINUM || operationMode == OperationMode.TRIAL; + } + } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java index a8627d215420..58ca42c7f681 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/RemoteClusterLicenseCheckerTests.java @@ -118,7 +118,7 @@ public void testCheckRemoteClusterLicensesGivenCompatibleLicenses() { responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); final RemoteClusterLicenseChecker licenseChecker = - new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + new RemoteClusterLicenseChecker(client, XPackLicenseState::isPlatinumOrTrialOperationMode); final AtomicReference licenseCheck = new AtomicReference<>(); licenseChecker.checkRemoteClusterLicenses( @@ -160,7 +160,7 @@ public void testCheckRemoteClusterLicensesGivenIncompatibleLicense() { }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); final RemoteClusterLicenseChecker licenseChecker = - new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + new RemoteClusterLicenseChecker(client, XPackLicenseState::isPlatinumOrTrialOperationMode); final AtomicReference licenseCheck = new AtomicReference<>(); licenseChecker.checkRemoteClusterLicenses( @@ -206,7 +206,7 @@ public void testCheckRemoteClusterLicencesGivenNonExistentCluster() { responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); final RemoteClusterLicenseChecker licenseChecker = - new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + new RemoteClusterLicenseChecker(client, XPackLicenseState::isPlatinumOrTrialOperationMode); final AtomicReference exception = new AtomicReference<>(); licenseChecker.checkRemoteClusterLicenses( @@ -246,7 +246,7 @@ public void testRemoteClusterLicenseCallUsesSystemContext() throws InterruptedEx }).when(client).execute(same(XPackInfoAction.INSTANCE), any(), any()); final RemoteClusterLicenseChecker licenseChecker = - new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + new RemoteClusterLicenseChecker(client, XPackLicenseState::isPlatinumOrTrialOperationMode); final List remoteClusterAliases = Collections.singletonList("valid"); licenseChecker.checkRemoteClusterLicenses( @@ -285,7 +285,7 @@ public void testListenerIsExecutedWithCallingContext() throws InterruptedExcepti responses.add(new XPackInfoResponse(null, createPlatinumLicenseResponse(), null)); final RemoteClusterLicenseChecker licenseChecker = - new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + new RemoteClusterLicenseChecker(client, XPackLicenseState::isPlatinumOrTrialOperationMode); final AtomicBoolean listenerInvoked = new AtomicBoolean(); threadPool.getThreadContext().putHeader("key", "value"); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java index d6ebdd0449e9..f3f4a771443e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java @@ -144,7 +144,7 @@ public void onFailure(Exception e) { if (RemoteClusterLicenseChecker.containsRemoteIndex(datafeed.getIndices())) { final RemoteClusterLicenseChecker remoteClusterLicenseChecker = - new RemoteClusterLicenseChecker(client, RemoteClusterLicenseChecker::isLicensePlatinumOrTrial); + new RemoteClusterLicenseChecker(client, XPackLicenseState::isMachineLearningAllowedForOperationMode); remoteClusterLicenseChecker.checkRemoteClusterLicenses( RemoteClusterLicenseChecker.remoteClusterAliases(datafeed.getIndices()), ActionListener.wrap( From 38886e8f23153f69f6d4801dda121cfa352775f9 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Tue, 21 Aug 2018 16:30:28 +1000 Subject: [PATCH 074/283] [DOCS] Add Kerberos troubleshooting documentation (#32803) This commit adds troubleshooting section for Kerberos. Most of the times the problems seen are caused due to invalid configurations like keytab missing principals or credentials not up to date. Time synchronization is an important part for Kerberos infrastructure and the time skew can cause problems. To debug further documentation explains how to enable JAAS Kerberos login module debugging and Kerberos/SPNEGO debugging by setting JVM system properties. --- .../docs/en/security/troubleshooting.asciidoc | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/x-pack/docs/en/security/troubleshooting.asciidoc b/x-pack/docs/en/security/troubleshooting.asciidoc index d1c88b2786f1..72a05ada2995 100644 --- a/x-pack/docs/en/security/troubleshooting.asciidoc +++ b/x-pack/docs/en/security/troubleshooting.asciidoc @@ -15,6 +15,7 @@ answers for frequently asked questions. * <> * <> * <> +* <> * <> * <> @@ -319,6 +320,77 @@ In this case, you must install the <>. -- +[[trb-security-kerberos]] +=== Common Kerberos exceptions + +*Symptoms:* + +* User authentication fails due to either GSS negotiation failure +or a service login failure (either on the server or in the {es} http client). +Some of the common exceptions are listed below with some tips to help resolve +them. + +*Resolution:* + +`Failure unspecified at GSS-API level (Mechanism level: Checksum failed)`:: ++ +-- + +When you see this error message on the HTTP client side, then it may be +related to an incorrect password. + +When you see this error message in the {es} server logs, then it may be +related to the {es} service keytab. The keytab file is present but it failed +to log in as the user. Please check the keytab expiry. Also check whether the +keytab contain up-to-date credentials; if not, replace them. + +You can use tools like `klist` or `ktab` to list principals inside +the keytab and validate them. You can use `kinit` to see if you can acquire +initial tickets using the keytab. Please check the tools and their documentation +in your Kerberos environment. + +Kerberos depends on proper hostname resolution, so please check your DNS infrastructure. +Incorrect DNS setup, DNS SRV records or configuration for KDC servers in `krb5.conf` +can cause problems with hostname resolution. + +-- + +`Failure unspecified at GSS-API level (Mechanism level: Request is a replay (34))`:: + +`Failure unspecified at GSS-API level (Mechanism level: Clock skew too great (37))`:: ++ +-- + +To prevent replay attacks, Kerberos V5 sets a maximum tolerance for computer +clock synchronization and it is typically 5 minutes. Please check whether +the time on the machines within the domain is in sync. + +-- + +As Kerberos logs are often cryptic in nature and many things can go wrong +as it depends on external services like DNS and NTP. You might +have to enable additional debug logs to determine the root cause of the issue. + +{es} uses a JAAS (Java Authentication and Authorization Service) Kerberos login +module to provide Kerberos support. To enable debug logs on {es} for the login +module use following Kerberos realm setting: +[source,yaml] +---------------- +xpack.security.authc.realms..krb.debug: true +---------------- + +For detailed information, see {ref}/security-settings.html#ref-kerberos-settings[Kerberos realm settings]. + +Sometimes you may need to go deeper to understand the problem during SPNEGO +GSS context negotiation or look at the Kerberos message exchange. To enable +Kerberos/SPNEGO debug logging on JVM, add following JVM system properties: + +`-Dsun.security.krb5.debug=true` + +`-Dsun.security.spnego.debug=true` + +For more information about JVM system properties, see {ref}/jvm-options.html[configuring JVM options]. + [[trb-security-internalserver]] === Internal Server Error in Kibana From b595b1a20c895fac3c1e9a6d0e65c7f02f6c03df Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 21 Aug 2018 17:58:37 +1000 Subject: [PATCH 075/283] Handle 6.4.0+ BWC for Application Privileges (#32929) When the application privileges feature was backported to 6.x/6.4 the BWC version checks on the backport were updated to 6.4.0, but master was not updated. This commit updates all relevant version checks, and adds tests. --- .../security/action/role/PutRoleRequest.java | 4 +- .../action/user/HasPrivilegesRequest.java | 4 +- .../action/user/HasPrivilegesResponse.java | 4 +- .../core/security/authz/RoleDescriptor.java | 4 +- .../action/role/PutRoleRequestTests.java | 6 ++ .../user/HasPrivilegesRequestTests.java | 6 +- .../user/HasPrivilegesResponseTests.java | 87 +++++++++++++++++++ .../security/authz/RoleDescriptorTests.java | 8 +- 8 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java index 96c9c817182f..82863a6e8d15 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequest.java @@ -167,7 +167,7 @@ public void readFrom(StreamInput in) throws IOException { for (int i = 0; i < indicesSize; i++) { indicesPrivileges.add(RoleDescriptor.IndicesPrivileges.createFrom(in)); } - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { applicationPrivileges = in.readList(RoleDescriptor.ApplicationResourcePrivileges::createFrom); conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in); } @@ -185,7 +185,7 @@ public void writeTo(StreamOutput out) throws IOException { for (RoleDescriptor.IndicesPrivileges index : indicesPrivileges) { index.writeTo(out); } - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { out.writeStreamableList(applicationPrivileges); ConditionalClusterPrivileges.writeArray(out, this.conditionalClusterPrivileges); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java index dc43db0115e0..4f5aed012cb1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequest.java @@ -109,7 +109,7 @@ public void readFrom(StreamInput in) throws IOException { for (int i = 0; i < indexSize; i++) { indexPrivileges[i] = RoleDescriptor.IndicesPrivileges.createFrom(in); } - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { applicationPrivileges = in.readArray(ApplicationResourcePrivileges::createFrom, ApplicationResourcePrivileges[]::new); } } @@ -123,7 +123,7 @@ public void writeTo(StreamOutput out) throws IOException { for (RoleDescriptor.IndicesPrivileges priv : indexPrivileges) { priv.writeTo(out); } - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { out.writeArray(ApplicationResourcePrivileges::write, applicationPrivileges); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java index b0711fc1bc12..8cd8b510c649 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java @@ -66,7 +66,7 @@ public void readFrom(StreamInput in) throws IOException { super.readFrom(in); completeMatch = in.readBoolean(); index = readResourcePrivileges(in); - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { application = in.readMap(StreamInput::readString, HasPrivilegesResponse::readResourcePrivileges); } } @@ -87,7 +87,7 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeBoolean(completeMatch); writeResourcePrivileges(out, index); - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { out.writeMap(application, StreamOutput::writeString, HasPrivilegesResponse::writeResourcePrivileges); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 54fd8cc7974b..38bd84888a88 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -252,7 +252,7 @@ public static RoleDescriptor readFrom(StreamInput in) throws IOException { final ApplicationResourcePrivileges[] applicationPrivileges; final ConditionalClusterPrivilege[] conditionalClusterPrivileges; - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { applicationPrivileges = in.readArray(ApplicationResourcePrivileges::createFrom, ApplicationResourcePrivileges[]::new); conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in); } else { @@ -276,7 +276,7 @@ public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws I if (out.getVersion().onOrAfter(Version.V_5_2_0)) { out.writeMap(descriptor.transientMetadata); } - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { out.writeArray(ApplicationResourcePrivileges::write, descriptor.applicationPrivileges); ConditionalClusterPrivileges.writeArray(out, descriptor.getConditionalClusterPrivileges()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java index ae458cbb2f5e..a2b8d40e44c0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java @@ -58,11 +58,17 @@ public void testSerialization() throws IOException { final PutRoleRequest original = buildRandomRequest(); final BytesStreamOutput out = new BytesStreamOutput(); + if (randomBoolean()) { + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_4_0, Version.CURRENT); + logger.info("Serializing with version {}", version); + out.setVersion(version); + } original.writeTo(out); final PutRoleRequest copy = new PutRoleRequest(); final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); StreamInput in = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(out.bytes())), registry); + in.setVersion(out.getVersion()); copy.readFrom(in); assertThat(copy.roleDescriptor(), equalTo(original.roleDescriptor())); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java index f458311e6853..a6706542e961 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesRequestTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; @@ -28,9 +29,10 @@ public class HasPrivilegesRequestTests extends ESTestCase { - public void testSerializationV7() throws IOException { + public void testSerializationV64OrLater() throws IOException { final HasPrivilegesRequest original = randomRequest(); - final HasPrivilegesRequest copy = serializeAndDeserialize(original, Version.V_7_0_0_alpha1); + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_4_0, Version.CURRENT); + final HasPrivilegesRequest copy = serializeAndDeserialize(original, version); assertThat(copy.username(), equalTo(original.username())); assertThat(copy.clusterPrivileges(), equalTo(original.clusterPrivileges())); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java new file mode 100644 index 000000000000..89c58945badd --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java @@ -0,0 +1,87 @@ +/* + * 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.security.action.user; + +import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class HasPrivilegesResponseTests extends ESTestCase { + + public void testSerializationV64OrLater() throws IOException { + final HasPrivilegesResponse original = randomResponse(); + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_4_0, Version.CURRENT); + final HasPrivilegesResponse copy = serializeAndDeserialize(original, version); + + assertThat(copy.isCompleteMatch(), equalTo(original.isCompleteMatch())); +// assertThat(copy.getClusterPrivileges(), equalTo(original.getClusterPrivileges())); + assertThat(copy.getIndexPrivileges(), equalTo(original.getIndexPrivileges())); + assertThat(copy.getApplicationPrivileges(), equalTo(original.getApplicationPrivileges())); + } + + public void testSerializationV63() throws IOException { + final HasPrivilegesResponse original = randomResponse(); + final HasPrivilegesResponse copy = serializeAndDeserialize(original, Version.V_6_3_0); + + assertThat(copy.isCompleteMatch(), equalTo(original.isCompleteMatch())); +// assertThat(copy.getClusterPrivileges(), equalTo(original.getClusterPrivileges())); + assertThat(copy.getIndexPrivileges(), equalTo(original.getIndexPrivileges())); + assertThat(copy.getApplicationPrivileges(), equalTo(Collections.emptyMap())); + } + + private HasPrivilegesResponse serializeAndDeserialize(HasPrivilegesResponse original, Version version) throws IOException { + logger.info("Test serialize/deserialize with version {}", version); + final BytesStreamOutput out = new BytesStreamOutput(); + out.setVersion(version); + original.writeTo(out); + + final HasPrivilegesResponse copy = new HasPrivilegesResponse(); + final StreamInput in = out.bytes().streamInput(); + in.setVersion(version); + copy.readFrom(in); + assertThat(in.read(), equalTo(-1)); + return copy; + } + + private HasPrivilegesResponse randomResponse() { + final Map cluster = new HashMap<>(); + for (String priv : randomArray(1, 6, String[]::new, () -> randomAlphaOfLengthBetween(3, 12))) { + cluster.put(priv, randomBoolean()); + } + final Collection index = randomResourcePrivileges(); + final Map> application = new HashMap<>(); + for (String app : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 6).toLowerCase(Locale.ROOT))) { + application.put(app, randomResourcePrivileges()); + } + return new HasPrivilegesResponse(randomBoolean(), cluster, index, application); + } + + private Collection randomResourcePrivileges() { + final Collection list = new ArrayList<>(); + for (String resource : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(2, 6))) { + final Map privileges = new HashMap<>(); + for (String priv : randomArray(1, 5, String[]::new, () -> randomAlphaOfLengthBetween(3, 8))) { + privileges.put(priv, randomBoolean()); + } + list.add(new HasPrivilegesResponse.ResourcePrivileges(resource, privileges)); + } + return list; + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java index 07686838ad0e..08e4b1123c70 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.authz; +import org.elasticsearch.Version; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -18,10 +19,11 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.XPackClientPlugin; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege; +import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.hamcrest.Matchers; @@ -208,7 +210,10 @@ public void testParse() throws Exception { } public void testSerialization() throws Exception { + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_4_0, null); + logger.info("Testing serialization with version {}", version); BytesStreamOutput output = new BytesStreamOutput(); + output.setVersion(version); RoleDescriptor.IndicesPrivileges[] groups = new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder() .indices("i1", "i2") @@ -235,6 +240,7 @@ public void testSerialization() throws Exception { final NamedWriteableRegistry registry = new NamedWriteableRegistry(new XPackClientPlugin(Settings.EMPTY).getNamedWriteables()); StreamInput streamInput = new NamedWriteableAwareStreamInput(ByteBufferStreamInput.wrap(BytesReference.toBytes(output.bytes())), registry); + streamInput.setVersion(version); final RoleDescriptor serialized = RoleDescriptor.readFrom(streamInput); assertEquals(descriptor, serialized); } From 200078734c5b603907d2ba45d5e001ed787f0506 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 21 Aug 2018 10:13:32 +0200 Subject: [PATCH 076/283] INGEST: Simplify IngestService (#33008) * INGEST: Simplify IngestService * Follow up to #32617 * Flatten redundant inner classes of `IngestService` --- .../elasticsearch/ingest/IngestService.java | 506 ++++++++---------- .../ingest/IngestServiceTests.java | 32 +- 2 files changed, 239 insertions(+), 299 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 4ca06f63991a..4cc4fb69a54f 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -71,26 +71,31 @@ public class IngestService implements ClusterStateApplier { public static final String NOOP_PIPELINE_NAME = "_none"; private final ClusterService clusterService; - private final PipelineStore pipelineStore; - private final PipelineExecutionService pipelineExecutionService; + private final Map processorFactories; + // Ideally this should be in IngestMetadata class, but we don't have the processor factories around there. + // We know of all the processor factories when a node with all its plugin have been initialized. Also some + // processor factories rely on other node services. Custom metadata is statically registered when classes + // are loaded, so in the cluster state we just save the pipeline config and here we keep the actual pipelines around. + private volatile Map pipelines = new HashMap<>(); + private final ThreadPool threadPool; + private final StatsHolder totalStats = new StatsHolder(); + private volatile Map statsHolderPerPipeline = Collections.emptyMap(); public IngestService(ClusterService clusterService, ThreadPool threadPool, Environment env, ScriptService scriptService, AnalysisRegistry analysisRegistry, List ingestPlugins) { this.clusterService = clusterService; - this.pipelineStore = new PipelineStore( - processorFactories( - ingestPlugins, - new Processor.Parameters( - env, scriptService, analysisRegistry, - threadPool.getThreadContext(), threadPool::relativeTimeInMillis, - (delay, command) -> threadPool.schedule( - TimeValue.timeValueMillis(delay), ThreadPool.Names.GENERIC, command - ) + this.processorFactories = processorFactories( + ingestPlugins, + new Processor.Parameters( + env, scriptService, analysisRegistry, + threadPool.getThreadContext(), threadPool::relativeTimeInMillis, + (delay, command) -> threadPool.schedule( + TimeValue.timeValueMillis(delay), ThreadPool.Names.GENERIC, command ) ) ); - this.pipelineExecutionService = new PipelineExecutionService(pipelineStore, threadPool); + this.threadPool = threadPool; } private static Map processorFactories(List ingestPlugins, @@ -114,8 +119,7 @@ public ClusterService getClusterService() { /** * Deletes the pipeline specified by id in the request. */ - public void delete(DeletePipelineRequest request, - ActionListener listener) { + public void delete(DeletePipelineRequest request, ActionListener listener) { clusterService.submitStateUpdateTask("delete-pipeline-" + request.getId(), new AckedClusterStateUpdateTask(request, listener) { @@ -198,32 +202,23 @@ static List innerGetPipelines(IngestMetadata ingestMetada return result; } - public void executeBulkRequest(Iterable> actionRequests, BiConsumer itemFailureHandler, - Consumer completionHandler) { - pipelineExecutionService.executeBulkRequest(actionRequests, itemFailureHandler, completionHandler); - } - - public IngestStats stats() { - return pipelineExecutionService.stats(); - } - /** * Stores the specified pipeline definition in the request. */ public void putPipeline(Map ingestInfos, PutPipelineRequest request, ActionListener listener) throws Exception { - pipelineStore.put(clusterService, ingestInfos, request, listener); + put(clusterService, ingestInfos, request, listener); } /** * Returns the pipeline by the specified id */ public Pipeline getPipeline(String id) { - return pipelineStore.get(id); + return pipelines.get(id); } public Map getProcessorFactories() { - return pipelineStore.getProcessorFactories(); + return processorFactories; } public IngestInfo info() { @@ -236,99 +231,64 @@ public IngestInfo info() { } Map pipelines() { - return pipelineStore.pipelines; - } - - void validatePipeline(Map ingestInfos, PutPipelineRequest request) throws Exception { - pipelineStore.validatePipeline(ingestInfos, request); - } - - void updatePipelineStats(IngestMetadata ingestMetadata) { - pipelineExecutionService.updatePipelineStats(ingestMetadata); + return pipelines; } @Override public void applyClusterState(final ClusterChangedEvent event) { ClusterState state = event.state(); - pipelineStore.innerUpdatePipelines(event.previousState(), state); + innerUpdatePipelines(event.previousState(), state); IngestMetadata ingestMetadata = state.getMetaData().custom(IngestMetadata.TYPE); if (ingestMetadata != null) { - pipelineExecutionService.updatePipelineStats(ingestMetadata); + updatePipelineStats(ingestMetadata); } } - public static final class PipelineStore { - - private final Map processorFactories; - - // Ideally this should be in IngestMetadata class, but we don't have the processor factories around there. - // We know of all the processor factories when a node with all its plugin have been initialized. Also some - // processor factories rely on other node services. Custom metadata is statically registered when classes - // are loaded, so in the cluster state we just save the pipeline config and here we keep the actual pipelines around. - volatile Map pipelines = new HashMap<>(); - - private PipelineStore(Map processorFactories) { - this.processorFactories = processorFactories; - } - - void innerUpdatePipelines(ClusterState previousState, ClusterState state) { - if (state.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) { - return; + private static Pipeline substitutePipeline(String id, ElasticsearchParseException e) { + String tag = e.getHeaderKeys().contains("processor_tag") ? e.getHeader("processor_tag").get(0) : null; + String type = e.getHeaderKeys().contains("processor_type") ? e.getHeader("processor_type").get(0) : "unknown"; + String errorMessage = "pipeline with id [" + id + "] could not be loaded, caused by [" + e.getDetailedMessage() + "]"; + Processor failureProcessor = new AbstractProcessor(tag) { + @Override + public void execute(IngestDocument ingestDocument) { + throw new IllegalStateException(errorMessage); } - IngestMetadata ingestMetadata = state.getMetaData().custom(IngestMetadata.TYPE); - IngestMetadata previousIngestMetadata = previousState.getMetaData().custom(IngestMetadata.TYPE); - if (Objects.equals(ingestMetadata, previousIngestMetadata)) { - return; + @Override + public String getType() { + return type; } + }; + String description = "this is a place holder pipeline, because pipeline with id [" + id + "] could not be loaded"; + return new Pipeline(id, description, null, new CompoundProcessor(failureProcessor)); + } - Map pipelines = new HashMap<>(); - List exceptions = new ArrayList<>(); - for (PipelineConfiguration pipeline : ingestMetadata.getPipelines().values()) { - try { - pipelines.put(pipeline.getId(), Pipeline.create(pipeline.getId(), pipeline.getConfigAsMap(), processorFactories)); - } catch (ElasticsearchParseException e) { - pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), e)); - exceptions.add(e); - } catch (Exception e) { - ElasticsearchParseException parseException = new ElasticsearchParseException( - "Error updating pipeline with id [" + pipeline.getId() + "]", e); - pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), parseException)); - exceptions.add(parseException); - } - } - this.pipelines = Collections.unmodifiableMap(pipelines); - ExceptionsHelper.rethrowAndSuppress(exceptions); + static ClusterState innerPut(PutPipelineRequest request, ClusterState currentState) { + IngestMetadata currentIngestMetadata = currentState.metaData().custom(IngestMetadata.TYPE); + Map pipelines; + if (currentIngestMetadata != null) { + pipelines = new HashMap<>(currentIngestMetadata.getPipelines()); + } else { + pipelines = new HashMap<>(); } - private static Pipeline substitutePipeline(String id, ElasticsearchParseException e) { - String tag = e.getHeaderKeys().contains("processor_tag") ? e.getHeader("processor_tag").get(0) : null; - String type = e.getHeaderKeys().contains("processor_type") ? e.getHeader("processor_type").get(0) : "unknown"; - String errorMessage = "pipeline with id [" + id + "] could not be loaded, caused by [" + e.getDetailedMessage() + "]"; - Processor failureProcessor = new AbstractProcessor(tag) { - @Override - public void execute(IngestDocument ingestDocument) { - throw new IllegalStateException(errorMessage); - } - - @Override - public String getType() { - return type; - } - }; - String description = "this is a place holder pipeline, because pipeline with id [" + id + "] could not be loaded"; - return new Pipeline(id, description, null, new CompoundProcessor(failureProcessor)); - } + pipelines.put(request.getId(), new PipelineConfiguration(request.getId(), request.getSource(), request.getXContentType())); + ClusterState.Builder newState = ClusterState.builder(currentState); + newState.metaData(MetaData.builder(currentState.getMetaData()) + .putCustom(IngestMetadata.TYPE, new IngestMetadata(pipelines)) + .build()); + return newState.build(); + } - /** - * Stores the specified pipeline definition in the request. - */ - public void put(ClusterService clusterService, Map ingestInfos, PutPipelineRequest request, - ActionListener listener) throws Exception { - // validates the pipeline and processor configuration before submitting a cluster update task: - validatePipeline(ingestInfos, request); - clusterService.submitStateUpdateTask("put-pipeline-" + request.getId(), - new AckedClusterStateUpdateTask(request, listener) { + /** + * Stores the specified pipeline definition in the request. + */ + public void put(ClusterService clusterService, Map ingestInfos, PutPipelineRequest request, + ActionListener listener) throws Exception { + // validates the pipeline and processor configuration before submitting a cluster update task: + validatePipeline(ingestInfos, request); + clusterService.submitStateUpdateTask("put-pipeline-" + request.getId(), + new AckedClusterStateUpdateTask(request, listener) { @Override protected AcknowledgedResponse newResponse(boolean acknowledged) { @@ -340,222 +300,202 @@ public ClusterState execute(ClusterState currentState) { return innerPut(request, currentState); } }); - } - - void validatePipeline(Map ingestInfos, PutPipelineRequest request) throws Exception { - if (ingestInfos.isEmpty()) { - throw new IllegalStateException("Ingest info is empty"); - } + } - Map pipelineConfig = XContentHelper.convertToMap(request.getSource(), false, request.getXContentType()).v2(); - Pipeline pipeline = Pipeline.create(request.getId(), pipelineConfig, processorFactories); - List exceptions = new ArrayList<>(); - for (Processor processor : pipeline.flattenAllProcessors()) { - for (Map.Entry entry : ingestInfos.entrySet()) { - if (entry.getValue().containsProcessor(processor.getType()) == false) { - String message = "Processor type [" + processor.getType() + "] is not installed on node [" + entry.getKey() + "]"; - exceptions.add( - ConfigurationUtils.newConfigurationException(processor.getType(), processor.getTag(), null, message) - ); - } - } - } - ExceptionsHelper.rethrowAndSuppress(exceptions); + void validatePipeline(Map ingestInfos, PutPipelineRequest request) throws Exception { + if (ingestInfos.isEmpty()) { + throw new IllegalStateException("Ingest info is empty"); } - static ClusterState innerPut(PutPipelineRequest request, ClusterState currentState) { - IngestMetadata currentIngestMetadata = currentState.metaData().custom(IngestMetadata.TYPE); - Map pipelines; - if (currentIngestMetadata != null) { - pipelines = new HashMap<>(currentIngestMetadata.getPipelines()); - } else { - pipelines = new HashMap<>(); + Map pipelineConfig = XContentHelper.convertToMap(request.getSource(), false, request.getXContentType()).v2(); + Pipeline pipeline = Pipeline.create(request.getId(), pipelineConfig, processorFactories); + List exceptions = new ArrayList<>(); + for (Processor processor : pipeline.flattenAllProcessors()) { + for (Map.Entry entry : ingestInfos.entrySet()) { + if (entry.getValue().containsProcessor(processor.getType()) == false) { + String message = "Processor type [" + processor.getType() + "] is not installed on node [" + entry.getKey() + "]"; + exceptions.add( + ConfigurationUtils.newConfigurationException(processor.getType(), processor.getTag(), null, message) + ); + } } - - pipelines.put(request.getId(), new PipelineConfiguration(request.getId(), request.getSource(), request.getXContentType())); - ClusterState.Builder newState = ClusterState.builder(currentState); - newState.metaData(MetaData.builder(currentState.getMetaData()) - .putCustom(IngestMetadata.TYPE, new IngestMetadata(pipelines)) - .build()); - return newState.build(); - } - - /** - * Returns the pipeline by the specified id - */ - public Pipeline get(String id) { - return pipelines.get(id); - } - - public Map getProcessorFactories() { - return processorFactories; } + ExceptionsHelper.rethrowAndSuppress(exceptions); } - private static final class PipelineExecutionService { - - private final PipelineStore store; - private final ThreadPool threadPool; + public void executeBulkRequest(Iterable> actionRequests, + BiConsumer itemFailureHandler, Consumer completionHandler) { + threadPool.executor(ThreadPool.Names.WRITE).execute(new AbstractRunnable() { - private final StatsHolder totalStats = new StatsHolder(); - private volatile Map statsHolderPerPipeline = Collections.emptyMap(); - - PipelineExecutionService(PipelineStore store, ThreadPool threadPool) { - this.store = store; - this.threadPool = threadPool; - } - - void executeBulkRequest(Iterable> actionRequests, - BiConsumer itemFailureHandler, - Consumer completionHandler) { - threadPool.executor(ThreadPool.Names.WRITE).execute(new AbstractRunnable() { - - @Override - public void onFailure(Exception e) { - completionHandler.accept(e); - } + @Override + public void onFailure(Exception e) { + completionHandler.accept(e); + } - @Override - protected void doRun() { - for (DocWriteRequest actionRequest : actionRequests) { - IndexRequest indexRequest = null; - if (actionRequest instanceof IndexRequest) { - indexRequest = (IndexRequest) actionRequest; - } else if (actionRequest instanceof UpdateRequest) { - UpdateRequest updateRequest = (UpdateRequest) actionRequest; - indexRequest = updateRequest.docAsUpsert() ? updateRequest.doc() : updateRequest.upsertRequest(); - } - if (indexRequest == null) { - continue; - } - String pipeline = indexRequest.getPipeline(); - if (NOOP_PIPELINE_NAME.equals(pipeline) == false) { - try { - innerExecute(indexRequest, getPipeline(indexRequest.getPipeline())); - //this shouldn't be needed here but we do it for consistency with index api - // which requires it to prevent double execution - indexRequest.setPipeline(NOOP_PIPELINE_NAME); - } catch (Exception e) { - itemFailureHandler.accept(indexRequest, e); + @Override + protected void doRun() { + for (DocWriteRequest actionRequest : actionRequests) { + IndexRequest indexRequest = null; + if (actionRequest instanceof IndexRequest) { + indexRequest = (IndexRequest) actionRequest; + } else if (actionRequest instanceof UpdateRequest) { + UpdateRequest updateRequest = (UpdateRequest) actionRequest; + indexRequest = updateRequest.docAsUpsert() ? updateRequest.doc() : updateRequest.upsertRequest(); + } + if (indexRequest == null) { + continue; + } + String pipelineId = indexRequest.getPipeline(); + if (NOOP_PIPELINE_NAME.equals(pipelineId) == false) { + try { + Pipeline pipeline = pipelines.get(pipelineId); + if (pipeline == null) { + throw new IllegalArgumentException("pipeline with id [" + pipelineId + "] does not exist"); } + innerExecute(indexRequest, pipeline); + //this shouldn't be needed here but we do it for consistency with index api + // which requires it to prevent double execution + indexRequest.setPipeline(NOOP_PIPELINE_NAME); + } catch (Exception e) { + itemFailureHandler.accept(indexRequest, e); } } - completionHandler.accept(null); } - }); - } - - IngestStats stats() { - Map statsHolderPerPipeline = this.statsHolderPerPipeline; - - Map statsPerPipeline = new HashMap<>(statsHolderPerPipeline.size()); - for (Map.Entry entry : statsHolderPerPipeline.entrySet()) { - statsPerPipeline.put(entry.getKey(), entry.getValue().createStats()); + completionHandler.accept(null); } + }); + } - return new IngestStats(totalStats.createStats(), statsPerPipeline); + public IngestStats stats() { + Map statsHolderPerPipeline = this.statsHolderPerPipeline; + + Map statsPerPipeline = new HashMap<>(statsHolderPerPipeline.size()); + for (Map.Entry entry : statsHolderPerPipeline.entrySet()) { + statsPerPipeline.put(entry.getKey(), entry.getValue().createStats()); } - void updatePipelineStats(IngestMetadata ingestMetadata) { - boolean changed = false; - Map newStatsPerPipeline = new HashMap<>(statsHolderPerPipeline); - Iterator iterator = newStatsPerPipeline.keySet().iterator(); - while (iterator.hasNext()) { - String pipeline = iterator.next(); - if (ingestMetadata.getPipelines().containsKey(pipeline) == false) { - iterator.remove(); - changed = true; - } - } - for (String pipeline : ingestMetadata.getPipelines().keySet()) { - if (newStatsPerPipeline.containsKey(pipeline) == false) { - newStatsPerPipeline.put(pipeline, new StatsHolder()); - changed = true; - } - } + return new IngestStats(totalStats.createStats(), statsPerPipeline); + } - if (changed) { - statsHolderPerPipeline = Collections.unmodifiableMap(newStatsPerPipeline); + void updatePipelineStats(IngestMetadata ingestMetadata) { + boolean changed = false; + Map newStatsPerPipeline = new HashMap<>(statsHolderPerPipeline); + Iterator iterator = newStatsPerPipeline.keySet().iterator(); + while (iterator.hasNext()) { + String pipeline = iterator.next(); + if (ingestMetadata.getPipelines().containsKey(pipeline) == false) { + iterator.remove(); + changed = true; } } - - private void innerExecute(IndexRequest indexRequest, Pipeline pipeline) throws Exception { - if (pipeline.getProcessors().isEmpty()) { - return; + for (String pipeline : ingestMetadata.getPipelines().keySet()) { + if (newStatsPerPipeline.containsKey(pipeline) == false) { + newStatsPerPipeline.put(pipeline, new StatsHolder()); + changed = true; } + } - long startTimeInNanos = System.nanoTime(); - // the pipeline specific stat holder may not exist and that is fine: - // (e.g. the pipeline may have been removed while we're ingesting a document - Optional pipelineStats = Optional.ofNullable(statsHolderPerPipeline.get(pipeline.getId())); - try { - totalStats.preIngest(); - pipelineStats.ifPresent(StatsHolder::preIngest); - String index = indexRequest.index(); - String type = indexRequest.type(); - String id = indexRequest.id(); - String routing = indexRequest.routing(); - Long version = indexRequest.version(); - VersionType versionType = indexRequest.versionType(); - Map sourceAsMap = indexRequest.sourceAsMap(); - IngestDocument ingestDocument = new IngestDocument(index, type, id, routing, version, versionType, sourceAsMap); - pipeline.execute(ingestDocument); - - Map metadataMap = ingestDocument.extractMetadata(); - //it's fine to set all metadata fields all the time, as ingest document holds their starting values - //before ingestion, which might also get modified during ingestion. - indexRequest.index((String) metadataMap.get(IngestDocument.MetaData.INDEX)); - indexRequest.type((String) metadataMap.get(IngestDocument.MetaData.TYPE)); - indexRequest.id((String) metadataMap.get(IngestDocument.MetaData.ID)); - indexRequest.routing((String) metadataMap.get(IngestDocument.MetaData.ROUTING)); - indexRequest.version(((Number) metadataMap.get(IngestDocument.MetaData.VERSION)).longValue()); - if (metadataMap.get(IngestDocument.MetaData.VERSION_TYPE) != null) { - indexRequest.versionType(VersionType.fromString((String) metadataMap.get(IngestDocument.MetaData.VERSION_TYPE))); - } - indexRequest.source(ingestDocument.getSourceAndMetadata()); - } catch (Exception e) { - totalStats.ingestFailed(); - pipelineStats.ifPresent(StatsHolder::ingestFailed); - throw e; - } finally { - long ingestTimeInMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeInNanos); - totalStats.postIngest(ingestTimeInMillis); - pipelineStats.ifPresent(statsHolder -> statsHolder.postIngest(ingestTimeInMillis)); - } + if (changed) { + statsHolderPerPipeline = Collections.unmodifiableMap(newStatsPerPipeline); + } + } + + private void innerExecute(IndexRequest indexRequest, Pipeline pipeline) throws Exception { + if (pipeline.getProcessors().isEmpty()) { + return; } - private Pipeline getPipeline(String pipelineId) { - Pipeline pipeline = store.get(pipelineId); - if (pipeline == null) { - throw new IllegalArgumentException("pipeline with id [" + pipelineId + "] does not exist"); + long startTimeInNanos = System.nanoTime(); + // the pipeline specific stat holder may not exist and that is fine: + // (e.g. the pipeline may have been removed while we're ingesting a document + Optional pipelineStats = Optional.ofNullable(statsHolderPerPipeline.get(pipeline.getId())); + try { + totalStats.preIngest(); + pipelineStats.ifPresent(StatsHolder::preIngest); + String index = indexRequest.index(); + String type = indexRequest.type(); + String id = indexRequest.id(); + String routing = indexRequest.routing(); + Long version = indexRequest.version(); + VersionType versionType = indexRequest.versionType(); + Map sourceAsMap = indexRequest.sourceAsMap(); + IngestDocument ingestDocument = new IngestDocument(index, type, id, routing, version, versionType, sourceAsMap); + pipeline.execute(ingestDocument); + + Map metadataMap = ingestDocument.extractMetadata(); + //it's fine to set all metadata fields all the time, as ingest document holds their starting values + //before ingestion, which might also get modified during ingestion. + indexRequest.index((String) metadataMap.get(IngestDocument.MetaData.INDEX)); + indexRequest.type((String) metadataMap.get(IngestDocument.MetaData.TYPE)); + indexRequest.id((String) metadataMap.get(IngestDocument.MetaData.ID)); + indexRequest.routing((String) metadataMap.get(IngestDocument.MetaData.ROUTING)); + indexRequest.version(((Number) metadataMap.get(IngestDocument.MetaData.VERSION)).longValue()); + if (metadataMap.get(IngestDocument.MetaData.VERSION_TYPE) != null) { + indexRequest.versionType(VersionType.fromString((String) metadataMap.get(IngestDocument.MetaData.VERSION_TYPE))); } - return pipeline; + indexRequest.source(ingestDocument.getSourceAndMetadata()); + } catch (Exception e) { + totalStats.ingestFailed(); + pipelineStats.ifPresent(StatsHolder::ingestFailed); + throw e; + } finally { + long ingestTimeInMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeInNanos); + totalStats.postIngest(ingestTimeInMillis); + pipelineStats.ifPresent(statsHolder -> statsHolder.postIngest(ingestTimeInMillis)); } + } - private static class StatsHolder { + private void innerUpdatePipelines(ClusterState previousState, ClusterState state) { + if (state.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) { + return; + } - private final MeanMetric ingestMetric = new MeanMetric(); - private final CounterMetric ingestCurrent = new CounterMetric(); - private final CounterMetric ingestFailed = new CounterMetric(); + IngestMetadata ingestMetadata = state.getMetaData().custom(IngestMetadata.TYPE); + IngestMetadata previousIngestMetadata = previousState.getMetaData().custom(IngestMetadata.TYPE); + if (Objects.equals(ingestMetadata, previousIngestMetadata)) { + return; + } - void preIngest() { - ingestCurrent.inc(); + Map pipelines = new HashMap<>(); + List exceptions = new ArrayList<>(); + for (PipelineConfiguration pipeline : ingestMetadata.getPipelines().values()) { + try { + pipelines.put(pipeline.getId(), Pipeline.create(pipeline.getId(), pipeline.getConfigAsMap(), processorFactories)); + } catch (ElasticsearchParseException e) { + pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), e)); + exceptions.add(e); + } catch (Exception e) { + ElasticsearchParseException parseException = new ElasticsearchParseException( + "Error updating pipeline with id [" + pipeline.getId() + "]", e); + pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), parseException)); + exceptions.add(parseException); } + } + this.pipelines = Collections.unmodifiableMap(pipelines); + ExceptionsHelper.rethrowAndSuppress(exceptions); + } - void postIngest(long ingestTimeInMillis) { - ingestCurrent.dec(); - ingestMetric.inc(ingestTimeInMillis); - } + private static class StatsHolder { - void ingestFailed() { - ingestFailed.inc(); - } + private final MeanMetric ingestMetric = new MeanMetric(); + private final CounterMetric ingestCurrent = new CounterMetric(); + private final CounterMetric ingestFailed = new CounterMetric(); - IngestStats.Stats createStats() { - return new IngestStats.Stats(ingestMetric.count(), ingestMetric.sum(), ingestCurrent.count(), ingestFailed.count()); - } + void preIngest() { + ingestCurrent.inc(); + } + void postIngest(long ingestTimeInMillis) { + ingestCurrent.dec(); + ingestMetric.inc(ingestTimeInMillis); } + void ingestFailed() { + ingestFailed.inc(); + } + + IngestStats.Stats createStats() { + return new IngestStats.Stats(ingestMetric.count(), ingestMetric.sum(), ingestCurrent.count(), ingestFailed.count()); + } } } diff --git a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java index 10516dc0d012..83a5bef4de27 100644 --- a/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/IngestServiceTests.java @@ -206,7 +206,7 @@ public void testCrud() throws Exception { PutPipelineRequest putRequest = new PutPipelineRequest(id, new BytesArray("{\"processors\": [{\"set\" : {\"field\": \"_field\", \"value\": \"_value\"}}]}"), XContentType.JSON); ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); pipeline = ingestService.getPipeline(id); assertThat(pipeline, notNullValue()); @@ -233,7 +233,7 @@ public void testPut() { // add a new pipeline: PutPipelineRequest putRequest = new PutPipelineRequest(id, new BytesArray("{\"processors\": []}"), XContentType.JSON); ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); pipeline = ingestService.getPipeline(id); assertThat(pipeline, notNullValue()); @@ -245,7 +245,7 @@ public void testPut() { putRequest = new PutPipelineRequest(id, new BytesArray("{\"processors\": [], \"description\": \"_description\"}"), XContentType.JSON); previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); pipeline = ingestService.getPipeline(id); assertThat(pipeline, notNullValue()); @@ -264,7 +264,7 @@ public void testPutWithErrorResponse() { PutPipelineRequest putRequest = new PutPipelineRequest(id, new BytesArray("{\"description\": \"empty processors\"}"), XContentType.JSON); ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); try { ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); fail("should fail"); @@ -439,7 +439,7 @@ public String getType() { PutPipelineRequest putRequest = new PutPipelineRequest(id, new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final SetOnce failure = new SetOnce<>(); final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline(id); @@ -467,7 +467,7 @@ public void testExecuteBulkPipelineDoesNotExist() { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); BulkRequest bulkRequest = new BulkRequest(); @@ -507,7 +507,7 @@ public void testExecuteSuccess() { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); @SuppressWarnings("unchecked") @@ -525,7 +525,7 @@ public void testExecuteEmptyPipeline() throws Exception { new PutPipelineRequest("_id", new BytesArray("{\"processors\": [], \"description\": \"_description\"}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); @SuppressWarnings("unchecked") @@ -545,7 +545,7 @@ public void testExecutePropagateAllMetaDataUpdates() throws Exception { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final long newVersion = randomLong(); final String versionType = randomFrom("internal", "external", "external_gt", "external_gte"); @@ -587,7 +587,7 @@ public void testExecuteFailure() throws Exception { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); doThrow(new RuntimeException()) @@ -616,7 +616,7 @@ public void testExecuteSuccessWithOnFailure() throws Exception { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); doThrow(new RuntimeException()).when(processor).execute(eqIndexTypeId(emptyMap())); @@ -645,7 +645,7 @@ public void testExecuteFailureWithNestedOnFailure() throws Exception { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final IndexRequest indexRequest = new IndexRequest("_index", "_type", "_id").source(emptyMap()).setPipeline("_id"); doThrow(new RuntimeException()) @@ -700,7 +700,7 @@ public void testBulkRequestExecutionWithFailures() throws Exception { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); @SuppressWarnings("unchecked") @@ -734,7 +734,7 @@ public void testBulkRequestExecution() { new PutPipelineRequest("_id", new BytesArray("{\"processors\": [], \"description\": \"_description\"}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); @SuppressWarnings("unchecked") @@ -762,12 +762,12 @@ public void testStats() { new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); // Start empty ClusterState previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); putRequest = new PutPipelineRequest("_id2", new BytesArray("{\"processors\": [{\"mock\" : {}}]}"), XContentType.JSON); previousClusterState = clusterState; - clusterState = IngestService.PipelineStore.innerPut(putRequest, clusterState); + clusterState = IngestService.innerPut(putRequest, clusterState); ingestService.applyClusterState(new ClusterChangedEvent("", clusterState, previousClusterState)); final Map configurationMap = new HashMap<>(); configurationMap.put("_id1", new PipelineConfiguration("_id1", new BytesArray("{}"), XContentType.JSON)); From 65d4f2787382a028653df2ffdfb25448ef082a77 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 21 Aug 2018 12:05:42 +0300 Subject: [PATCH 077/283] [DOCS] Add configurable password hashing docs (#32849) * [DOCS] Add configurable password hashing docs Adds documentation about the newly introduced configuration option for setting the password hashing algorithm to be used for the users cache and for storing credentials for the native and file realm. --- .../settings/security-hash-settings.asciidoc | 84 +++++++++++++++++++ .../settings/security-settings.asciidoc | 21 +++-- .../docs/en/rest-api/security/users.asciidoc | 0 .../configuring-file-realm.asciidoc | 15 ++-- .../configuring-native-realm.asciidoc | 7 ++ .../authentication/user-cache.asciidoc | 23 +---- 6 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 docs/reference/settings/security-hash-settings.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/users.asciidoc diff --git a/docs/reference/settings/security-hash-settings.asciidoc b/docs/reference/settings/security-hash-settings.asciidoc new file mode 100644 index 000000000000..061ca38d545c --- /dev/null +++ b/docs/reference/settings/security-hash-settings.asciidoc @@ -0,0 +1,84 @@ +[float] +[[hashing-settings]] +==== User cache and password hash algorithms + +Certain realms store user credentials in memory. To limit exposure +to credential theft and mitigate credential compromise, the cache only stores +a hashed version of the user credentials in memory. By default, the user cache +is hashed with a salted `sha-256` hash algorithm. You can use a different +hashing algorithm by setting the `cache.hash_algo` realm settings to any of the +following values: + +[[cache-hash-algo]] +.Cache hash algorithms +|======================= +| Algorithm | | | Description +| `ssha256` | | | Uses a salted `sha-256` algorithm (default). +| `md5` | | | Uses `MD5` algorithm. +| `sha1` | | | Uses `SHA1` algorithm. +| `bcrypt` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds. +| `bcrypt4` | | | Uses `bcrypt` algorithm with salt generated in 16 rounds. +| `bcrypt5` | | | Uses `bcrypt` algorithm with salt generated in 32 rounds. +| `bcrypt6` | | | Uses `bcrypt` algorithm with salt generated in 64 rounds. +| `bcrypt7` | | | Uses `bcrypt` algorithm with salt generated in 128 rounds. +| `bcrypt8` | | | Uses `bcrypt` algorithm with salt generated in 256 rounds. +| `bcrypt9` | | | Uses `bcrypt` algorithm with salt generated in 512 rounds. +| `pbkdf2` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations. +| `pbkdf2_1000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000 iterations. +| `pbkdf2_10000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations. +| `pbkdf2_50000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 50000 iterations. +| `pbkdf2_100000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 100000 iterations. +| `pbkdf2_500000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 500000 iterations. +| `pbkdf2_1000000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000000 iterations. +| `noop`,`clear_text` | | | Doesn't hash the credentials and keeps it in clear text in + memory. CAUTION: keeping clear text is considered insecure + and can be compromised at the OS level (for example through + memory dumps and using `ptrace`). +|======================= + +Likewise, realms that store passwords hash them using cryptographically strong +and password-specific salt values. You can configure the algorithm for password +hashing by setting the `xpack.security.authc.password_hashing.algorithm` setting +to one of the following: + +[[password-hashing-algorithms]] +.Password hashing algorithms +|======================= +| Algorithm | | | Description + +| `bcrypt` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds. (default) +| `bcrypt4` | | | Uses `bcrypt` algorithm with salt generated in 16 rounds. +| `bcrypt5` | | | Uses `bcrypt` algorithm with salt generated in 32 rounds. +| `bcrypt6` | | | Uses `bcrypt` algorithm with salt generated in 64 rounds. +| `bcrypt7` | | | Uses `bcrypt` algorithm with salt generated in 128 rounds. +| `bcrypt8` | | | Uses `bcrypt` algorithm with salt generated in 256 rounds. +| `bcrypt9` | | | Uses `bcrypt` algorithm with salt generated in 512 rounds. +| `bcrypt10` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds. +| `bcrypt11` | | | Uses `bcrypt` algorithm with salt generated in 2048 rounds. +| `bcrypt12` | | | Uses `bcrypt` algorithm with salt generated in 4096 rounds. +| `bcrypt13` | | | Uses `bcrypt` algorithm with salt generated in 8192 rounds. +| `bcrypt14` | | | Uses `bcrypt` algorithm with salt generated in 16384 rounds. +| `pbkdf2` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations. +| `pbkdf2_1000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000 iterations. +| `pbkdf2_10000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations. +| `pbkdf2_50000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 50000 iterations. +| `pbkdf2_100000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 100000 iterations. +| `pbkdf2_500000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 500000 iterations. +| `pbkdf2_1000000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000000 iterations. +|======================= + + diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 9aa4483a8f20..6a7742c4c00d 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -52,6 +52,12 @@ sensitive nature of the information. `xpack.security.authc.accept_default_password`:: In `elasticsearch.yml`, set this to `false` to disable support for the default "changeme" password. +[[password-hashing-settings]] +==== Password hashing settings +`xpack.security.authc.password_hashing.algorithm`:: +Specifies the hashing algorithm that is used for secure user credential storage. +See <>. Defaults to `bcrypt`. + [float] [[anonymous-access-settings]] ==== Anonymous access settings @@ -164,9 +170,8 @@ the standard {es} <>. Defaults to `20m`. cache at any given time. Defaults to 100,000. `cache.hash_algo`:: (Expert Setting) The hashing algorithm that is used for the -in-memory cached user credentials. For possible values, see -{xpack-ref}/controlling-user-cache.html[Cache hash algorithms]. Defaults to -`ssha256`. +in-memory cached user credentials. For possible values, see <>. +Defaults to `ssha256`. [[ref-users-settings]] @@ -190,8 +195,7 @@ Defaults to 100,000. `cache.hash_algo`:: (Expert Setting) The hashing algorithm that is used for the in-memory cached -user credentials. See the {xpack-ref}/controlling-user-cache.html#controlling-user-cache[Cache hash algorithms] table for -all possible values. Defaults to `ssha256`. +user credentials. See <>. Defaults to `ssha256`. [[ref-ldap-settings]] [float] @@ -444,8 +448,7 @@ Defaults to `100000`. `cache.hash_algo`:: (Expert Setting) Specifies the hashing algorithm that is used for the -in-memory cached user credentials. See {xpack-ref}/controlling-user-cache.html#controlling-user-cache[Cache hash algorithms] -table for all possible values. Defaults to `ssha256`. +in-memory cached user credentials. See <>. Defaults to `ssha256`. [[ref-ad-settings]] [float] @@ -684,7 +687,7 @@ Defaults to `100000`. `cache.hash_algo`:: (Expert Setting) Specifies the hashing algorithm that is used for -the in-memory cached user credentials (see {xpack-ref}/controlling-user-cache.html#controlling-user-cache[Cache hash algorithms] table for all possible values). Defaults to `ssha256`. +the in-memory cached user credentials. See <>. Defaults to `ssha256`. `follow_referrals`:: If set to `true` {security} follows referrals returned by the LDAP server. @@ -1335,3 +1338,5 @@ List of IP addresses to allow for this profile. `transport.profiles.$PROFILE.xpack.security.filter.deny`:: List of IP addresses to deny for this profile. + +include::security-hash-settings.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/security/users.asciidoc b/x-pack/docs/en/rest-api/security/users.asciidoc new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/x-pack/docs/en/security/authentication/configuring-file-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-file-realm.asciidoc index 683da76bb7b9..fbf823dae706 100644 --- a/x-pack/docs/en/security/authentication/configuring-file-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-file-realm.asciidoc @@ -55,18 +55,23 @@ cluster. + -- The `users` file stores all the users and their passwords. Each line in the file -represents a single user entry consisting of the username and **hashed** password. +represents a single user entry consisting of the username and **hashed** and **salted** password. [source,bash] ---------------------------------------------------------------------- rdeniro:$2a$10$BBJ/ILiyJ1eBTYoRKxkqbuDEdYECplvxnqQ47uiowE7yGqvCEgj9W alpacino:$2a$10$cNwHnElYiMYZ/T3K4PvzGeJ1KbpXZp2PfoQD.gfaVdImnHOwIuBKS -jacknich:$2a$10$GYUNWyABV/Ols/.bcwxuBuuaQzV6WIauW6RdboojxcixBq3LtI3ni +jacknich:{PBKDF2}50000$z1CLJt0MEFjkIK5iEfgvfnA6xq7lF25uasspsTKSo5Q=$XxCVLbaKDimOdyWgLCLJiyoiWpA/XDMe/xtVgn1r5Sg= ---------------------------------------------------------------------- -{security} uses `bcrypt` to hash the user passwords. +NOTE: To limit exposure to credential theft and mitigate credential compromise, +the file realm stores passwords and caches user credentials according to +security best practices. By default, a hashed version of user credentials +is stored in memory, using a salted `sha-256` hash algorithm and a hashed +version of passwords is stored on disk salted and hashed with the `bcrypt` +hash algorithm. To use different hash algorithms, see <>. -While it is possible to modify this files directly using any standard text +While it is possible to modify the `users` files directly using any standard text editor, we strongly recommend using the <> tool to apply the required changes. @@ -103,4 +108,4 @@ By default, {security} checks these files for changes every 5 seconds. You can change this default behavior by changing the `resource.reload.interval.high` setting in the `elasticsearch.yml` file (as this is a common setting in {es}, changing its value may effect other schedules in the system). --- \ No newline at end of file +-- diff --git a/x-pack/docs/en/security/authentication/configuring-native-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-native-realm.asciidoc index 3cda29c2c711..e9fb9cd0eb8a 100644 --- a/x-pack/docs/en/security/authentication/configuring-native-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-native-realm.asciidoc @@ -34,6 +34,13 @@ xpack: type: native order: 0 ------------------------------------------------------------ + +NOTE: To limit exposure to credential theft and mitigate credential compromise, +the native realm stores passwords and caches user credentials according to +security best practices. By default, a hashed version of user credentials +is stored in memory, using a salted `sha-256` hash algorithm and a hashed +version of passwords is stored on disk salted and hashed with the `bcrypt` +hash algorithm. To use different hash algorithms, see <>. -- . Restart {es}. diff --git a/x-pack/docs/en/security/authentication/user-cache.asciidoc b/x-pack/docs/en/security/authentication/user-cache.asciidoc index 36af070bf067..716e7af99145 100644 --- a/x-pack/docs/en/security/authentication/user-cache.asciidoc +++ b/x-pack/docs/en/security/authentication/user-cache.asciidoc @@ -12,27 +12,8 @@ object to avoid unnecessarily needing to perform role mapping on each request. The cached user credentials are hashed in memory. By default, {security} uses a salted `sha-256` hash algorithm. You can use a different hashing algorithm by -setting the `cache_hash_algo` setting to any of the following: - -[[cache-hash-algo]] -.Cache hash algorithms -|======================= -| Algorithm | | | Description -| `ssha256` | | | Uses a salted `sha-256` algorithm (default). -| `md5` | | | Uses `MD5` algorithm. -| `sha1` | | | Uses `SHA1` algorithm. -| `bcrypt` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds. -| `bcrypt4` | | | Uses `bcrypt` algorithm with salt generated in 16 rounds. -| `bcrypt5` | | | Uses `bcrypt` algorithm with salt generated in 32 rounds. -| `bcrypt6` | | | Uses `bcrypt` algorithm with salt generated in 64 rounds. -| `bcrypt7` | | | Uses `bcrypt` algorithm with salt generated in 128 rounds. -| `bcrypt8` | | | Uses `bcrypt` algorithm with salt generated in 256 rounds. -| `bcrypt9` | | | Uses `bcrypt` algorithm with salt generated in 512 rounds. -| `noop`,`clear_text` | | | Doesn't hash the credentials and keeps it in clear text in - memory. CAUTION: keeping clear text is considered insecure - and can be compromised at the OS level (for example through - memory dumps and using `ptrace`). -|======================= +setting the `cache.hash_algo` realm settings. See +{ref}/security-settings.html#hashing-settings[User cache and password hash algorithms]. [[cache-eviction-api]] ==== Evicting users from the cache From 92076497e507ab27bbb603f8dbcc6c2e236eaafa Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Tue, 21 Aug 2018 12:43:25 +0200 Subject: [PATCH 078/283] Use a dedicated ConnectionManger for RemoteClusterConnection (#32988) This change introduces a dedicated ConnectionManager for every RemoteClusterConnection such that there is not state shared with the TransportService internal ConnectionManager. All connections to a remote cluster are isolated from the TransportService but still uses the TransportService and it's internal properties like the Transport, tracing and internal listener actions on disconnects etc. This allows a remote cluster connection to have a different lifecycle than a local cluster connection, also local discovery code doesn't get notified if there is a disconnect on from a remote cluster and each connection can use it's own dedicated connection profile which allows to have a reduced set of connections per cluster without conflicting with the local cluster. Closes #31835 --- .../transport/ConnectionManager.java | 67 ++++++++------- .../transport/RemoteClusterConnection.java | 86 +++++++++---------- .../transport/RemoteClusterService.java | 8 +- .../transport/TransportService.java | 25 ++++-- .../TransportClientNodesServiceTests.java | 4 +- .../cluster/NodeConnectionsServiceTests.java | 2 +- .../transport/ConnectionManagerTests.java | 6 +- .../transport/RemoteClusterClientTests.java | 18 ++-- .../RemoteClusterConnectionTests.java | 58 +++++++------ .../transport/RemoteClusterServiceTests.java | 19 ++-- .../transport/StubbableConnectionManager.java | 4 +- 11 files changed, 167 insertions(+), 130 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java index 84c337399d5b..0c6be15fd92b 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java @@ -44,6 +44,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -62,6 +63,7 @@ public class ConnectionManager implements Closeable { private final TimeValue pingSchedule; private final ConnectionProfile defaultProfile; private final Lifecycle lifecycle = new Lifecycle(); + private final AtomicBoolean closed = new AtomicBoolean(false); private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); private final DelegatingNodeConnectionListener connectionListener = new DelegatingNodeConnectionListener(); @@ -83,7 +85,9 @@ public ConnectionManager(Settings settings, Transport transport, ThreadPool thre } public void addListener(TransportConnectionListener listener) { - this.connectionListener.listeners.add(listener); + if (connectionListener.listeners.contains(listener) == false) { + this.connectionListener.listeners.add(listener); + } } public void removeListener(TransportConnectionListener listener) { @@ -186,45 +190,50 @@ public void disconnectFromNode(DiscoveryNode node) { } } - public int connectedNodeCount() { + /** + * Returns the number of nodes this manager is connected to. + */ + public int size() { return connectedNodes.size(); } @Override public void close() { - lifecycle.moveToStopped(); - CountDownLatch latch = new CountDownLatch(1); + if (closed.compareAndSet(false, true)) { + lifecycle.moveToStopped(); + CountDownLatch latch = new CountDownLatch(1); - // TODO: Consider moving all read/write lock (in Transport and this class) to the TransportService - threadPool.generic().execute(() -> { - closeLock.writeLock().lock(); - try { - // we are holding a write lock so nobody modifies the connectedNodes / openConnections map - it's safe to first close - // all instances and then clear them maps - Iterator> iterator = connectedNodes.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry next = iterator.next(); - try { - IOUtils.closeWhileHandlingException(next.getValue()); - } finally { - iterator.remove(); + // TODO: Consider moving all read/write lock (in Transport and this class) to the TransportService + threadPool.generic().execute(() -> { + closeLock.writeLock().lock(); + try { + // we are holding a write lock so nobody modifies the connectedNodes / openConnections map - it's safe to first close + // all instances and then clear them maps + Iterator> iterator = connectedNodes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry next = iterator.next(); + try { + IOUtils.closeWhileHandlingException(next.getValue()); + } finally { + iterator.remove(); + } } + } finally { + closeLock.writeLock().unlock(); + latch.countDown(); } - } finally { - closeLock.writeLock().unlock(); - latch.countDown(); - } - }); + }); - try { try { - latch.await(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - // ignore + try { + latch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // ignore + } + } finally { + lifecycle.moveToClosed(); } - } finally { - lifecycle.moveToClosed(); } } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java index 15cf7899dc03..5621b3855781 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java @@ -35,6 +35,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.TimeValue; @@ -80,16 +81,17 @@ final class RemoteClusterConnection extends AbstractComponent implements TransportConnectionListener, Closeable { private final TransportService transportService; + private final ConnectionManager connectionManager; private final ConnectionProfile remoteProfile; private final ConnectedNodes connectedNodes; private final String clusterAlias; private final int maxNumRemoteConnections; private final Predicate nodePredicate; + private final ThreadPool threadPool; private volatile List> seedNodes; private volatile boolean skipUnavailable; private final ConnectHandler connectHandler; private SetOnce remoteClusterName = new SetOnce<>(); - private final ClusterName localClusterName; /** * Creates a new {@link RemoteClusterConnection} @@ -97,13 +99,14 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo * @param clusterAlias the configured alias of the cluster to connect to * @param seedNodes a list of seed nodes to discover eligible nodes from * @param transportService the local nodes transport service + * @param connectionManager the connection manager to use for this remote connection * @param maxNumRemoteConnections the maximum number of connections to the remote cluster * @param nodePredicate a predicate to filter eligible remote nodes to connect to */ RemoteClusterConnection(Settings settings, String clusterAlias, List> seedNodes, - TransportService transportService, int maxNumRemoteConnections, Predicate nodePredicate) { + TransportService transportService, ConnectionManager connectionManager, int maxNumRemoteConnections, + Predicate nodePredicate) { super(settings); - this.localClusterName = ClusterName.CLUSTER_NAME_SETTING.get(settings); this.transportService = transportService; this.maxNumRemoteConnections = maxNumRemoteConnections; this.nodePredicate = nodePredicate; @@ -122,7 +125,11 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo this.skipUnavailable = RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE .getConcreteSettingForNamespace(clusterAlias).get(settings); this.connectHandler = new ConnectHandler(); - transportService.addConnectionListener(this); + this.threadPool = transportService.threadPool; + this.connectionManager = connectionManager; + connectionManager.addListener(this); + // we register the transport service here as a listener to make sure we notify handlers on disconnect etc. + connectionManager.addListener(transportService); } /** @@ -183,8 +190,9 @@ public void ensureConnected(ActionListener voidActionListener) { private void fetchShardsInternal(ClusterSearchShardsRequest searchShardsRequest, final ActionListener listener) { - final DiscoveryNode node = connectedNodes.getAny(); - transportService.sendRequest(node, ClusterSearchShardsAction.NAME, searchShardsRequest, + final DiscoveryNode node = getAnyConnectedNode(); + Transport.Connection connection = connectionManager.getConnection(node); + transportService.sendRequest(connection, ClusterSearchShardsAction.NAME, searchShardsRequest, TransportRequestOptions.EMPTY, new TransportResponseHandler() { @Override @@ -219,12 +227,16 @@ void collectNodes(ActionListener> listener) { request.clear(); request.nodes(true); request.local(true); // run this on the node that gets the request it's as good as any other - final DiscoveryNode node = connectedNodes.getAny(); - transportService.sendRequest(node, ClusterStateAction.NAME, request, TransportRequestOptions.EMPTY, + final DiscoveryNode node = getAnyConnectedNode(); + Transport.Connection connection = connectionManager.getConnection(node); + transportService.sendRequest(connection, ClusterStateAction.NAME, request, TransportRequestOptions.EMPTY, new TransportResponseHandler() { + @Override - public ClusterStateResponse newInstance() { - return new ClusterStateResponse(); + public ClusterStateResponse read(StreamInput in) throws IOException { + ClusterStateResponse response = new ClusterStateResponse(); + response.readFrom(in); + return response; } @Override @@ -261,11 +273,11 @@ public String executor() { * If such node is not connected, the returned connection will be a proxy connection that redirects to it. */ Transport.Connection getConnection(DiscoveryNode remoteClusterNode) { - if (transportService.nodeConnected(remoteClusterNode)) { - return transportService.getConnection(remoteClusterNode); + if (connectionManager.nodeConnected(remoteClusterNode)) { + return connectionManager.getConnection(remoteClusterNode); } - DiscoveryNode discoveryNode = connectedNodes.getAny(); - Transport.Connection connection = transportService.getConnection(discoveryNode); + DiscoveryNode discoveryNode = getAnyConnectedNode(); + Transport.Connection connection = connectionManager.getConnection(discoveryNode); return new ProxyConnection(connection, remoteClusterNode); } @@ -317,33 +329,18 @@ public Version getVersion() { } Transport.Connection getConnection() { - return transportService.getConnection(getAnyConnectedNode()); + return connectionManager.getConnection(getAnyConnectedNode()); } @Override public void close() throws IOException { - connectHandler.close(); + IOUtils.close(connectHandler, connectionManager); } public boolean isClosed() { return connectHandler.isClosed(); } - private ConnectionProfile getRemoteProfile(ClusterName name) { - // we can only compare the cluster name to make a decision if we should use a remote profile - // we can't use a cluster UUID here since we could be connecting to that remote cluster before - // the remote node has joined its cluster and have a cluster UUID. The fact that we just lose a - // rather smallish optimization on the connection layer under certain situations where remote clusters - // have the same name as the local one is minor here. - // the alternative here is to complicate the remote infrastructure to also wait until we formed a cluster, - // gained a cluster UUID and then start connecting etc. we rather use this simplification in order to maintain simplicity - if (this.localClusterName.equals(name)) { - return null; - } else { - return remoteProfile; - } - } - /** * The connect handler manages node discovery and the actual connect to the remote cluster. * There is at most one connect job running at any time. If such a connect job is triggered @@ -387,7 +384,7 @@ private void connect(ActionListener connectListener, boolean forceRun) { final boolean runConnect; final Collection> toNotify; final ActionListener listener = connectListener == null ? null : - ContextPreservingActionListener.wrapPreservingContext(connectListener, transportService.getThreadPool().getThreadContext()); + ContextPreservingActionListener.wrapPreservingContext(connectListener, threadPool.getThreadContext()); synchronized (queue) { if (listener != null && queue.offer(listener) == false) { listener.onFailure(new RejectedExecutionException("connect queue is full")); @@ -415,7 +412,6 @@ private void connect(ActionListener connectListener, boolean forceRun) { } private void forkConnect(final Collection> toNotify) { - ThreadPool threadPool = transportService.getThreadPool(); ExecutorService executor = threadPool.executor(ThreadPool.Names.MANAGEMENT); executor.submit(new AbstractRunnable() { @Override @@ -452,13 +448,13 @@ protected void doRun() { maybeConnect(); } }); - collectRemoteNodes(seedNodes.iterator(), transportService, listener); + collectRemoteNodes(seedNodes.iterator(), transportService, connectionManager, listener); } }); } private void collectRemoteNodes(Iterator> seedNodes, - final TransportService transportService, ActionListener listener) { + final TransportService transportService, final ConnectionManager manager, ActionListener listener) { if (Thread.currentThread().isInterrupted()) { listener.onFailure(new InterruptedException("remote connect thread got interrupted")); } @@ -467,7 +463,7 @@ private void collectRemoteNodes(Iterator> seedNodes, cancellableThreads.executeIO(() -> { final DiscoveryNode seedNode = seedNodes.next().get(); final TransportService.HandshakeResponse handshakeResponse; - Transport.Connection connection = transportService.openConnection(seedNode, + Transport.Connection connection = manager.openConnection(seedNode, ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, null, null)); boolean success = false; try { @@ -482,7 +478,7 @@ private void collectRemoteNodes(Iterator> seedNodes, final DiscoveryNode handshakeNode = handshakeResponse.getDiscoveryNode(); if (nodePredicate.test(handshakeNode) && connectedNodes.size() < maxNumRemoteConnections) { - transportService.connectToNode(handshakeNode, getRemoteProfile(handshakeResponse.getClusterName())); + manager.connectToNode(handshakeNode, remoteProfile, transportService.connectionValidator(handshakeNode)); if (remoteClusterName.get() == null) { assert handshakeResponse.getClusterName().value() != null; remoteClusterName.set(handshakeResponse.getClusterName()); @@ -524,7 +520,7 @@ private void collectRemoteNodes(Iterator> seedNodes, // ISE if we fail the handshake with an version incompatible node if (seedNodes.hasNext()) { logger.debug(() -> new ParameterizedMessage("fetching nodes from external cluster {} failed", clusterAlias), ex); - collectRemoteNodes(seedNodes, transportService, listener); + collectRemoteNodes(seedNodes, transportService, manager, listener); } else { listener.onFailure(ex); } @@ -552,7 +548,6 @@ final boolean isClosed() { /* This class handles the _state response from the remote cluster when sniffing nodes to connect to */ private class SniffClusterStateResponseHandler implements TransportResponseHandler { - private final TransportService transportService; private final Transport.Connection connection; private final ActionListener listener; private final Iterator> seedNodes; @@ -561,7 +556,6 @@ private class SniffClusterStateResponseHandler implements TransportResponseHandl SniffClusterStateResponseHandler(TransportService transportService, Transport.Connection connection, ActionListener listener, Iterator> seedNodes, CancellableThreads cancellableThreads) { - this.transportService = transportService; this.connection = connection; this.listener = listener; this.seedNodes = seedNodes; @@ -592,8 +586,8 @@ public void handleResponse(ClusterStateResponse response) { for (DiscoveryNode node : nodesIter) { if (nodePredicate.test(node) && connectedNodes.size() < maxNumRemoteConnections) { try { - transportService.connectToNode(node, getRemoteProfile(remoteClusterName.get())); // noop if node is - // connected + connectionManager.connectToNode(node, remoteProfile, + transportService.connectionValidator(node)); // noop if node is connected connectedNodes.add(node); } catch (ConnectTransportException | IllegalStateException ex) { // ISE if we fail the handshake with an version incompatible node @@ -609,7 +603,7 @@ public void handleResponse(ClusterStateResponse response) { listener.onFailure(ex); // we got canceled - fail the listener and step out } catch (Exception ex) { logger.warn(() -> new ParameterizedMessage("fetching nodes from external cluster {} failed", clusterAlias), ex); - collectRemoteNodes(seedNodes, transportService, listener); + collectRemoteNodes(seedNodes, transportService, connectionManager, listener); } } @@ -620,7 +614,7 @@ public void handleException(TransportException exp) { IOUtils.closeWhileHandlingException(connection); } finally { // once the connection is closed lets try the next node - collectRemoteNodes(seedNodes, transportService, listener); + collectRemoteNodes(seedNodes, transportService, connectionManager, listener); } } @@ -715,4 +709,8 @@ private synchronized void ensureIteratorAvailable() { } } } + + ConnectionManager getConnectionManager() { + return connectionManager; + } } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index 956a0d94179e..34f13b672874 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.transport; +import java.util.Collection; import java.util.function.Supplier; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -139,7 +140,8 @@ private synchronized void updateRemoteClusters(Map getConnections() { + return remoteClusters.values(); + } } diff --git a/server/src/main/java/org/elasticsearch/transport/TransportService.java b/server/src/main/java/org/elasticsearch/transport/TransportService.java index fb14ae96dbf2..e37ea81211ad 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportService.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportService.java @@ -28,6 +28,7 @@ import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.CheckedBiConsumer; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.component.AbstractLifecycleComponent; @@ -56,6 +57,7 @@ import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Collections; @@ -268,8 +270,9 @@ protected void doStart() { @Override protected void doStop() { try { - connectionManager.close(); - transport.stop(); + IOUtils.close(connectionManager, remoteClusterService, transport::stop); + } catch (IOException e) { + throw new UncheckedIOException(e); } finally { // in case the transport is not connected to our local node (thus cleaned on node disconnect) // make sure to clean any leftover on going handles @@ -306,7 +309,7 @@ public void doRun() { @Override protected void doClose() throws IOException { - IOUtils.close(remoteClusterService, transport); + transport.close(); } /** @@ -364,14 +367,18 @@ public void connectToNode(final DiscoveryNode node, ConnectionProfile connection if (isLocalNode(node)) { return; } + connectionManager.connectToNode(node, connectionProfile, connectionValidator(node)); + } - connectionManager.connectToNode(node, connectionProfile, (newConnection, actualProfile) -> { + public CheckedBiConsumer connectionValidator(DiscoveryNode node) { + return (newConnection, actualProfile) -> { // We don't validate cluster names to allow for CCS connections. final DiscoveryNode remote = handshake(newConnection, actualProfile.getHandshakeTimeout().millis(), cn -> true).discoveryNode; if (validateConnections && node.equals(remote) == false) { throw new ConnectTransportException(node, "handshake failed. unexpected remote node " + remote); } - }); + }; + } /** @@ -562,8 +569,12 @@ public final void sendRequest(final Transport.Conn final TransportRequest request, final TransportRequestOptions options, TransportResponseHandler handler) { - - asyncSender.sendRequest(connection, action, request, options, handler); + try { + asyncSender.sendRequest(connection, action, request, options, handler); + } catch (NodeNotConnectedException ex) { + // the caller might not handle this so we invoke the handler + handler.handleException(ex); + } } /** diff --git a/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java b/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java index afc6b47483eb..9baf2e1c9564 100644 --- a/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java @@ -379,10 +379,10 @@ public void testSniffNodesSamplerClosesConnections() throws Exception { transportClientNodesService.addTransportAddresses(remoteService.getLocalDiscoNode().getAddress()); assertEquals(1, transportClientNodesService.connectedNodes().size()); - assertEquals(1, clientService.connectionManager().connectedNodeCount()); + assertEquals(1, clientService.connectionManager().size()); transportClientNodesService.doSample(); - assertEquals(1, clientService.connectionManager().connectedNodeCount()); + assertEquals(1, clientService.connectionManager().size()); establishedConnections.clear(); handler.blockRequest(); diff --git a/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java index 473f5152e8fd..b8c39c48f887 100644 --- a/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java @@ -134,7 +134,7 @@ public void testReconnect() { private void assertConnectedExactlyToNodes(ClusterState state) { assertConnected(state.nodes()); - assertThat(transportService.getConnectionManager().connectedNodeCount(), equalTo(state.nodes().getSize())); + assertThat(transportService.getConnectionManager().size(), equalTo(state.nodes().getSize())); } private void assertConnected(Iterable nodes) { diff --git a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java index 3c099c32bde2..bff5a2b122d2 100644 --- a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java @@ -159,7 +159,7 @@ public void onNodeDisconnected(DiscoveryNode node) { assertFalse(connection.isClosed()); assertTrue(connectionManager.nodeConnected(node)); assertSame(connection, connectionManager.getConnection(node)); - assertEquals(1, connectionManager.connectedNodeCount()); + assertEquals(1, connectionManager.size()); assertEquals(1, nodeConnectedCount.get()); assertEquals(0, nodeDisconnectedCount.get()); @@ -169,7 +169,7 @@ public void onNodeDisconnected(DiscoveryNode node) { connection.close(); } assertTrue(connection.isClosed()); - assertEquals(0, connectionManager.connectedNodeCount()); + assertEquals(0, connectionManager.size()); assertEquals(1, nodeConnectedCount.get()); assertEquals(1, nodeDisconnectedCount.get()); } @@ -205,7 +205,7 @@ public void onNodeDisconnected(DiscoveryNode node) { assertTrue(connection.isClosed()); assertFalse(connectionManager.nodeConnected(node)); expectThrows(NodeNotConnectedException.class, () -> connectionManager.getConnection(node)); - assertEquals(0, connectionManager.connectedNodeCount()); + assertEquals(0, connectionManager.size()); assertEquals(0, nodeConnectedCount.get()); assertEquals(0, nodeDisconnectedCount.get()); } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java index 8cfec0a07f91..34e22fd20de7 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterClientTests.java @@ -81,13 +81,15 @@ public void testEnsureWeReconnect() throws Exception { try (MockTransportService service = MockTransportService.createNewService(localSettings, Version.CURRENT, threadPool, null)) { Semaphore semaphore = new Semaphore(1); service.start(); - service.addConnectionListener(new TransportConnectionListener() { - @Override - public void onNodeDisconnected(DiscoveryNode node) { - if (remoteNode.equals(node)) { - semaphore.release(); + service.getRemoteClusterService().getConnections().forEach(con -> { + con.getConnectionManager().addListener(new TransportConnectionListener() { + @Override + public void onNodeDisconnected(DiscoveryNode node) { + if (remoteNode.equals(node)) { + semaphore.release(); + } } - } + }); }); // this test is not perfect since we might reconnect concurrently but it will fail most of the time if we don't have // the right calls in place in the RemoteAwareClient @@ -95,7 +97,9 @@ public void onNodeDisconnected(DiscoveryNode node) { for (int i = 0; i < 10; i++) { semaphore.acquire(); try { - service.disconnectFromNode(remoteNode); + service.getRemoteClusterService().getConnections().forEach(con -> { + con.getConnectionManager().disconnectFromNode(remoteNode); + }); semaphore.acquire(); RemoteClusterService remoteClusterService = service.getRemoteClusterService(); Client client = remoteClusterService.getRemoteClusterClient(threadPool, "test"); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index 3d0388ccfad9..e40486d63dc4 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -145,7 +145,7 @@ public static MockTransportService startTransport( } } - public void testLocalProfileIsUsedForLocalCluster() throws Exception { + public void testRemoteProfileIsUsedForLocalCluster() throws Exception { List knownNodes = new CopyOnWriteArrayList<>(); try (MockTransportService seedTransport = startTransport("seed_node", knownNodes, Version.CURRENT); MockTransportService discoverableTransport = startTransport("discoverable_node", knownNodes, Version.CURRENT)) { @@ -159,7 +159,7 @@ public void testLocalProfileIsUsedForLocalCluster() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); @@ -175,9 +175,12 @@ public ClusterSearchShardsResponse read(StreamInput in) throws IOException { }); TransportRequestOptions options = TransportRequestOptions.builder().withType(TransportRequestOptions.Type.BULK) .build(); - service.sendRequest(connection.getConnection(), ClusterSearchShardsAction.NAME, new ClusterSearchShardsRequest(), - options, futureHandler); - futureHandler.txGet(); + IllegalStateException ise = (IllegalStateException) expectThrows(SendRequestTransportException.class, () -> { + service.sendRequest(discoverableNode, + ClusterSearchShardsAction.NAME, new ClusterSearchShardsRequest(), options, futureHandler); + futureHandler.txGet(); + }).getCause(); + assertEquals(ise.getMessage(), "can't select channel size is 0 for types: [RECOVERY, BULK, STATE]"); } } } @@ -199,7 +202,7 @@ public void testRemoteProfileIsUsedForRemoteCluster() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); @@ -255,7 +258,7 @@ public void testDiscoverSingleNode() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); @@ -284,7 +287,7 @@ public void testDiscoverSingleNodeWithIncompatibleSeed() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, Integer.MAX_VALUE, n -> true)) { + seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { updateSeedNodes(connection, seedNodes); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); @@ -311,7 +314,7 @@ public void testNodeDisconnected() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); @@ -360,7 +363,8 @@ public void testFilterDiscoveredNodes() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> n.equals(rejectedNode) == false)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, + n -> n.equals(rejectedNode) == false)) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); if (rejectedNode.equals(seedNode)) { assertFalse(service.nodeConnected(seedNode)); @@ -399,7 +403,7 @@ public void testConnectWithIncompatibleTransports() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { expectThrows(Exception.class, () -> updateSeedNodes(connection, Arrays.asList(() -> seedNode))); assertFalse(service.nodeConnected(seedNode)); assertTrue(connection.assertNoRunningConnections()); @@ -462,7 +466,7 @@ public void close() { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { connection.addConnectedNode(seedNode); for (DiscoveryNode node : knownNodes) { final Transport.Connection transportConnection = connection.getConnection(node); @@ -505,7 +509,7 @@ public void run() { CountDownLatch listenerCalled = new CountDownLatch(1); AtomicReference exceptionReference = new AtomicReference<>(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { ActionListener listener = ActionListener.wrap(x -> { listenerCalled.countDown(); fail("expected exception"); @@ -542,7 +546,7 @@ public void testFetchShards() throws Exception { service.acceptIncomingRequests(); List> nodes = Collections.singletonList(() -> seedNode); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - nodes, service, Integer.MAX_VALUE, n -> true)) { + nodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { if (randomBoolean()) { updateSeedNodes(connection, nodes); } @@ -582,7 +586,7 @@ public void testFetchShardsThreadContextHeader() throws Exception { service.acceptIncomingRequests(); List> nodes = Collections.singletonList(() -> seedNode); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - nodes, service, Integer.MAX_VALUE, n -> true)) { + nodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { SearchRequest request = new SearchRequest("test-index"); Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { @@ -636,7 +640,7 @@ public void testFetchShardsSkipUnavailable() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Collections.singletonList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Collections.singletonList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { SearchRequest request = new SearchRequest("test-index"); ClusterSearchShardsRequest searchShardsRequest = new ClusterSearchShardsRequest("test-index") @@ -746,7 +750,7 @@ public void testTriggerUpdatesConcurrently() throws IOException, InterruptedExce service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, Integer.MAX_VALUE, n -> true)) { + seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { int numThreads = randomIntBetween(4, 10); Thread[] threads = new Thread[numThreads]; CyclicBarrier barrier = new CyclicBarrier(numThreads); @@ -824,7 +828,7 @@ public void testCloseWhileConcurrentlyConnecting() throws IOException, Interrupt service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, Integer.MAX_VALUE, n -> true)) { + seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { int numThreads = randomIntBetween(4, 10); Thread[] threads = new Thread[numThreads]; CyclicBarrier barrier = new CyclicBarrier(numThreads + 1); @@ -913,7 +917,7 @@ public void testGetConnectionInfo() throws Exception { service.acceptIncomingRequests(); int maxNumConnections = randomIntBetween(1, 5); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, maxNumConnections, n -> true)) { + seedNodes, service, service.connectionManager(), maxNumConnections, n -> true)) { // test no nodes connected RemoteConnectionInfo remoteConnectionInfo = assertSerialization(connection.getConnectionInfo()); assertNotNull(remoteConnectionInfo); @@ -1060,7 +1064,7 @@ public void testEnsureConnected() throws IOException, InterruptedException { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { assertFalse(service.nodeConnected(seedNode)); assertFalse(service.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); @@ -1109,7 +1113,7 @@ public void testCollectNodes() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { if (randomBoolean()) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); } @@ -1157,7 +1161,7 @@ public void testConnectedNodesConcurrentAccess() throws IOException, Interrupted service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, Integer.MAX_VALUE, n -> true)) { + seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { final int numGetThreads = randomIntBetween(4, 10); final Thread[] getThreads = new Thread[numGetThreads]; final int numModifyingThreads = randomIntBetween(4, 10); @@ -1247,7 +1251,7 @@ public void testClusterNameIsChecked() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList( () -> seedNode), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList( () -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); assertTrue(service.nodeConnected(seedNode)); assertTrue(service.nodeConnected(discoverableNode)); @@ -1327,7 +1331,7 @@ public void close() { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Collections.singletonList(() -> connectedNode), service, Integer.MAX_VALUE, n -> true)) { + Collections.singletonList(() -> connectedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { connection.addConnectedNode(connectedNode); for (int i = 0; i < 10; i++) { //always a direct connection as the remote node is already connected @@ -1335,9 +1339,9 @@ public void close() { assertSame(seedConnection, remoteConnection); } for (int i = 0; i < 10; i++) { - //always a direct connection as the remote node is already connected + // we don't use the transport service connection manager so we will get a proxy connection for the local node Transport.Connection remoteConnection = connection.getConnection(service.getLocalNode()); - assertThat(remoteConnection, not(instanceOf(RemoteClusterConnection.ProxyConnection.class))); + assertThat(remoteConnection, instanceOf(RemoteClusterConnection.ProxyConnection.class)); assertThat(remoteConnection.getNode(), equalTo(service.getLocalNode())); } for (int i = 0; i < 10; i++) { @@ -1369,7 +1373,7 @@ public void testLazyResolveTransportAddress() throws Exception { return seedNode; }; try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedSupplier), service, Integer.MAX_VALUE, n -> true)) { + Arrays.asList(seedSupplier), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true)) { updateSeedNodes(connection, Arrays.asList(seedSupplier)); // Closing connections leads to RemoteClusterConnection.ConnectHandler.collectRemoteNodes // being called again so we try to resolve the same seed node's host twice diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java index c94b1cbdef54..f1929e72d8b3 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java @@ -283,6 +283,7 @@ public void testRemoteNodeAttribute() throws IOException, InterruptedException { assertTrue(service.isRemoteClusterRegistered("cluster_2")); assertFalse(service.isRemoteNodeConnected("cluster_2", c2N1Node)); assertTrue(service.isRemoteNodeConnected("cluster_2", c2N2Node)); + assertEquals(0, transportService.getConnectionManager().size()); } } } @@ -347,6 +348,7 @@ public void testRemoteNodeRoles() throws IOException, InterruptedException { assertTrue(service.isRemoteClusterRegistered("cluster_2")); assertFalse(service.isRemoteNodeConnected("cluster_2", c2N1Node)); assertTrue(service.isRemoteNodeConnected("cluster_2", c2N2Node)); + assertEquals(0, transportService.getConnectionManager().size()); } } } @@ -579,14 +581,16 @@ public void testCollectSearchShards() throws Exception { } CountDownLatch disconnectedLatch = new CountDownLatch(numDisconnectedClusters); - service.addConnectionListener(new TransportConnectionListener() { - @Override - public void onNodeDisconnected(DiscoveryNode node) { - if (disconnectedNodes.remove(node)) { - disconnectedLatch.countDown(); + for (RemoteClusterConnection connection : remoteClusterService.getConnections()) { + connection.getConnectionManager().addListener(new TransportConnectionListener() { + @Override + public void onNodeDisconnected(DiscoveryNode node) { + if (disconnectedNodes.remove(node)) { + disconnectedLatch.countDown(); + } } - } - }); + }); + } for (DiscoveryNode disconnectedNode : disconnectedNodes) { service.addFailToSendNoConnectRule(disconnectedNode.getAddress()); @@ -664,6 +668,7 @@ public void onNodeDisconnected(DiscoveryNode node) { assertTrue(shardsResponse != ClusterSearchShardsResponse.EMPTY); } } + assertEquals(0, service.getConnectionManager().size()); } } } finally { diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java index 486ccc805d05..012369feb839 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java @@ -120,8 +120,8 @@ public void disconnectFromNode(DiscoveryNode node) { } @Override - public int connectedNodeCount() { - return delegate.connectedNodeCount(); + public int size() { + return delegate.size(); } @Override From 38bdf9ce3238c5399e18b33efba34df3a269e17c Mon Sep 17 00:00:00 2001 From: markharwood Date: Tue, 21 Aug 2018 13:29:18 +0100 Subject: [PATCH 079/283] HLRC GraphClient and associated tests (#32366) GraphClient for the high level REST client and associated tests. Part of #29827 work --- .../org/elasticsearch/client/GraphClient.java | 63 +++++ .../client/RequestConverters.java | 8 + .../client/RestHighLevelClient.java | 11 + .../org/elasticsearch/client/GraphIT.java | 139 +++++++++++ .../client/RequestConvertersTests.java | 32 +++ .../client/RestHighLevelClientTests.java | 1 + .../documentation/GraphDocumentationIT.java | 125 ++++++++++ .../high-level/graph/explore.asciidoc | 53 ++++ .../high-level/supported-apis.asciidoc | 8 + .../xpack/core/graph/action/Connection.java | 141 ----------- .../core/graph/action/GraphExploreAction.java | 1 + .../action/GraphExploreRequestBuilder.java | 3 + .../action/TransportGraphExploreAction.java | 18 +- .../graph/rest/action/RestGraphAction.java | 8 +- .../xpack/graph/test/GraphTests.java | 10 +- .../authz/IndicesAndAliasesResolver.java | 2 +- .../authz/IndicesAndAliasesResolverTests.java | 2 +- .../protocol/xpack/graph/Connection.java | 229 ++++++++++++++++++ .../xpack/graph}/GraphExploreRequest.java | 150 ++++++++---- .../xpack/graph}/GraphExploreResponse.java | 103 ++++++-- .../protocol/xpack/graph}/Hop.java | 41 +++- .../protocol/xpack/graph}/Vertex.java | 99 +++++++- .../protocol/xpack/graph}/VertexRequest.java | 64 ++++- .../protocol/xpack/graph/package-info.java | 24 ++ .../graph/GraphExploreResponseTests.java | 131 ++++++++++ 25 files changed, 1219 insertions(+), 247 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/GraphClient.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/GraphIT.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/GraphDocumentationIT.java create mode 100644 docs/java-rest/high-level/graph/explore.asciidoc delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Connection.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java rename x-pack/{plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action => protocol/src/main/java/org/elasticsearch/protocol/xpack/graph}/GraphExploreRequest.java (67%) rename x-pack/{plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action => protocol/src/main/java/org/elasticsearch/protocol/xpack/graph}/GraphExploreResponse.java (55%) rename x-pack/{plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action => protocol/src/main/java/org/elasticsearch/protocol/xpack/graph}/Hop.java (73%) rename x-pack/{plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action => protocol/src/main/java/org/elasticsearch/protocol/xpack/graph}/Vertex.java (59%) rename x-pack/{plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action => protocol/src/main/java/org/elasticsearch/protocol/xpack/graph}/VertexRequest.java (70%) create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/GraphClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/GraphClient.java new file mode 100644 index 000000000000..293105f5abeb --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/GraphClient.java @@ -0,0 +1,63 @@ +/* + * 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; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreResponse; + +import java.io.IOException; + +import static java.util.Collections.emptySet; + + +public class GraphClient { + private final RestHighLevelClient restHighLevelClient; + + GraphClient(RestHighLevelClient restHighLevelClient) { + this.restHighLevelClient = restHighLevelClient; + } + + /** + * Executes an exploration request using the Graph API. + * + * See Graph API + * on elastic.co. + */ + public final GraphExploreResponse explore(GraphExploreRequest graphExploreRequest, + RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(graphExploreRequest, RequestConverters::xPackGraphExplore, + options, GraphExploreResponse::fromXContext, emptySet()); + } + + /** + * Asynchronously executes an exploration request using the Graph API. + * + * See Graph API + * on elastic.co. + */ + public final void exploreAsync(GraphExploreRequest graphExploreRequest, + RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(graphExploreRequest, RequestConverters::xPackGraphExplore, + options, GraphExploreResponse::fromXContext, listener, emptySet()); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 0e5fce5b2272..9dd316a0fb02 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -114,6 +114,7 @@ import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; import org.elasticsearch.script.mustache.SearchTemplateRequest; @@ -1124,6 +1125,13 @@ static Request xPackInfo(XPackInfoRequest infoRequest) { return request; } + static Request xPackGraphExplore(GraphExploreRequest exploreRequest) throws IOException { + String endpoint = endpoint(exploreRequest.indices(), exploreRequest.types(), "_xpack/graph/_explore"); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + request.setEntity(createEntity(exploreRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request xPackWatcherPutWatch(PutWatchRequest putWatchRequest) { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index e705ca12806b..2b71b5be59d2 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -209,6 +209,7 @@ public class RestHighLevelClient implements Closeable { private final TasksClient tasksClient = new TasksClient(this); private final XPackClient xPackClient = new XPackClient(this); private final WatcherClient watcherClient = new WatcherClient(this); + private final GraphClient graphClient = new GraphClient(this); private final LicenseClient licenseClient = new LicenseClient(this); private final MigrationClient migrationClient = new MigrationClient(this); private final MachineLearningClient machineLearningClient = new MachineLearningClient(this); @@ -324,6 +325,16 @@ public final XPackClient xpack() { * Watcher APIs on elastic.co for more information. */ public WatcherClient watcher() { return watcherClient; } + + /** + * Provides methods for accessing the Elastic Licensed Graph explore API that + * is shipped with the default distribution of Elasticsearch. All of + * these APIs will 404 if run against the OSS distribution of Elasticsearch. + *

+ * See the + * Graph API on elastic.co for more information. + */ + public GraphClient graph() { return graphClient; } /** * Provides methods for accessing the Elastic Licensed Licensing APIs that diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/GraphIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/GraphIT.java new file mode 100644 index 000000000000..4376b47d737b --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/GraphIT.java @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client; + +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.action.ShardOperationFailedException; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreResponse; +import org.elasticsearch.protocol.xpack.graph.Hop; +import org.elasticsearch.protocol.xpack.graph.Vertex; +import org.elasticsearch.protocol.xpack.graph.VertexRequest; +import org.hamcrest.Matchers; +import org.junit.Before; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class GraphIT extends ESRestHighLevelClientTestCase { + + @Before + public void indexDocuments() throws IOException { + // Create chain of doc IDs across indices 1->2->3 + Request doc1 = new Request(HttpPut.METHOD_NAME, "/index1/type/1"); + doc1.setJsonEntity("{ \"num\":[1], \"const\":\"start\"}"); + client().performRequest(doc1); + + Request doc2 = new Request(HttpPut.METHOD_NAME, "/index2/type/1"); + doc2.setJsonEntity("{\"num\":[1,2], \"const\":\"foo\"}"); + client().performRequest(doc2); + + Request doc3 = new Request(HttpPut.METHOD_NAME, "/index2/type/2"); + doc3.setJsonEntity("{\"num\":[2,3], \"const\":\"foo\"}"); + client().performRequest(doc3); + + Request doc4 = new Request(HttpPut.METHOD_NAME, "/index_no_field_data/type/2"); + doc4.setJsonEntity("{\"num\":\"string\", \"const\":\"foo\"}"); + client().performRequest(doc4); + + Request doc5 = new Request(HttpPut.METHOD_NAME, "/index_no_field_data/type/2"); + doc5.setJsonEntity("{\"num\":[2,4], \"const\":\"foo\"}"); + client().performRequest(doc5); + + + client().performRequest(new Request(HttpPost.METHOD_NAME, "/_refresh")); + } + + public void testCleanExplore() throws Exception { + GraphExploreRequest graphExploreRequest = new GraphExploreRequest(); + graphExploreRequest.indices("index1", "index2"); + graphExploreRequest.useSignificance(false); + int numHops = 3; + for (int i = 0; i < numHops; i++) { + QueryBuilder guidingQuery = null; + if (i == 0) { + guidingQuery = new TermQueryBuilder("const.keyword", "start"); + } else if (randomBoolean()){ + guidingQuery = new TermQueryBuilder("const.keyword", "foo"); + } + Hop hop = graphExploreRequest.createNextHop(guidingQuery); + VertexRequest vr = hop.addVertexRequest("num"); + vr.minDocCount(1); + } + Map expectedTermsAndDepths = new HashMap<>(); + expectedTermsAndDepths.put("1", 0); + expectedTermsAndDepths.put("2", 1); + expectedTermsAndDepths.put("3", 2); + + GraphExploreResponse exploreResponse = highLevelClient().graph().explore(graphExploreRequest, RequestOptions.DEFAULT); + Map actualTermsAndDepths = new HashMap<>(); + Collection v = exploreResponse.getVertices(); + for (Vertex vertex : v) { + actualTermsAndDepths.put(vertex.getTerm(), vertex.getHopDepth()); + } + assertEquals(expectedTermsAndDepths, actualTermsAndDepths); + assertThat(exploreResponse.isTimedOut(), Matchers.is(false)); + ShardOperationFailedException[] failures = exploreResponse.getShardFailures(); + assertThat(failures.length, Matchers.equalTo(0)); + + } + + public void testBadExplore() throws Exception { + //Explore indices where lack of fielddata=true on one index leads to partial failures + GraphExploreRequest graphExploreRequest = new GraphExploreRequest(); + graphExploreRequest.indices("index1", "index2", "index_no_field_data"); + graphExploreRequest.useSignificance(false); + int numHops = 3; + for (int i = 0; i < numHops; i++) { + QueryBuilder guidingQuery = null; + if (i == 0) { + guidingQuery = new TermQueryBuilder("const.keyword", "start"); + } else if (randomBoolean()){ + guidingQuery = new TermQueryBuilder("const.keyword", "foo"); + } + Hop hop = graphExploreRequest.createNextHop(guidingQuery); + VertexRequest vr = hop.addVertexRequest("num"); + vr.minDocCount(1); + } + Map expectedTermsAndDepths = new HashMap<>(); + expectedTermsAndDepths.put("1", 0); + expectedTermsAndDepths.put("2", 1); + expectedTermsAndDepths.put("3", 2); + + GraphExploreResponse exploreResponse = highLevelClient().graph().explore(graphExploreRequest, RequestOptions.DEFAULT); + Map actualTermsAndDepths = new HashMap<>(); + Collection v = exploreResponse.getVertices(); + for (Vertex vertex : v) { + actualTermsAndDepths.put(vertex.getTerm(), vertex.getHopDepth()); + } + assertEquals(expectedTermsAndDepths, actualTermsAndDepths); + assertThat(exploreResponse.isTimedOut(), Matchers.is(false)); + ShardOperationFailedException[] failures = exploreResponse.getShardFailures(); + assertThat(failures.length, Matchers.equalTo(1)); + assertTrue(failures[0].reason().contains("Fielddata is disabled")); + + } + + +} \ No newline at end of file diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 47195f0bb2ab..ebabb8f95b59 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -118,6 +118,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.RandomCreateIndexGenerator; import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.rankeval.PrecisionAtK; @@ -128,6 +129,8 @@ import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.Hop; import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.rest.action.search.RestSearchAction; @@ -2598,6 +2601,35 @@ public void testXPackPutWatch() throws Exception { request.getEntity().writeTo(bos); assertThat(bos.toString("UTF-8"), is(body)); } + + public void testGraphExplore() throws Exception { + Map expectedParams = new HashMap<>(); + + GraphExploreRequest graphExploreRequest = new GraphExploreRequest(); + graphExploreRequest.sampleDiversityField("diversity"); + graphExploreRequest.indices("index1", "index2"); + graphExploreRequest.types("type1", "type2"); + int timeout = randomIntBetween(10000, 20000); + graphExploreRequest.timeout(TimeValue.timeValueMillis(timeout)); + graphExploreRequest.useSignificance(randomBoolean()); + int numHops = randomIntBetween(1, 5); + for (int i = 0; i < numHops; i++) { + int hopNumber = i + 1; + QueryBuilder guidingQuery = null; + if (randomBoolean()) { + guidingQuery = new TermQueryBuilder("field" + hopNumber, "value" + hopNumber); + } + Hop hop = graphExploreRequest.createNextHop(guidingQuery); + hop.addVertexRequest("field" + hopNumber); + hop.getVertexRequest(0).addInclude("value" + hopNumber, hopNumber); + } + Request request = RequestConverters.xPackGraphExplore(graphExploreRequest); + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/index1,index2/type1,type2/_xpack/graph/_explore", request.getEndpoint()); + assertEquals(expectedParams, request.getParameters()); + assertThat(request.getEntity().getContentType().getValue(), is(XContentType.JSON.mediaTypeWithoutParameters())); + assertToXContentBody(graphExploreRequest, request.getEntity()); + } public void testXPackDeleteWatch() { DeleteWatchRequest deleteWatchRequest = new DeleteWatchRequest(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index b5d8dbb628eb..1036b79a4a5d 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -758,6 +758,7 @@ public void testApiNamingConventions() throws Exception { apiName.startsWith("license.") == false && apiName.startsWith("machine_learning.") == false && apiName.startsWith("watcher.") == false && + apiName.startsWith("graph.") == false && apiName.startsWith("migration.") == false) { apiNotFound.add(apiName); } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/GraphDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/GraphDocumentationIT.java new file mode 100644 index 000000000000..8631e18b8739 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/GraphDocumentationIT.java @@ -0,0 +1,125 @@ +/* + * 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.documentation; + +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.client.ESRestHighLevelClientTestCase; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.common.SuppressForbidden; +import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.protocol.xpack.graph.Connection; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreResponse; +import org.elasticsearch.protocol.xpack.graph.Hop; +import org.elasticsearch.protocol.xpack.graph.Vertex; +import org.elasticsearch.protocol.xpack.graph.VertexRequest; +import org.junit.Before; + +import java.io.IOException; +import java.util.Collection; + +public class GraphDocumentationIT extends ESRestHighLevelClientTestCase { + + + @Before + public void indexDocuments() throws IOException { + // Create chain of doc IDs across indices 1->2->3 + Request doc1 = new Request(HttpPut.METHOD_NAME, "/index1/type/1"); + doc1.setJsonEntity("{ \"participants\":[1,2], \"text\":\"let's start projectx\", \"attachment_md5\":\"324FHDGHFDG4564\"}"); + client().performRequest(doc1); + + Request doc2 = new Request(HttpPut.METHOD_NAME, "/index2/type/2"); + doc2.setJsonEntity("{\"participants\":[2,3,4], \"text\":\"got something you both may be interested in\"}"); + client().performRequest(doc2); + + client().performRequest(new Request(HttpPost.METHOD_NAME, "/_refresh")); + } + + @SuppressForbidden(reason = "system out is ok for a documentation example") + public void testExplore() throws Exception { + RestHighLevelClient client = highLevelClient(); + + + + // tag::x-pack-graph-explore-request + GraphExploreRequest request = new GraphExploreRequest(); + request.indices("index1", "index2"); + request.useSignificance(false); + TermQueryBuilder startingQuery = new TermQueryBuilder("text", "projectx"); + + Hop hop1 = request.createNextHop(startingQuery); // <1> + VertexRequest people = hop1.addVertexRequest("participants"); // <2> + people.minDocCount(1); + VertexRequest files = hop1.addVertexRequest("attachment_md5"); + files.minDocCount(1); + + Hop hop2 = request.createNextHop(null); // <3> + VertexRequest vr2 = hop2.addVertexRequest("participants"); + vr2.minDocCount(5); + + GraphExploreResponse exploreResponse = client.graph().explore(request, RequestOptions.DEFAULT); // <4> + // end::x-pack-graph-explore-request + + + // tag::x-pack-graph-explore-response + Collection v = exploreResponse.getVertices(); + Collection c = exploreResponse.getConnections(); + for (Vertex vertex : v) { + System.out.println(vertex.getField() + ":" + vertex.getTerm() + // <1> + " discovered at hop depth " + vertex.getHopDepth()); + } + for (Connection link : c) { + System.out.println(link.getFrom() + " -> " + link.getTo() // <2> + + " evidenced by " + link.getDocCount() + " docs"); + } + // end::x-pack-graph-explore-response + + + Collection initialVertices = exploreResponse.getVertices(); + + // tag::x-pack-graph-explore-expand + GraphExploreRequest expandRequest = new GraphExploreRequest(); + expandRequest.indices("index1", "index2"); + + + Hop expandHop1 = expandRequest.createNextHop(null); // <1> + VertexRequest fromPeople = expandHop1.addVertexRequest("participants"); // <2> + for (Vertex vertex : initialVertices) { + if (vertex.getField().equals("participants")) { + fromPeople.addInclude(vertex.getTerm(), 1f); + } + } + + Hop expandHop2 = expandRequest.createNextHop(null); + VertexRequest newPeople = expandHop2.addVertexRequest("participants"); // <3> + for (Vertex vertex : initialVertices) { + if (vertex.getField().equals("participants")) { + newPeople.addExclude(vertex.getTerm()); + } + } + + GraphExploreResponse expandResponse = client.graph().explore(expandRequest, RequestOptions.DEFAULT); + // end::x-pack-graph-explore-expand + + } + +} diff --git a/docs/java-rest/high-level/graph/explore.asciidoc b/docs/java-rest/high-level/graph/explore.asciidoc new file mode 100644 index 000000000000..f2718209f4b9 --- /dev/null +++ b/docs/java-rest/high-level/graph/explore.asciidoc @@ -0,0 +1,53 @@ +[[java-rest-high-x-pack-graph-explore]] +=== X-Pack Graph explore API + +[[java-rest-high-x-pack-graph-explore-execution]] +==== Initial request + +Graph queries are executed using the `explore()` method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/GraphDocumentationIT.java[x-pack-graph-explore-request] +-------------------------------------------------- +<1> In this example we seed the exploration with a query to find messages mentioning the mysterious `projectx` +<2> What we want to discover in these messages are the ids of `participants` in the communications and the md5 hashes +of any attached files. In each case, we want to find people or files that have had at least one document connecting them +to projectx. +<3> The next "hop" in the graph exploration is to find the people who have shared several messages with the people or files +discovered in the previous hop (the projectx conspirators). The `minDocCount` control is used here to ensure the people +discovered have had at least 5 communications with projectx entities. Note we could also supply a "guiding query" here e.g. a +date range to consider only recent communications but we pass null to consider all connections. +<4> Finally we call the graph explore API with the GraphExploreRequest object. + + +==== Response + +Graph responses consist of Vertex and Connection objects (aka "nodes" and "edges" respectively): + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/GraphDocumentationIT.java[x-pack-graph-explore-response] +-------------------------------------------------- +<1> Each Vertex is a unique term (a combination of fieldname and term value). The "hopDepth" property tells us at which point in the +requested exploration this term was first discovered. +<2> Each Connection is a pair of Vertex objects and includes a docCount property telling us how many times these two +Vertex terms have been sighted together + + +[[java-rest-high-x-pack-graph-expand-execution]] +==== Expanding a client-side Graph + +Typically once an application has rendered an initial GraphExploreResponse as a collection of vertices and connecting lines (graph visualization toolkits such as D3, sigma.js or Keylines help here) the next step a user may want to do is "expand". This involves finding new vertices that might be connected to the existing ones currently shown. + +To do this we use the same `explore` method but our request contains details about which vertices to expand from and which vertices to avoid re-discovering. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/GraphDocumentationIT.java[x-pack-graph-explore-expand] +-------------------------------------------------- +<1> Unlike the initial request we do not need to pass a starting query +<2> In the first hop which represents our "from" vertices we explicitly list the terms that we already have on-screen and want to expand by using the `addInclude` filter. +We can supply a boost for those terms that are considered more important to follow than others but here we select a common value of 1 for all. +<3> When defining the second hop which represents the "to" vertices we hope to discover we explicitly list the terms that we already know about using the `addExclude` filter + diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 96e93ba204cf..b3de26e56bd0 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -231,3 +231,11 @@ The Java High Level REST Client supports the following Watcher APIs: include::watcher/put-watch.asciidoc[] include::watcher/delete-watch.asciidoc[] + +== Graph APIs + +The Java High Level REST Client supports the following Graph APIs: + +* <> + +include::graph/explore.asciidoc[] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Connection.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Connection.java deleted file mode 100644 index f3d928964491..000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Connection.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.core.graph.action; - -import com.carrotsearch.hppc.ObjectIntHashMap; - -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.ToXContent.Params; -import org.elasticsearch.xpack.core.graph.action.Vertex.VertexId; - -import java.io.IOException; -import java.util.Map; - -/** - * A Connection links exactly two {@link Vertex} objects. The basis of a - * connection is one or more documents have been found that contain - * this pair of terms and the strength of the connection is recorded - * as a weight. - */ -public class Connection { - Vertex from; - Vertex to; - double weight; - long docCount; - - public Connection(Vertex from, Vertex to, double weight, long docCount) { - this.from = from; - this.to = to; - this.weight = weight; - this.docCount = docCount; - } - - void readFrom(StreamInput in, Map vertices) throws IOException { - from = vertices.get(new VertexId(in.readString(), in.readString())); - to = vertices.get(new VertexId(in.readString(), in.readString())); - weight = in.readDouble(); - docCount = in.readVLong(); - } - - Connection() { - } - - void writeTo(StreamOutput out) throws IOException { - out.writeString(from.getField()); - out.writeString(from.getTerm()); - out.writeString(to.getField()); - out.writeString(to.getTerm()); - out.writeDouble(weight); - out.writeVLong(docCount); - } - - public ConnectionId getId() { - return new ConnectionId(from.getId(), to.getId()); - } - - public Vertex getFrom() { - return from; - } - - public Vertex getTo() { - return to; - } - - /** - * @return a measure of the relative connectedness between a pair of {@link Vertex} objects - */ - public double getWeight() { - return weight; - } - - /** - * @return the number of documents in the sampled set that contained this - * pair of {@link Vertex} objects. - */ - public long getDocCount() { - return docCount; - } - - void toXContent(XContentBuilder builder, Params params, ObjectIntHashMap vertexNumbers) throws IOException { - builder.field("source", vertexNumbers.get(from)); - builder.field("target", vertexNumbers.get(to)); - builder.field("weight", weight); - builder.field("doc_count", docCount); - } - - /** - * An identifier (implements hashcode and equals) that represents a - * unique key for a {@link Connection} - */ - public static class ConnectionId { - private final VertexId source; - private final VertexId target; - - public ConnectionId(VertexId source, VertexId target) { - this.source = source; - this.target = target; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - ConnectionId vertexId = (ConnectionId) o; - - if (source != null ? !source.equals(vertexId.source) : vertexId.source != null) - return false; - if (target != null ? !target.equals(vertexId.target) : vertexId.target != null) - return false; - - return true; - } - - @Override - public int hashCode() { - int result = source != null ? source.hashCode() : 0; - result = 31 * result + (target != null ? target.hashCode() : 0); - return result; - } - - public VertexId getSource() { - return source; - } - - public VertexId getTarget() { - return target; - } - - @Override - public String toString() { - return getSource() + "->" + getTarget(); - } - } -} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreAction.java index 5503eb692558..e4fd8d043510 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreAction.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.graph.action; import org.elasticsearch.action.Action; +import org.elasticsearch.protocol.xpack.graph.GraphExploreResponse; public class GraphExploreAction extends Action { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreRequestBuilder.java index d5e756f78a20..37456f234648 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreRequestBuilder.java @@ -11,6 +11,9 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreResponse; +import org.elasticsearch.protocol.xpack.graph.Hop; import org.elasticsearch.search.aggregations.bucket.sampler.SamplerAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.significant.SignificantTerms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregator; diff --git a/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/action/TransportGraphExploreAction.java b/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/action/TransportGraphExploreAction.java index 4eb136040e98..25f2511fbc0a 100644 --- a/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/action/TransportGraphExploreAction.java +++ b/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/action/TransportGraphExploreAction.java @@ -24,6 +24,15 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.license.LicenseUtils; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.protocol.xpack.graph.Connection; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreResponse; +import org.elasticsearch.protocol.xpack.graph.Hop; +import org.elasticsearch.protocol.xpack.graph.Vertex; +import org.elasticsearch.protocol.xpack.graph.VertexRequest; +import org.elasticsearch.protocol.xpack.graph.Connection.ConnectionId; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest.TermBoost; +import org.elasticsearch.protocol.xpack.graph.Vertex.VertexId; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.bucket.sampler.DiversifiedAggregationBuilder; @@ -39,16 +48,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.XPackField; -import org.elasticsearch.xpack.core.graph.action.Connection; -import org.elasticsearch.xpack.core.graph.action.Connection.ConnectionId; import org.elasticsearch.xpack.core.graph.action.GraphExploreAction; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest.TermBoost; -import org.elasticsearch.xpack.core.graph.action.GraphExploreResponse; -import org.elasticsearch.xpack.core.graph.action.Hop; -import org.elasticsearch.xpack.core.graph.action.Vertex; -import org.elasticsearch.xpack.core.graph.action.Vertex.VertexId; -import org.elasticsearch.xpack.core.graph.action.VertexRequest; import java.util.ArrayList; import java.util.HashMap; diff --git a/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/rest/action/RestGraphAction.java b/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/rest/action/RestGraphAction.java index 3f11d0c72bd4..778eb261a070 100644 --- a/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/rest/action/RestGraphAction.java +++ b/x-pack/plugin/graph/src/main/java/org/elasticsearch/xpack/graph/rest/action/RestGraphAction.java @@ -12,14 +12,14 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.Hop; +import org.elasticsearch.protocol.xpack.graph.VertexRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest.TermBoost; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.xpack.core.XPackClient; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest.TermBoost; -import org.elasticsearch.xpack.core.graph.action.Hop; -import org.elasticsearch.xpack.core.graph.action.VertexRequest; import org.elasticsearch.xpack.core.rest.XPackRestHandler; import java.io.IOException; diff --git a/x-pack/plugin/graph/src/test/java/org/elasticsearch/xpack/graph/test/GraphTests.java b/x-pack/plugin/graph/src/test/java/org/elasticsearch/xpack/graph/test/GraphTests.java index 5bebef3d2d48..a58d8e8a8b0c 100644 --- a/x-pack/plugin/graph/src/test/java/org/elasticsearch/xpack/graph/test/GraphTests.java +++ b/x-pack/plugin/graph/src/test/java/org/elasticsearch/xpack/graph/test/GraphTests.java @@ -17,6 +17,11 @@ import org.elasticsearch.index.query.ScriptQueryBuilder; import org.elasticsearch.license.LicenseService; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; +import org.elasticsearch.protocol.xpack.graph.GraphExploreResponse; +import org.elasticsearch.protocol.xpack.graph.Hop; +import org.elasticsearch.protocol.xpack.graph.Vertex; +import org.elasticsearch.protocol.xpack.graph.VertexRequest; import org.elasticsearch.script.MockScriptPlugin; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; @@ -24,12 +29,7 @@ import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.graph.Graph; import org.elasticsearch.xpack.core.graph.action.GraphExploreAction; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest; import org.elasticsearch.xpack.core.graph.action.GraphExploreRequestBuilder; -import org.elasticsearch.xpack.core.graph.action.GraphExploreResponse; -import org.elasticsearch.xpack.core.graph.action.Hop; -import org.elasticsearch.xpack.core.graph.action.Vertex; -import org.elasticsearch.xpack.core.graph.action.VertexRequest; import java.util.Collection; import java.util.Collections; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index c388fd5627c3..3cf2034cc74b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -25,9 +25,9 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import java.util.ArrayList; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index cf9c09759ea0..c0867875b018 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -46,12 +46,12 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest; import org.elasticsearch.search.internal.ShardSearchTransportRequest; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.graph.action.GraphExploreAction; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest; import org.elasticsearch.xpack.core.security.authc.DefaultAuthenticationFailureHandler; import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java new file mode 100644 index 000000000000..455434f7ac4a --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java @@ -0,0 +1,229 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.graph; + +import com.carrotsearch.hppc.ObjectIntHashMap; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContent.Params; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.graph.Vertex.VertexId; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * A Connection links exactly two {@link Vertex} objects. The basis of a + * connection is one or more documents have been found that contain + * this pair of terms and the strength of the connection is recorded + * as a weight. + */ +public class Connection { + private Vertex from; + private Vertex to; + private double weight; + private long docCount; + + public Connection(Vertex from, Vertex to, double weight, long docCount) { + this.from = from; + this.to = to; + this.weight = weight; + this.docCount = docCount; + } + + public Connection(StreamInput in, Map vertices) throws IOException { + from = vertices.get(new VertexId(in.readString(), in.readString())); + to = vertices.get(new VertexId(in.readString(), in.readString())); + weight = in.readDouble(); + docCount = in.readVLong(); + } + + Connection() { + } + + void writeTo(StreamOutput out) throws IOException { + out.writeString(from.getField()); + out.writeString(from.getTerm()); + out.writeString(to.getField()); + out.writeString(to.getTerm()); + out.writeDouble(weight); + out.writeVLong(docCount); + } + + public ConnectionId getId() { + return new ConnectionId(from.getId(), to.getId()); + } + + public Vertex getFrom() { + return from; + } + + public Vertex getTo() { + return to; + } + + /** + * @return a measure of the relative connectedness between a pair of {@link Vertex} objects + */ + public double getWeight() { + return weight; + } + + /** + * @return the number of documents in the sampled set that contained this + * pair of {@link Vertex} objects. + */ + public long getDocCount() { + return docCount; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Connection other = (Connection) obj; + return docCount == other.docCount && + weight == other.weight && + Objects.equals(to, other.to) && + Objects.equals(from, other.from); + } + + @Override + public int hashCode() { + return Objects.hash(docCount, weight, from, to); + } + + + private static final ParseField SOURCE = new ParseField("source"); + private static final ParseField TARGET = new ParseField("target"); + private static final ParseField WEIGHT = new ParseField("weight"); + private static final ParseField DOC_COUNT = new ParseField("doc_count"); + + + void toXContent(XContentBuilder builder, Params params, ObjectIntHashMap vertexNumbers) throws IOException { + builder.field(SOURCE.getPreferredName(), vertexNumbers.get(from)); + builder.field(TARGET.getPreferredName(), vertexNumbers.get(to)); + builder.field(WEIGHT.getPreferredName(), weight); + builder.field(DOC_COUNT.getPreferredName(), docCount); + } + + //When deserializing from XContent we need to wait for all vertices to be loaded before + // Connection objects can be created that reference them. This class provides the interim + // state for connections. + static class UnresolvedConnection { + int fromIndex; + int toIndex; + double weight; + long docCount; + UnresolvedConnection(int fromIndex, int toIndex, double weight, long docCount) { + super(); + this.fromIndex = fromIndex; + this.toIndex = toIndex; + this.weight = weight; + this.docCount = docCount; + } + public Connection resolve(List vertices) { + return new Connection(vertices.get(fromIndex), vertices.get(toIndex), weight, docCount); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "ConnectionParser", true, + args -> { + int source = (Integer) args[0]; + int target = (Integer) args[1]; + double weight = (Double) args[2]; + long docCount = (Long) args[3]; + return new UnresolvedConnection(source, target, weight, docCount); + }); + + static { + PARSER.declareInt(constructorArg(), SOURCE); + PARSER.declareInt(constructorArg(), TARGET); + PARSER.declareDouble(constructorArg(), WEIGHT); + PARSER.declareLong(constructorArg(), DOC_COUNT); + } + static UnresolvedConnection fromXContent(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + } + + + /** + * An identifier (implements hashcode and equals) that represents a + * unique key for a {@link Connection} + */ + public static class ConnectionId { + private final VertexId source; + private final VertexId target; + + public ConnectionId(VertexId source, VertexId target) { + this.source = source; + this.target = target; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + ConnectionId vertexId = (ConnectionId) o; + + if (source != null ? !source.equals(vertexId.source) : vertexId.source != null) + return false; + if (target != null ? !target.equals(vertexId.target) : vertexId.target != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = source != null ? source.hashCode() : 0; + result = 31 * result + (target != null ? target.hashCode() : 0); + return result; + } + + public VertexId getSource() { + return source; + } + + public VertexId getTarget() { + return target; + } + + @Override + public String toString() { + return getSource() + "->" + getTarget(); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreRequest.java similarity index 67% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreRequest.java rename to x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreRequest.java index e44f9f760375..495ea5fd28ac 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreRequest.java @@ -1,9 +1,22 @@ /* - * 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. + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ -package org.elasticsearch.xpack.core.graph.action; +package org.elasticsearch.protocol.xpack.graph; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; @@ -14,6 +27,8 @@ 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.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.aggregations.bucket.sampler.SamplerAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.significant.SignificantTerms; @@ -29,7 +44,7 @@ * Holds the criteria required to guide the exploration of connected terms which * can be returned as a graph. */ -public class GraphExploreRequest extends ActionRequest implements IndicesRequest.Replaceable { +public class GraphExploreRequest extends ActionRequest implements IndicesRequest.Replaceable, ToXContentObject { public static final String NO_HOPS_ERROR_MESSAGE = "Graph explore request must have at least one hop"; public static final String NO_VERTICES_ERROR_MESSAGE = "Graph explore hop must have at least one VertexRequest"; @@ -51,8 +66,8 @@ public GraphExploreRequest() { } /** - * Constructs a new graph request to run against the provided - * indices. No indices means it will run against all indices. + * Constructs a new graph request to run against the provided indices. No + * indices means it will run against all indices. */ public GraphExploreRequest(String... indices) { this.indices = indices; @@ -75,7 +90,6 @@ public String[] indices() { return this.indices; } - @Override public GraphExploreRequest indices(String... indices) { this.indices = indices; @@ -123,10 +137,14 @@ public TimeValue timeout() { } /** - * Graph exploration can be set to timeout after the given period. Search operations involved in - * each hop are limited to the remaining time available but can still overrun due to the nature - * of their "best efforts" timeout support. When a timeout occurs partial results are returned. - * @param timeout a {@link TimeValue} object which determines the maximum length of time to spend exploring + * Graph exploration can be set to timeout after the given period. Search + * operations involved in each hop are limited to the remaining time + * available but can still overrun due to the nature of their "best efforts" + * timeout support. When a timeout occurs partial results are returned. + * + * @param timeout + * a {@link TimeValue} object which determines the maximum length + * of time to spend exploring */ public GraphExploreRequest timeout(TimeValue timeout) { if (timeout == null) { @@ -153,10 +171,10 @@ public void readFrom(StreamInput in) throws IOException { sampleSize = in.readInt(); sampleDiversityField = in.readOptionalString(); maxDocsPerDiversityValue = in.readInt(); - + useSignificance = in.readBoolean(); returnDetailedInfo = in.readBoolean(); - + int numHops = in.readInt(); Hop parentHop = null; for (int i = 0; i < numHops; i++) { @@ -180,7 +198,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeInt(sampleSize); out.writeOptionalString(sampleDiversityField); out.writeInt(maxDocsPerDiversityValue); - + out.writeBoolean(useSignificance); out.writeBoolean(returnDetailedInfo); out.writeInt(hops.size()); @@ -196,18 +214,21 @@ public String toString() { } /** - * The number of top-matching documents that are considered during each hop (default is - * {@link SamplerAggregationBuilder#DEFAULT_SHARD_SAMPLE_SIZE} - * Very small values (less than 50) may not provide sufficient weight-of-evidence to identify - * significant connections between terms. - *

Very large values (many thousands) are not recommended with loosely defined queries (fuzzy queries or those - * with many OR clauses). - * This is because any useful signals in the best documents are diluted with irrelevant noise from low-quality matches. - * Performance is also typically better with smaller samples as there are less look-ups required for background frequencies - * of terms found in the documents + * The number of top-matching documents that are considered during each hop + * (default is {@link SamplerAggregationBuilder#DEFAULT_SHARD_SAMPLE_SIZE} + * Very small values (less than 50) may not provide sufficient + * weight-of-evidence to identify significant connections between terms. + *

+ * Very large values (many thousands) are not recommended with loosely + * defined queries (fuzzy queries or those with many OR clauses). This is + * because any useful signals in the best documents are diluted with + * irrelevant noise from low-quality matches. Performance is also typically + * better with smaller samples as there are less look-ups required for + * background frequencies of terms found in the documents *

* - * @param maxNumberOfDocsPerHop shard-level sample size in documents + * @param maxNumberOfDocsPerHop + * shard-level sample size in documents */ public void sampleSize(int maxNumberOfDocsPerHop) { sampleSize = maxNumberOfDocsPerHop; @@ -242,10 +263,13 @@ public int maxDocsPerDiversityValue() { } /** - * Controls the choice of algorithm used to select interesting terms. The default - * value is true which means terms are selected based on significance (see the {@link SignificantTerms} - * aggregation) rather than popularity (using the {@link TermsAggregator}). - * @param value true if the significant_terms algorithm should be used. + * Controls the choice of algorithm used to select interesting terms. The + * default value is true which means terms are selected based on + * significance (see the {@link SignificantTerms} aggregation) rather than + * popularity (using the {@link TermsAggregator}). + * + * @param value + * true if the significant_terms algorithm should be used. */ public void useSignificance(boolean value) { this.useSignificance = value; @@ -254,32 +278,37 @@ public void useSignificance(boolean value) { public boolean useSignificance() { return useSignificance; } - + /** - * Return detailed information about vertex frequencies as part of JSON results - defaults to false - * @param value true if detailed information is required in JSON responses + * Return detailed information about vertex frequencies as part of JSON + * results - defaults to false + * + * @param value + * true if detailed information is required in JSON responses */ public void returnDetailedInfo(boolean value) { this.returnDetailedInfo = value; - } + } public boolean returnDetailedInfo() { return returnDetailedInfo; } - /** - * Add a stage in the graph exploration. Each hop represents a stage of - * querying elasticsearch to identify terms which can then be connnected - * to other terms in a subsequent hop. - * @param guidingQuery optional choice of query which influences which documents - * are considered in this stage - * @return a {@link Hop} object that holds settings for a stage in the graph exploration + * Add a stage in the graph exploration. Each hop represents a stage of + * querying elasticsearch to identify terms which can then be connnected to + * other terms in a subsequent hop. + * + * @param guidingQuery + * optional choice of query which influences which documents are + * considered in this stage + * @return a {@link Hop} object that holds settings for a stage in the graph + * exploration */ public Hop createNextHop(QueryBuilder guidingQuery) { Hop parent = null; if (hops.size() > 0) { - parent = hops.get(hops.size() - 1); + parent = hops.get(hops.size() - 1); } Hop newHop = new Hop(parent); newHop.guidingQuery = guidingQuery; @@ -330,6 +359,43 @@ void writeTo(StreamOutput out) throws IOException { } } - + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + builder.startObject("controls"); + { + if (sampleSize != SamplerAggregationBuilder.DEFAULT_SHARD_SAMPLE_SIZE) { + builder.field("sample_size", sampleSize); + } + if (sampleDiversityField != null) { + builder.startObject("sample_diversity"); + builder.field("field", sampleDiversityField); + builder.field("max_docs_per_value", maxDocsPerDiversityValue); + builder.endObject(); + } + builder.field("use_significance", useSignificance); + if (returnDetailedInfo) { + builder.field("return_detailed_stats", returnDetailedInfo); + } + } + builder.endObject(); + + for (Hop hop : hops) { + if (hop.parentHop != null) { + builder.startObject("connections"); + } + hop.toXContent(builder, params); + } + for (Hop hop : hops) { + if (hop.parentHop != null) { + builder.endObject(); + } + } + builder.endObject(); + + return builder; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponse.java similarity index 55% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreResponse.java rename to x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponse.java index 3d6c5f5aaca5..baaaedf0163e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/GraphExploreResponse.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponse.java @@ -1,28 +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. + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ -package org.elasticsearch.xpack.core.graph.action; +package org.elasticsearch.protocol.xpack.graph; import com.carrotsearch.hppc.ObjectIntHashMap; + import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ShardOperationFailedException; import org.elasticsearch.action.search.ShardSearchFailure; +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.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.xpack.core.graph.action.Connection.ConnectionId; -import org.elasticsearch.xpack.core.graph.action.Vertex.VertexId; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.graph.Connection.ConnectionId; +import org.elasticsearch.protocol.xpack.graph.Connection.UnresolvedConnection; +import org.elasticsearch.protocol.xpack.graph.Vertex.VertexId; import java.io.IOException; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.elasticsearch.action.search.ShardSearchFailure.readShardSearchFailure; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; /** * Graph explore response holds a graph of {@link Vertex} and {@link Connection} objects @@ -100,8 +121,7 @@ public void readFrom(StreamInput in) throws IOException { connections = new HashMap<>(); for (int i = 0; i < size; i++) { - Connection e = new Connection(); - e.readFrom(in, vertices); + Connection e = new Connection(in, vertices); connections.put(e.getId(), e); } @@ -146,23 +166,19 @@ public void writeTo(StreamOutput out) throws IOException { } - static final class Fields { - static final String TOOK = "took"; - static final String TIMED_OUT = "timed_out"; - static final String INDICES = "_indices"; - static final String FAILURES = "failures"; - static final String VERTICES = "vertices"; - static final String CONNECTIONS = "connections"; - - } + private static final ParseField TOOK = new ParseField("took"); + private static final ParseField TIMED_OUT = new ParseField("timed_out"); + private static final ParseField VERTICES = new ParseField("vertices"); + private static final ParseField CONNECTIONS = new ParseField("connections"); + private static final ParseField FAILURES = new ParseField("failures"); @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(Fields.TOOK, tookInMillis); - builder.field(Fields.TIMED_OUT, timedOut); + builder.field(TOOK.getPreferredName(), tookInMillis); + builder.field(TIMED_OUT.getPreferredName(), timedOut); - builder.startArray(Fields.FAILURES); + builder.startArray(FAILURES.getPreferredName()); if (shardFailures != null) { for (ShardOperationFailedException shardFailure : shardFailures) { builder.startObject(); @@ -178,7 +194,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws extraParams.put(RETURN_DETAILED_INFO_PARAM, Boolean.toString(returnDetailedInfo)); Params extendedParams = new DelegatingMapParams(extraParams, params); - builder.startArray(Fields.VERTICES); + builder.startArray(VERTICES.getPreferredName()); for (Vertex vertex : vertices.values()) { builder.startObject(); vertexNumbers.put(vertex, vertexNumbers.size()); @@ -187,7 +203,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } builder.endArray(); - builder.startArray(Fields.CONNECTIONS); + builder.startArray(CONNECTIONS.getPreferredName()); for (Connection connection : connections.values()) { builder.startObject(); connection.toXContent(builder, extendedParams, vertexNumbers); @@ -198,5 +214,48 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "GraphExploreResponsenParser", true, + args -> { + GraphExploreResponse result = new GraphExploreResponse(); + result.vertices = new HashMap<>(); + result.connections = new HashMap<>(); + + result.tookInMillis = (Long) args[0]; + result.timedOut = (Boolean) args[1]; + + @SuppressWarnings("unchecked") + List vertices = (List) args[2]; + @SuppressWarnings("unchecked") + List unresolvedConnections = (List) args[3]; + @SuppressWarnings("unchecked") + List failures = (List) args[4]; + for (Vertex vertex : vertices) { + // reverse-engineer if detailed stats were requested - + // mainly here for testing framework's equality tests + result.returnDetailedInfo = result.returnDetailedInfo || vertex.getFg() > 0; + result.vertices.put(vertex.getId(), vertex); + } + for (UnresolvedConnection unresolvedConnection : unresolvedConnections) { + Connection resolvedConnection = unresolvedConnection.resolve(vertices); + result.connections.put(resolvedConnection.getId(), resolvedConnection); + } + if (failures.size() > 0) { + result.shardFailures = failures.toArray(new ShardSearchFailure[failures.size()]); + } + return result; + }); + + static { + PARSER.declareLong(constructorArg(), TOOK); + PARSER.declareBoolean(constructorArg(), TIMED_OUT); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> Vertex.fromXContent(p), VERTICES); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> UnresolvedConnection.fromXContent(p), CONNECTIONS); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ShardSearchFailure.fromXContent(p), FAILURES); + } + + public static GraphExploreResponse fromXContext(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Hop.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Hop.java similarity index 73% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Hop.java rename to x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Hop.java index 8ba7005f15fc..70ec61067f5b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Hop.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Hop.java @@ -1,14 +1,29 @@ /* - * 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. + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ -package org.elasticsearch.xpack.core.graph.action; +package org.elasticsearch.protocol.xpack.graph; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ValidateActions; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -41,7 +56,7 @@ *

* */ -public class Hop { +public class Hop implements ToXContentFragment{ final Hop parentHop; List vertices = null; QueryBuilder guidingQuery = null; @@ -139,4 +154,20 @@ public int getNumberVertexRequests() { public VertexRequest getVertexRequest(int requestNumber) { return getEffectiveVertexRequests().get(requestNumber); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (guidingQuery != null) { + builder.field("query"); + guidingQuery.toXContent(builder, params); + } + if(vertices != null && vertices.size()>0) { + builder.startArray("vertices"); + for (VertexRequest vertexRequest : vertices) { + vertexRequest.toXContent(builder, params); + } + builder.endArray(); + } + return builder; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Vertex.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Vertex.java similarity index 59% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Vertex.java rename to x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Vertex.java index c85d6d7dfd6e..cfc26f44fac0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/Vertex.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/Vertex.java @@ -1,16 +1,36 @@ /* - * 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. + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ -package org.elasticsearch.xpack.core.graph.action; +package org.elasticsearch.protocol.xpack.graph; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; /** * A vertex in a graph response represents a single term (a field and value pair) @@ -27,6 +47,13 @@ public class Vertex implements ToXContentFragment { private final int depth; private final long bg; private long fg; + private static final ParseField FIELD = new ParseField("field"); + private static final ParseField TERM = new ParseField("term"); + private static final ParseField WEIGHT = new ParseField("weight"); + private static final ParseField DEPTH = new ParseField("depth"); + private static final ParseField FG = new ParseField("fg"); + private static final ParseField BG = new ParseField("bg"); + public Vertex(String field, String term, double weight, int depth, long bg, long fg) { super(); @@ -50,20 +77,72 @@ void writeTo(StreamOutput out) throws IOException { out.writeVLong(bg); out.writeVLong(fg); } + + @Override + public int hashCode() { + return Objects.hash(field, term, weight, depth, bg, fg); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Vertex other = (Vertex) obj; + return depth == other.depth && + weight == other.weight && + bg == other.bg && + fg == other.fg && + Objects.equals(field, other.field) && + Objects.equals(term, other.term); + + } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { boolean returnDetailedInfo = params.paramAsBoolean(GraphExploreResponse.RETURN_DETAILED_INFO_PARAM, false); - builder.field("field", field); - builder.field("term", term); - builder.field("weight", weight); - builder.field("depth", depth); + builder.field(FIELD.getPreferredName(), field); + builder.field(TERM.getPreferredName(), term); + builder.field(WEIGHT.getPreferredName(), weight); + builder.field(DEPTH.getPreferredName(), depth); if (returnDetailedInfo) { - builder.field("fg", fg); - builder.field("bg", bg); + builder.field(FG.getPreferredName(), fg); + builder.field(BG.getPreferredName(), bg); } return builder; } + + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "VertexParser", true, + args -> { + String field = (String) args[0]; + String term = (String) args[1]; + double weight = (Double) args[2]; + int depth = (Integer) args[3]; + Long optionalBg = (Long) args[4]; + Long optionalFg = (Long) args[5]; + long bg = optionalBg == null ? 0 : optionalBg; + long fg = optionalFg == null ? 0 : optionalFg; + return new Vertex(field, term, weight, depth, bg, fg); + }); + + static { + PARSER.declareString(constructorArg(), FIELD); + PARSER.declareString(constructorArg(), TERM); + PARSER.declareDouble(constructorArg(), WEIGHT); + PARSER.declareInt(constructorArg(), DEPTH); + PARSER.declareLong(optionalConstructorArg(), BG); + PARSER.declareLong(optionalConstructorArg(), FG); + } + + static Vertex fromXContent(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + /** * @return a {@link VertexId} object that uniquely identifies this Vertex diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/VertexRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/VertexRequest.java similarity index 70% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/VertexRequest.java rename to x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/VertexRequest.java index f7f7dec4b172..116497fe2301 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/graph/action/VertexRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/VertexRequest.java @@ -1,13 +1,28 @@ /* - * 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. + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ -package org.elasticsearch.xpack.core.graph.action; +package org.elasticsearch.protocol.xpack.graph; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.xpack.core.graph.action.GraphExploreRequest.TermBoost; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.protocol.xpack.graph.GraphExploreRequest.TermBoost; import java.io.IOException; import java.util.HashMap; @@ -21,9 +36,10 @@ * inclusion list to filter which terms are considered. * */ -public class VertexRequest { +public class VertexRequest implements ToXContentObject { private String fieldName; - private int size = 5; + private int size = DEFAULT_SIZE; + public static final int DEFAULT_SIZE = 5; private Map includes; private Set excludes; public static final int DEFAULT_MIN_DOC_COUNT = 3; @@ -195,4 +211,38 @@ public VertexRequest shardMinDocCount(int value) { return this; } + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("field", fieldName); + if (size != DEFAULT_SIZE) { + builder.field("size", size); + } + if (minDocCount != DEFAULT_MIN_DOC_COUNT) { + builder.field("min_doc_count", minDocCount); + } + if (shardMinDocCount != DEFAULT_SHARD_MIN_DOC_COUNT) { + builder.field("shard_min_doc_count", shardMinDocCount); + } + if(includes!=null) { + builder.startArray("include"); + for (TermBoost tb : includes.values()) { + builder.startObject(); + builder.field("term", tb.term); + builder.field("boost", tb.boost); + builder.endObject(); + } + builder.endArray(); + } + if(excludes!=null) { + builder.startArray("exclude"); + for (String value : excludes) { + builder.value(value); + } + builder.endArray(); + } + builder.endObject(); + return builder; + } + } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java new file mode 100644 index 000000000000..f4f666074a11 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Request and Response objects for the default distribution's Graph + * APIs. + */ +package org.elasticsearch.protocol.xpack.graph; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java new file mode 100644 index 000000000000..74b434581788 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -0,0 +1,131 @@ +/* + * 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.protocol.xpack.graph; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ShardOperationFailedException; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; + +public class GraphExploreResponseTests extends AbstractXContentTestCase< GraphExploreResponse> { + + @Override + protected GraphExploreResponse createTestInstance() { + return createInstance(0); + } + private static GraphExploreResponse createInstance(int numFailures) { + int numItems = randomIntBetween(4, 128); + boolean timedOut = randomBoolean(); + boolean showDetails = randomBoolean(); + long overallTookInMillis = randomNonNegativeLong(); + Map vertices = new HashMap<>(); + Map connections = new HashMap<>(); + ShardOperationFailedException [] failures = new ShardOperationFailedException [numFailures]; + for (int i = 0; i < failures.length; i++) { + failures[i] = new ShardSearchFailure(new ElasticsearchException("an error")); + } + + //Create random set of vertices + for (int i = 0; i < numItems; i++) { + Vertex v = new Vertex("field1", randomAlphaOfLength(5), randomDouble(), 0, + showDetails?randomIntBetween(100, 200):0, + showDetails?randomIntBetween(1, 100):0); + vertices.put(v.getId(), v); + } + + //Wire up half the vertices randomly + Vertex[] vs = vertices.values().toArray(new Vertex[vertices.size()]); + for (int i = 0; i < numItems/2; i++) { + Vertex v1 = vs[randomIntBetween(0, vs.length-1)]; + Vertex v2 = vs[randomIntBetween(0, vs.length-1)]; + if(v1 != v2) { + Connection conn = new Connection(v1, v2, randomDouble(), randomLongBetween(1, 10)); + connections.put(conn.getId(), conn); + } + } + return new GraphExploreResponse(overallTookInMillis, timedOut, failures, vertices, connections, showDetails); + } + + + private static GraphExploreResponse createTestInstanceWithFailures() { + return createInstance(randomIntBetween(1, 128)); + } + + @Override + protected GraphExploreResponse doParseInstance(XContentParser parser) throws IOException { + return GraphExploreResponse.fromXContext(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { + return field -> field.startsWith("responses"); + } + + @Override + protected void assertEqualInstances( GraphExploreResponse expectedInstance, GraphExploreResponse newInstance) { + assertThat(newInstance.getTook(), equalTo(expectedInstance.getTook())); + assertThat(newInstance.isTimedOut(), equalTo(expectedInstance.isTimedOut())); + + Connection[] newConns = newInstance.getConnections().toArray(new Connection[0]); + Connection[] expectedConns = expectedInstance.getConnections().toArray(new Connection[0]); + assertArrayEquals(expectedConns, newConns); + + Vertex[] newVertices = newInstance.getVertices().toArray(new Vertex[0]); + Vertex[] expectedVertices = expectedInstance.getVertices().toArray(new Vertex[0]); + assertArrayEquals(expectedVertices, newVertices); + + ShardOperationFailedException[] newFailures = newInstance.getShardFailures(); + ShardOperationFailedException[] expectedFailures = expectedInstance.getShardFailures(); + assertEquals(expectedFailures.length, newFailures.length); + + } + + /** + * Test parsing {@link GraphExploreResponse} with inner failures as they don't support asserting on xcontent equivalence, given + * exceptions are not parsed back as the same original class. We run the usual {@link AbstractXContentTestCase#testFromXContent()} + * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. + */ + public void testFromXContentWithFailures() throws IOException { + Supplier< GraphExploreResponse> instanceSupplier = GraphExploreResponseTests::createTestInstanceWithFailures; + //with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, + //but that does not bother our assertions, as we only want to test that we don't break. + boolean supportsUnknownFields = true; + //exceptions are not of the same type whenever parsed back + boolean assertToXContentEquivalence = false; + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, + this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); + } + +} From 3f91bbfa6b408c2e72471c39cdeef4cbfe30eef2 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 21 Aug 2018 08:08:26 -0500 Subject: [PATCH 080/283] [ML] Allowing _close to accept body payloads for options (#32989) (#33000) --- .../xpack/ml/rest/job/RestCloseJobAction.java | 25 ++++---- .../xpack/ml/integration/CloseJobsIT.java | 57 +++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CloseJobsIT.java diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java index fc0638048ce9..ae6257d53850 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java @@ -12,10 +12,10 @@ import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; -import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.core.ml.action.CloseJobAction; import org.elasticsearch.xpack.core.ml.action.CloseJobAction.Request; import org.elasticsearch.xpack.core.ml.job.config.Job; +import org.elasticsearch.xpack.ml.MachineLearning; import java.io.IOException; @@ -34,16 +34,21 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { - Request request = new Request(restRequest.param(Job.ID.getPreferredName())); - if (restRequest.hasParam(Request.TIMEOUT.getPreferredName())) { - request.setCloseTimeout(TimeValue.parseTimeValue( + Request request; + if (restRequest.hasContentOrSourceParam()) { + request = Request.parseRequest(restRequest.param(Job.ID.getPreferredName()), restRequest.contentParser()); + } else { + request = new Request(restRequest.param(Job.ID.getPreferredName())); + if (restRequest.hasParam(Request.TIMEOUT.getPreferredName())) { + request.setCloseTimeout(TimeValue.parseTimeValue( restRequest.param(Request.TIMEOUT.getPreferredName()), Request.TIMEOUT.getPreferredName())); - } - if (restRequest.hasParam(Request.FORCE.getPreferredName())) { - request.setForce(restRequest.paramAsBoolean(Request.FORCE.getPreferredName(), request.isForce())); - } - if (restRequest.hasParam(Request.ALLOW_NO_JOBS.getPreferredName())) { - request.setAllowNoJobs(restRequest.paramAsBoolean(Request.ALLOW_NO_JOBS.getPreferredName(), request.allowNoJobs())); + } + if (restRequest.hasParam(Request.FORCE.getPreferredName())) { + request.setForce(restRequest.paramAsBoolean(Request.FORCE.getPreferredName(), request.isForce())); + } + if (restRequest.hasParam(Request.ALLOW_NO_JOBS.getPreferredName())) { + request.setAllowNoJobs(restRequest.paramAsBoolean(Request.ALLOW_NO_JOBS.getPreferredName(), request.allowNoJobs())); + } } return channel -> client.execute(CloseJobAction.INSTANCE, request, new RestToXContentListener<>(channel)); } diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CloseJobsIT.java b/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CloseJobsIT.java new file mode 100644 index 000000000000..95ec9728842c --- /dev/null +++ b/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CloseJobsIT.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.integration; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.SecuritySettingsSourceField; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xpack.ml.MachineLearning; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.equalTo; + +public class CloseJobsIT extends ESRestTestCase { + + private static final String BASIC_AUTH_VALUE = basicAuthHeaderValue("x_pack_rest_user", + SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING); + + @Override + protected Settings restClientSettings() { + return Settings.builder().put(super.restClientSettings()).put(ThreadContext.PREFIX + ".Authorization", BASIC_AUTH_VALUE).build(); + } + + public void testCloseJobsAcceptsOptionsFromPayload() throws Exception { + + Request request = new Request("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + "job-that-doesnot-exist*" + "/_close"); + request.setJsonEntity("{\"allow_no_jobs\":false}"); + request.setOptions(RequestOptions.DEFAULT); + ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest(request)); + assertThat(exception.getResponse().getStatusLine().getStatusCode(), equalTo(404)); + + request.setJsonEntity("{\"allow_no_jobs\":true}"); + Response response = client().performRequest(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + String responseAsString = responseEntityToString(response); + assertEquals(responseAsString, "{\"closed\":true}"); + } + + private static String responseEntityToString(Response response) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } +} From 1b583978e9659de9ba6506ee6c4bbadff174ef65 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 21 Aug 2018 16:20:00 +0300 Subject: [PATCH 081/283] [DOCS] Add FIPS 140-2 documentation (#32928) * Add relevant documentation for FIPS 140-2 compliance. * Introduce `fips_mode` setting. * Discuss necessary configuration for FIPS 140-2 * Discuss introduced limitations by FIPS 140-2 --- .../settings/security-settings.asciidoc | 16 ++- .../docs/en/security/configuring-es.asciidoc | 4 + .../en/security/fips-140-compliance.asciidoc | 128 ++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 x-pack/docs/en/security/fips-140-compliance.asciidoc diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 6a7742c4c00d..74deee3473fd 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -46,6 +46,9 @@ settings for the ad1 realm: `xpack.security.authc.realms.ad1.*`. The API already omits all `ssl` settings, `bind_dn`, and `bind_password` due to the sensitive nature of the information. +`xpack.security.fips_mode.enabled`:: +Enables fips mode of operation. Set this to `true` if you run this {es} instance in a FIPS 140-2 enabled JVM. For more information, see <>. Defaults to `false`. + [float] [[password-security-settings]] ==== Default password security settings @@ -1124,7 +1127,12 @@ settings such as those for HTTP or Transport. `xpack.ssl.supported_protocols`:: Supported protocols with versions. Valid protocols: `SSLv2Hello`, `SSLv3`, `TLSv1`, `TLSv1.1`, `TLSv1.2`. Defaults to `TLSv1.2`, `TLSv1.1`, -`TLSv1`. +`TLSv1`. ++ +-- +NOTE: If `xpack.security.fips_mode.enabled` is `true`, you cannot use `SSLv2Hello` +or `SSLv3`. See <>. +-- `xpack.ssl.client_authentication`:: Controls the server's behavior in regard to requesting a certificate @@ -1223,6 +1231,9 @@ Password to the truststore. `xpack.ssl.truststore.secure_password` (<>):: Password to the truststore. +WARNING: If `xpack.security.fips_mode.enabled` is `true`, you cannot use Java +keystore files. See <>. + [float] ===== PKCS#12 files @@ -1261,6 +1272,9 @@ Password to the truststore. `xpack.ssl.truststore.secure_password` (<>):: Password to the truststore. +WARNING: If `xpack.security.fips_mode.enabled` is `true`, you cannot use PKCS#12 +keystore files. See <>. + [[pkcs12-truststore-note]] [NOTE] Storing trusted certificates in a PKCS#12 file, although supported, is diff --git a/x-pack/docs/en/security/configuring-es.asciidoc b/x-pack/docs/en/security/configuring-es.asciidoc index a13547263a58..47d580491c13 100644 --- a/x-pack/docs/en/security/configuring-es.asciidoc +++ b/x-pack/docs/en/security/configuring-es.asciidoc @@ -27,6 +27,9 @@ https://www.elastic.co/subscriptions and your cluster. If you are using a trial license, the default value is `false`. For more information, see {ref}/security-settings.html[Security Settings in {es}]. +. If you plan to run {es} in a Federal Information Processing Standard (FIPS) +140-2 enabled JVM, see <>. + . Configure Transport Layer Security (TLS/SSL) for internode-communication. + -- @@ -145,5 +148,6 @@ 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[] +include::fips-140-compliance.asciidoc[] include::{es-repo-dir}/settings/security-settings.asciidoc[] include::{es-repo-dir}/settings/audit-settings.asciidoc[] diff --git a/x-pack/docs/en/security/fips-140-compliance.asciidoc b/x-pack/docs/en/security/fips-140-compliance.asciidoc new file mode 100644 index 000000000000..ceb605c2e2db --- /dev/null +++ b/x-pack/docs/en/security/fips-140-compliance.asciidoc @@ -0,0 +1,128 @@ +[role="xpack"] +[[fips-140-compliance]] +=== FIPS 140-2 + +The Federal Information Processing Standard (FIPS) Publication 140-2, (FIPS PUB +140-2), titled "Security Requirements for Cryptographic Modules" is a U.S. +government computer security standard used to approve cryptographic modules. +{es} offers a FIPS 140-2 compliant mode and as such can run in a FIPS 140-2 +enabled JVM. In order to set {es} in fips mode, you must set the +`xpack.security.fips_mode.enabled` to `true` in `elasticsearch.yml` + +For {es}, FIPS 140-2 compliance is ensured by + +- Using FIPS approved / NIST recommended cryptographic algorithms. +- Delegating the implementation of these cryptographic algorithms to a NIST + validated cryptographic module (available via the Java Security Provider + in use in the JVM). +- Allowing the configuration of {es} in a FIPS 140-2 compliant manner, as + documented below. + +[float] +=== Upgrade considerations + +If you plan to upgrade your existing Cluster to a version that can be run in +a FIPS 140-2 enabled JVM, the suggested approach is to first perform a rolling +upgrade to the new version in your existing JVM and perform all necessary +configuration changes in preparation for running in fips mode. You can then +perform a rolling restart of the nodes, this time starting each node in the FIPS +140-2 JVM. This will allow {es} to take care of a couple of things automatically for you: + +- <> will be upgraded to the latest format version as + previous format versions cannot be loaded in a FIPS 140-2 JVM. +- Self-generated trial licenses will be upgraded to the latest format that + is compliant with FIPS 140-2. + +If you are on a appropriate license level (platinum) you can elect to perform +a rolling upgrade while at the same time running each upgraded node in a +FIPS 140-2 JVM. In this case, you would need to also regenerate your +`elasticsearch.keystore` and migrate all secure settings to it, in addition to the +necessary configuration changes outlined below, before starting each node. + +[float] +=== Configuring {es} for FIPS 140-2 + +Apart from setting `xpack.security.fips_mode.enabled`, a number of security +related settings need to be configured accordingly in order to be compliant +and able to run {es} successfully in a FIPS 140-2 enabled JVM. + +[float] +==== TLS + +SSLv2 and SSLv3 are not allowed by FIPS 140-2, so `SSLv2Hello` and `SSLv3` cannot +be used for <> + +NOTE: The use of TLS ciphers is mainly governed by the relevant crypto module +(the FIPS Approved Security Provider that your JVM uses). All the ciphers that +are configured by default in {es} are FIPS 140-2 compliant and as such can be +used in a FIPS 140-2 JVM. (see <>) + +[float] +==== TLS Keystores and keys + +Keystores can be used in a number of <> in order to +conveniently store key and trust material. Neither `JKS`, nor `PKCS#12` keystores +can be used in a FIPS 140-2 enabled JVM however, so you must refrain from using +these keystores. Your FIPS 140-2 provider may provide a compliant keystore that +can be used or you can use PEM encoded files. To use PEM encoded key material, +you can use the relevant `\*.key` and `*.certificate` configuration +options, and for trust material you can use `*.certificate_authorities`. + + +FIPS 140-2 compliance dictates that the length of the public keys used for TLS +must correspond to the strength of the symmetric key algorithm in use in TLS. +Depending on the value of <> that +you select to use, the TLS keys must have corresponding length according to +the following table: + +[[comparable-key-strength]] +.Comparable key strengths +|======================= +| Symmetric Key Algorithm | RSA key Length | ECC key length +| `3DES` | 2048 | 224-255 +| `AES-128` | 3072 | 256-383 +| `AES-256` | 15630 | 512+ +|======================= + +[float] +==== Password Hashing + +{es} offers a number of algorithms for securely hashing credentials in memory and +on disk. However, only the `PBKDF2` family of algorithms is compliant with FIPS +140-2 for password hashing. You must set the the `cache.hash_algo` realm settings +and the `xpack.security.authc.password_hashing.algorithm` setting to one of the +available `PBKDF2` values. +See <>. + +Password hashing configuration changes are not retroactive so the stored hashed +credentials of existing users of the file and native realms will not be updated +on disk. +Authentication will still work, but in order to ensure FIPS 140-2 compliance, +you would need to recreate users or change their password using the +<> CLI tool for the file realm and the +<> for the native realm. + +The user cache will be emptied upon node restart, so any existing hashes using +non-compliant algorithms will be discarded and the new ones will be created +using the compliant `PBKDF2` algorithm you have selected. + +[float] +=== Limitations + +Due to the limitations that FIPS 140-2 compliance enforces, a small number of +features are not available while running in fips mode. The list is as follows: + +* Azure Classic Discovery Plugin +* Ingest Attachment Plugin +* The {ref}/certutil.html[`elasticsearch-certutil`] tool. However, + `elasticsearch-certutil` can very well be used in a non FIPS 140-2 + enabled JVM (pointing `JAVA_HOME` environment variable to a different java + installation) in order to generate the keys and certificates that + can be later used in the FIPS 140-2 enabled JVM. +* The `elasticsearch-plugin` tool. Accordingly, `elasticsearch-plugin` can be + used with a different (non FIPS 140-2 enabled) Java installation if + available. +* The SQL CLI client cannot run in a FIPS 140-2 enabled JVM while using + TLS for transport security or PKI for client authentication. +* The SAML Realm cannot decrypt and consume encrypted Assertions or encrypted + attributes in Attribute Statements from the SAML IdP. \ No newline at end of file From 5b446b81efd500fcfef424d871a972c12e853ece Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Tue, 21 Aug 2018 16:53:59 +0200 Subject: [PATCH 082/283] Reenable MonitoringIT#testMonitoringService. When discussing this test, it made little sense that testMonitoringService would fail but not testMonitoringBulk given their similarity. So we argeed to enable it again. Relates #29880 --- .../elasticsearch/xpack/monitoring/integration/MonitoringIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java index efc32fccb3dd..0ee5f85bfad2 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java @@ -186,7 +186,6 @@ public void testMonitoringBulk() throws Exception { * This test waits for the monitoring service to collect monitoring documents and then checks that all expected documents * have been indexed with the expected information. */ - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/29880") public void testMonitoringService() throws Exception { final boolean createAPMIndex = randomBoolean(); final String indexName = createAPMIndex ? "apm-2017.11.06" : "books"; From bdfcc326d7ee351390fb26f928261926c39ae6ff Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 21 Aug 2018 11:02:25 -0400 Subject: [PATCH 083/283] Enable avoiding mmap bootstrap check (#32421) The maximum map count boostrap check can be a hindrance to users that do not own the underlying platform on which they are executing Elasticsearch. This is because addressing it requires tuning the kernel and a platform provider might now allow this, especially on shared infrastructure. However, this bootstrap check is not needed if mmapfs is not in use. Today we do not have a way for the user to communicate that they are not going to use mmapfs. This commit therefore adds a setting that enables the user to disallow mmapfs. When mmapfs is disallowed, the maximum map count bootstrap check is not enforced. Additionally, we fallback to a different default index store and prevent the explicit use of mmapfs for an index. --- docs/reference/index-modules/store.asciidoc | 7 ++ .../reference/setup/bootstrap-checks.asciidoc | 5 + .../bootstrap/BootstrapChecks.java | 24 ++-- .../common/settings/ClusterSettings.java | 2 + .../org/elasticsearch/index/IndexModule.java | 105 ++++++++++++++---- .../index/store/FsDirectoryService.java | 18 ++- .../elasticsearch/indices/IndicesService.java | 8 ++ .../bootstrap/BootstrapChecksTests.java | 27 +---- .../bootstrap/MaxMapCountCheckTests.java | 67 ++++++++++- .../elasticsearch/index/IndexModuleTests.java | 17 +++ .../plugins/IndexStorePluginTests.java | 27 ++++- 11 files changed, 243 insertions(+), 64 deletions(-) diff --git a/docs/reference/index-modules/store.asciidoc b/docs/reference/index-modules/store.asciidoc index a1e00bac6164..c2b3d700e9b7 100644 --- a/docs/reference/index-modules/store.asciidoc +++ b/docs/reference/index-modules/store.asciidoc @@ -67,6 +67,13 @@ process equal to the size of the file being mapped. Before using this class, be sure you have allowed plenty of <>. +[[allow-mmapfs]] +You can restrict the use of the `mmapfs` store type via the setting +`node.store.allow_mmapfs`. This is a boolean setting indicating whether or not +`mmapfs` is allowed. The default is to allow `mmapfs`. This setting is useful, +for example, if you are in an environment where you can not control the ability +to create a lot of memory maps so you need disable the ability to use `mmapfs`. + === Pre-loading data into the file system cache NOTE: This is an expert setting, the details of which may change in the future. diff --git a/docs/reference/setup/bootstrap-checks.asciidoc b/docs/reference/setup/bootstrap-checks.asciidoc index a8b8dd82d617..f0e5cfc71c99 100644 --- a/docs/reference/setup/bootstrap-checks.asciidoc +++ b/docs/reference/setup/bootstrap-checks.asciidoc @@ -155,6 +155,11 @@ the kernel allows a process to have at least 262,144 memory-mapped areas and is enforced on Linux only. To pass the maximum map count check, you must configure `vm.max_map_count` via `sysctl` to be at least `262144`. +Alternatively, the maximum map count check is only needed if you are using +`mmapfs` as the <> for your indices. If you +<> the use of `mmapfs` then this bootstrap check will +not be enforced. + === Client JVM check There are two different JVMs provided by OpenJDK-derived JVMs: the diff --git a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java index 1a028042db29..c5a8e806f41a 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/BootstrapChecks.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.discovery.DiscoveryModule; +import org.elasticsearch.index.IndexModule; import org.elasticsearch.monitor.jvm.JvmInfo; import org.elasticsearch.monitor.process.ProcessProbe; import org.elasticsearch.node.NodeValidationException; @@ -393,17 +394,22 @@ long getMaxFileSize() { static class MaxMapCountCheck implements BootstrapCheck { - private static final long LIMIT = 1 << 18; + static final long LIMIT = 1 << 18; @Override - public BootstrapCheckResult check(BootstrapContext context) { - if (getMaxMapCount() != -1 && getMaxMapCount() < LIMIT) { - final String message = String.format( - Locale.ROOT, - "max virtual memory areas vm.max_map_count [%d] is too low, increase to at least [%d]", - getMaxMapCount(), - LIMIT); - return BootstrapCheckResult.failure(message); + public BootstrapCheckResult check(final BootstrapContext context) { + // we only enforce the check if mmapfs is an allowed store type + if (IndexModule.NODE_STORE_ALLOW_MMAPFS.get(context.settings)) { + if (getMaxMapCount() != -1 && getMaxMapCount() < LIMIT) { + final String message = String.format( + Locale.ROOT, + "max virtual memory areas vm.max_map_count [%d] is too low, increase to at least [%d]", + getMaxMapCount(), + LIMIT); + return BootstrapCheckResult.failure(message); + } else { + return BootstrapCheckResult.success(); + } } else { return BootstrapCheckResult.success(); } 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 de8691b3b687..bf53a3dc01a7 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -63,6 +63,7 @@ import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.gateway.GatewayService; import org.elasticsearch.http.HttpTransportSettings; +import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.indices.IndexingMemoryController; import org.elasticsearch.indices.IndicesQueryCache; @@ -264,6 +265,7 @@ public void apply(Settings value, Settings current, Settings previous) { HierarchyCircuitBreakerService.REQUEST_CIRCUIT_BREAKER_OVERHEAD_SETTING, HierarchyCircuitBreakerService.ACCOUNTING_CIRCUIT_BREAKER_LIMIT_SETTING, HierarchyCircuitBreakerService.ACCOUNTING_CIRCUIT_BREAKER_OVERHEAD_SETTING, + IndexModule.NODE_STORE_ALLOW_MMAPFS, ClusterService.CLUSTER_SERVICE_SLOW_TASK_LOGGING_THRESHOLD_SETTING, SearchService.DEFAULT_SEARCH_TIMEOUT_SETTING, SearchService.DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS, diff --git a/server/src/main/java/org/elasticsearch/index/IndexModule.java b/server/src/main/java/org/elasticsearch/index/IndexModule.java index 715b78b14ffd..7f2eae492fd5 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexModule.java +++ b/server/src/main/java/org/elasticsearch/index/IndexModule.java @@ -21,10 +21,11 @@ import org.apache.lucene.search.similarities.BM25Similarity; import org.apache.lucene.search.similarities.Similarity; +import org.apache.lucene.store.MMapDirectory; +import org.apache.lucene.util.Constants; import org.apache.lucene.util.SetOnce; import org.elasticsearch.Version; import org.elasticsearch.client.Client; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.TriFunction; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.Setting; @@ -59,7 +60,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -84,8 +84,10 @@ */ public final class IndexModule { + public static final Setting NODE_STORE_ALLOW_MMAPFS = Setting.boolSetting("node.store.allow_mmapfs", true, Property.NodeScope); + public static final Setting INDEX_STORE_TYPE_SETTING = - new Setting<>("index.store.type", "", Function.identity(), Property.IndexScope, Property.NodeScope); + new Setting<>("index.store.type", "", Function.identity(), Property.IndexScope, Property.NodeScope); /** On which extensions to load data into the file-system cache upon opening of files. * This only works with the mmap directory, and even in that case is still @@ -289,7 +291,7 @@ IndexEventListener freeze() { // pkg private for testing } } - private static boolean isBuiltinType(String storeType) { + public static boolean isBuiltinType(String storeType) { for (Type type : Type.values()) { if (type.match(storeType)) { return true; @@ -298,21 +300,48 @@ private static boolean isBuiltinType(String storeType) { return false; } + public enum Type { - NIOFS, - MMAPFS, - SIMPLEFS, - FS; + NIOFS("niofs"), + MMAPFS("mmapfs"), + SIMPLEFS("simplefs"), + FS("fs"); + + private final String settingsKey; + + Type(final String settingsKey) { + this.settingsKey = settingsKey; + } + + private static final Map TYPES; + + static { + final Map types = new HashMap<>(4); + for (final Type type : values()) { + types.put(type.settingsKey, type); + } + TYPES = Collections.unmodifiableMap(types); + } public String getSettingsKey() { - return this.name().toLowerCase(Locale.ROOT); + return this.settingsKey; + } + + public static Type fromSettingsKey(final String key) { + final Type type = TYPES.get(key); + if (type == null) { + throw new IllegalArgumentException("no matching type for [" + key + "]"); + } + return type; } + /** * Returns true iff this settings matches the type. */ public boolean match(String setting) { return getSettingsKey().equals(setting); } + } /** @@ -325,6 +354,16 @@ public interface IndexSearcherWrapperFactory { IndexSearcherWrapper newWrapper(IndexService indexService); } + public static Type defaultStoreType(final boolean allowMmapfs) { + if (allowMmapfs && Constants.JRE_IS_64BIT && MMapDirectory.UNMAP_SUPPORTED) { + return Type.MMAPFS; + } else if (Constants.WINDOWS) { + return Type.SIMPLEFS; + } else { + return Type.NIOFS; + } + } + public IndexService newIndexService( NodeEnvironment environment, NamedXContentRegistry xContentRegistry, @@ -343,20 +382,7 @@ public IndexService newIndexService( IndexSearcherWrapperFactory searcherWrapperFactory = indexSearcherWrapper.get() == null ? (shard) -> null : indexSearcherWrapper.get(); eventListener.beforeIndexCreated(indexSettings.getIndex(), indexSettings.getSettings()); - final String storeType = indexSettings.getValue(INDEX_STORE_TYPE_SETTING); - final IndexStore store; - if (Strings.isEmpty(storeType) || isBuiltinType(storeType)) { - store = new IndexStore(indexSettings); - } else { - Function factory = indexStoreFactories.get(storeType); - if (factory == null) { - throw new IllegalArgumentException("Unknown store type [" + storeType + "]"); - } - store = factory.apply(indexSettings); - if (store == null) { - throw new IllegalStateException("store must not be null"); - } - } + final IndexStore store = getIndexStore(indexSettings, indexStoreFactories); final QueryCache queryCache; if (indexSettings.getValue(INDEX_QUERY_CACHE_ENABLED_SETTING)) { BiFunction queryCacheProvider = forceQueryCacheProvider.get(); @@ -375,6 +401,39 @@ public IndexService newIndexService( indicesFieldDataCache, searchOperationListeners, indexOperationListeners, namedWriteableRegistry); } + private static IndexStore getIndexStore( + final IndexSettings indexSettings, final Map> indexStoreFactories) { + final String storeType = indexSettings.getValue(INDEX_STORE_TYPE_SETTING); + final Type type; + final Boolean allowMmapfs = NODE_STORE_ALLOW_MMAPFS.get(indexSettings.getNodeSettings()); + if (storeType.isEmpty() || Type.FS.getSettingsKey().equals(storeType)) { + type = defaultStoreType(allowMmapfs); + } else { + if (isBuiltinType(storeType)) { + type = Type.fromSettingsKey(storeType); + } else { + type = null; + } + } + if (type != null && type == Type.MMAPFS && allowMmapfs == false) { + throw new IllegalArgumentException("store type [mmapfs] is not allowed"); + } + final IndexStore store; + if (storeType.isEmpty() || isBuiltinType(storeType)) { + store = new IndexStore(indexSettings); + } else { + Function factory = indexStoreFactories.get(storeType); + if (factory == null) { + throw new IllegalArgumentException("Unknown store type [" + storeType + "]"); + } + store = factory.apply(indexSettings); + if (store == null) { + throw new IllegalStateException("store must not be null"); + } + } + return store; + } + /** * creates a new mapper service to do administrative work like mapping updates. This *should not* be used for document parsing. * doing so will result in an exception. diff --git a/server/src/main/java/org/elasticsearch/index/store/FsDirectoryService.java b/server/src/main/java/org/elasticsearch/index/store/FsDirectoryService.java index fc6054300664..f95cdb3a9f69 100644 --- a/server/src/main/java/org/elasticsearch/index/store/FsDirectoryService.java +++ b/server/src/main/java/org/elasticsearch/index/store/FsDirectoryService.java @@ -20,7 +20,6 @@ package org.elasticsearch.index.store; import org.apache.lucene.store.Directory; -import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.FileSwitchDirectory; import org.apache.lucene.store.LockFactory; import org.apache.lucene.store.MMapDirectory; @@ -77,10 +76,21 @@ public Directory newDirectory() throws IOException { } protected Directory newFSDirectory(Path location, LockFactory lockFactory) throws IOException { - final String storeType = indexSettings.getSettings().get(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), - IndexModule.Type.FS.getSettingsKey()); + final String storeType = + indexSettings.getSettings().get(IndexModule.INDEX_STORE_TYPE_SETTING.getKey(), IndexModule.Type.FS.getSettingsKey()); if (IndexModule.Type.FS.match(storeType)) { - return FSDirectory.open(location, lockFactory); // use lucene defaults + final IndexModule.Type type = + IndexModule.defaultStoreType(IndexModule.NODE_STORE_ALLOW_MMAPFS.get(indexSettings.getNodeSettings())); + switch (type) { + case MMAPFS: + return new MMapDirectory(location, lockFactory); + case SIMPLEFS: + return new SimpleFSDirectory(location, lockFactory); + case NIOFS: + return new NIOFSDirectory(location, lockFactory); + default: + throw new AssertionError("unexpected built-in store type [" + type + "]"); + } } else if (IndexModule.Type.SIMPLEFS.match(storeType)) { return new SimpleFSDirectory(location, lockFactory); } else if (IndexModule.Type.NIOFS.match(storeType)) { diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index 5c097ba774f4..1c83a880511c 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -228,6 +228,14 @@ public void onRemoval(ShardId shardId, String fieldName, boolean wasEvicted, lon this.cacheCleaner = new CacheCleaner(indicesFieldDataCache, indicesRequestCache, logger, threadPool, this.cleanInterval); this.metaStateService = metaStateService; this.engineFactoryProviders = engineFactoryProviders; + + // do not allow any plugin-provided index store type to conflict with a built-in type + for (final String indexStoreType : indexStoreFactories.keySet()) { + if (IndexModule.isBuiltinType(indexStoreType)) { + throw new IllegalStateException("registered index store type [" + indexStoreType + "] conflicts with a built-in type"); + } + } + this.indexStoreFactories = indexStoreFactories; } diff --git a/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java b/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java index 8180dd96e8e1..1e18135f4eb7 100644 --- a/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java +++ b/server/src/test/java/org/elasticsearch/bootstrap/BootstrapChecksTests.java @@ -52,7 +52,7 @@ public class BootstrapChecksTests extends ESTestCase { - private static final BootstrapContext defaultContext = new BootstrapContext(Settings.EMPTY, MetaData.EMPTY_META_DATA); + static final BootstrapContext defaultContext = new BootstrapContext(Settings.EMPTY, MetaData.EMPTY_META_DATA); public void testNonProductionMode() throws NodeValidationException { // nothing should happen since we are in non-production mode @@ -356,31 +356,6 @@ long getRlimInfinity() { BootstrapChecks.check(defaultContext, true, Collections.singletonList(check)); } - public void testMaxMapCountCheck() throws NodeValidationException { - final int limit = 1 << 18; - final AtomicLong maxMapCount = new AtomicLong(randomIntBetween(1, limit - 1)); - final BootstrapChecks.MaxMapCountCheck check = new BootstrapChecks.MaxMapCountCheck() { - @Override - long getMaxMapCount() { - return maxMapCount.get(); - } - }; - - final NodeValidationException e = expectThrows( - NodeValidationException.class, - () -> BootstrapChecks.check(defaultContext, true, Collections.singletonList(check))); - assertThat(e.getMessage(), containsString("max virtual memory areas vm.max_map_count")); - - maxMapCount.set(randomIntBetween(limit + 1, Integer.MAX_VALUE)); - - BootstrapChecks.check(defaultContext, true, Collections.singletonList(check)); - - // nothing should happen if current vm.max_map_count is not - // available - maxMapCount.set(-1); - BootstrapChecks.check(defaultContext, true, Collections.singletonList(check)); - } - public void testClientJvmCheck() throws NodeValidationException { final AtomicReference vmName = new AtomicReference<>("Java HotSpot(TM) 32-Bit Client VM"); final BootstrapCheck check = new BootstrapChecks.ClientJvmCheck() { diff --git a/server/src/test/java/org/elasticsearch/bootstrap/MaxMapCountCheckTests.java b/server/src/test/java/org/elasticsearch/bootstrap/MaxMapCountCheckTests.java index c5b99a91ffa3..9a964a97bd73 100644 --- a/server/src/test/java/org/elasticsearch/bootstrap/MaxMapCountCheckTests.java +++ b/server/src/test/java/org/elasticsearch/bootstrap/MaxMapCountCheckTests.java @@ -24,16 +24,21 @@ import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.util.Constants; +import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.MockLogAppender; import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Predicate; import static org.hamcrest.CoreMatchers.equalTo; @@ -45,6 +50,66 @@ public class MaxMapCountCheckTests extends ESTestCase { + // initialize as if the max map count is under the limit, tests can override by setting maxMapCount before executing the check + private final AtomicLong maxMapCount = new AtomicLong(randomIntBetween(1, Math.toIntExact(BootstrapChecks.MaxMapCountCheck.LIMIT) - 1)); + private final BootstrapChecks.MaxMapCountCheck check = new BootstrapChecks.MaxMapCountCheck() { + @Override + long getMaxMapCount() { + return maxMapCount.get(); + } + }; + + private void assertFailure(final BootstrapCheck.BootstrapCheckResult result) { + assertTrue(result.isFailure()); + assertThat( + result.getMessage(), + equalTo( + "max virtual memory areas vm.max_map_count [" + maxMapCount.get() + "] is too low, " + + "increase to at least [" + BootstrapChecks.MaxMapCountCheck.LIMIT + "]")); + } + + public void testMaxMapCountCheckBelowLimit() { + assertFailure(check.check(BootstrapChecksTests.defaultContext)); + } + + public void testMaxMapCountCheckBelowLimitAndMemoryMapAllowed() { + /* + * There are two ways that memory maps are allowed: + * - by default + * - mmapfs is explicitly allowed + * We want to test that if mmapfs is allowed then the max map count check is enforced. + */ + final List settingsThatAllowMemoryMap = new ArrayList<>(); + settingsThatAllowMemoryMap.add(Settings.EMPTY); + settingsThatAllowMemoryMap.add(Settings.builder().put("node.store.allow_mmapfs", true).build()); + + for (final Settings settingThatAllowsMemoryMap : settingsThatAllowMemoryMap) { + assertFailure(check.check(new BootstrapContext(settingThatAllowsMemoryMap, MetaData.EMPTY_META_DATA))); + } + } + + public void testMaxMapCountCheckNotEnforcedIfMemoryMapNotAllowed() { + // nothing should happen if current vm.max_map_count is under the limit but mmapfs is not allowed + final Settings settings = Settings.builder().put("node.store.allow_mmapfs", false).build(); + final BootstrapContext context = new BootstrapContext(settings, MetaData.EMPTY_META_DATA); + final BootstrapCheck.BootstrapCheckResult result = check.check(context); + assertTrue(result.isSuccess()); + } + + public void testMaxMapCountCheckAboveLimit() { + // nothing should happen if current vm.max_map_count exceeds the limit + maxMapCount.set(randomIntBetween(Math.toIntExact(BootstrapChecks.MaxMapCountCheck.LIMIT) + 1, Integer.MAX_VALUE)); + final BootstrapCheck.BootstrapCheckResult result = check.check(BootstrapChecksTests.defaultContext); + assertTrue(result.isSuccess()); + } + + public void testMaxMapCountCheckMaxMapCountNotAvailable() { + // nothing should happen if current vm.max_map_count is not available + maxMapCount.set(-1); + final BootstrapCheck.BootstrapCheckResult result = check.check(BootstrapChecksTests.defaultContext); + assertTrue(result.isSuccess()); + } + public void testGetMaxMapCountOnLinux() { if (Constants.LINUX) { final BootstrapChecks.MaxMapCountCheck check = new BootstrapChecks.MaxMapCountCheck(); @@ -142,7 +207,7 @@ private ParameterizedMessageLoggingExpectation( } @Override - public void match(LogEvent event) { + public void match(final LogEvent event) { if (event.getLevel().equals(level) && event.getLoggerName().equals(loggerName) && event.getMessage() instanceof ParameterizedMessage) { diff --git a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java index a82b932e2b57..000722863887 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java @@ -87,6 +87,8 @@ import java.util.function.Function; import static java.util.Collections.emptyMap; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; public class IndexModuleTests extends ESTestCase { @@ -376,6 +378,21 @@ public void testDisableQueryCacheHasPrecedenceOverForceQueryCache() throws IOExc indexService.close("simon says", false); } + public void testMmapfsStoreTypeNotAllowed() { + final Settings settings = Settings.builder() + .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) + .put("index.store.type", "mmapfs") + .build(); + final Settings nodeSettings = Settings.builder() + .put(IndexModule.NODE_STORE_ALLOW_MMAPFS.getKey(), false) + .build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(new Index("foo", "_na_"), settings, nodeSettings); + final IndexModule module = + new IndexModule(indexSettings, emptyAnalysisRegistry, new InternalEngineFactory(), Collections.emptyMap()); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> newIndexService(module)); + assertThat(e, hasToString(containsString("store type [mmapfs] is not allowed"))); + } + class CustomQueryCache implements QueryCache { @Override diff --git a/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java b/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java index c53d798f7b48..d413c0f0be22 100644 --- a/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.bootstrap.JavaVersion; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.store.IndexStore; import org.elasticsearch.node.MockNode; @@ -32,6 +33,7 @@ import java.util.function.Function; import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasToString; public class IndexStorePluginTests extends ESTestCase { @@ -54,7 +56,30 @@ public Map> getIndexStoreFactories() } - public void testDuplicateIndexStoreProviders() { + public static class ConflictingStorePlugin extends Plugin implements IndexStorePlugin { + + public static final String TYPE; + + static { + TYPE = randomFrom(Arrays.asList(IndexModule.Type.values())).getSettingsKey(); + } + + @Override + public Map> getIndexStoreFactories() { + return Collections.singletonMap(TYPE, IndexStore::new); + } + + } + + public void testIndexStoreFactoryConflictsWithBuiltInIndexStoreType() { + final Settings settings = Settings.builder().put("path.home", createTempDir()).build(); + final IllegalStateException e = expectThrows( + IllegalStateException.class, () -> new MockNode(settings, Collections.singletonList(ConflictingStorePlugin.class))); + assertThat(e, hasToString(containsString( + "registered index store type [" + ConflictingStorePlugin.TYPE + "] conflicts with a built-in type"))); + } + + public void testDuplicateIndexStoreFactories() { final Settings settings = Settings.builder().put("path.home", createTempDir()).build(); final IllegalStateException e = expectThrows( IllegalStateException.class, () -> new MockNode(settings, Arrays.asList(BarStorePlugin.class, FooStorePlugin.class))); From 28d12b05b77472d0596df319d7129cea3ba9154a Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 21 Aug 2018 12:23:21 -0400 Subject: [PATCH 084/283] Move ML tests to be sub-projects of ML (#33026) This commit moves the ML QA tests to be a sub-project of ML. The purpose of this refactoring is to enable ML developers to run :x-pack:plugin:ml:check and run the vast majority of a ML tests with a single command (this still does not contain the ML REST tests, nor the upgrade tests). This simplifies local development for faster iteration. --- x-pack/plugin/ml/build.gradle | 10 ++++++ .../ml/qa/basic-multi-node}/build.gradle | 0 .../ml/integration/MlBasicMultiNodeIT.java | 0 x-pack/plugin/ml/qa/build.gradle | 31 +++++++++++++++++++ .../ml/qa/disabled}/build.gradle | 0 .../ml/integration/MlPluginDisabledIT.java | 0 .../ml/qa/ml-with-security}/build.gradle | 0 .../ml/qa/ml-with-security}/roles.yml | 0 .../smoketest/MlWithSecurityIT.java | 0 .../MlWithSecurityInsufficientRoleIT.java | 0 .../smoketest/MlWithSecurityUserRoleIT.java | 0 .../qa/native-multi-node-tests}/build.gradle | 0 .../integration/AutodetectMemoryLimitIT.java | 0 .../integration/BasicRenormalizationIT.java | 0 .../ml/integration/CategorizationIT.java | 0 .../xpack/ml/integration/DatafeedJobsIT.java | 0 .../ml/integration/DatafeedJobsRestIT.java | 0 .../ml/integration/DeleteExpiredDataIT.java | 0 .../ml/integration/DetectionRulesIT.java | 0 .../xpack/ml/integration/ForecastIT.java | 0 ...erimResultsDeletedAfterReopeningJobIT.java | 0 .../xpack/ml/integration/MlJobIT.java | 0 .../MlNativeAutodetectIntegTestCase.java | 0 .../xpack/ml/integration/ModelPlotsIT.java | 0 .../ml/integration/OverallBucketsIT.java | 0 .../xpack/ml/integration/PersistJobIT.java | 0 .../ReopenJobResetsFinishedTimeIT.java | 0 .../ml/integration/ReopenJobWithGapIT.java | 0 .../integration/RestoreModelSnapshotIT.java | 0 .../ml/integration/RevertModelSnapshotIT.java | 0 .../ml/integration/ScheduledEventsIT.java | 0 .../integration/UpdateInterimResultsIT.java | 0 .../ml/qa/no-bootstrap-tests}/build.gradle | 0 .../NamedPipeHelperNoBootstrapTests.java | 0 .../ml/qa/single-node-tests}/build.gradle | 0 .../ml/transforms/PainlessDomainSplitIT.java | 0 36 files changed, 41 insertions(+) rename x-pack/{qa/ml-basic-multi-node => plugin/ml/qa/basic-multi-node}/build.gradle (100%) rename x-pack/{qa/ml-basic-multi-node => plugin/ml/qa/basic-multi-node}/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java (100%) create mode 100644 x-pack/plugin/ml/qa/build.gradle rename x-pack/{qa/ml-disabled => plugin/ml/qa/disabled}/build.gradle (100%) rename x-pack/{qa/ml-disabled => plugin/ml/qa/disabled}/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java (100%) rename x-pack/{qa/smoke-test-ml-with-security => plugin/ml/qa/ml-with-security}/build.gradle (100%) rename x-pack/{qa/smoke-test-ml-with-security => plugin/ml/qa/ml-with-security}/roles.yml (100%) rename x-pack/{qa/smoke-test-ml-with-security => plugin/ml/qa/ml-with-security}/src/test/java/org/elasticsearch/smoketest/MlWithSecurityIT.java (100%) rename x-pack/{qa/smoke-test-ml-with-security => plugin/ml/qa/ml-with-security}/src/test/java/org/elasticsearch/smoketest/MlWithSecurityInsufficientRoleIT.java (100%) rename x-pack/{qa/smoke-test-ml-with-security => plugin/ml/qa/ml-with-security}/src/test/java/org/elasticsearch/smoketest/MlWithSecurityUserRoleIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/build.gradle (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/CategorizationIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/DetectionRulesIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/ForecastIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/InterimResultsDeletedAfterReopeningJobIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/ModelPlotsIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/OverallBucketsIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/PersistJobIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobResetsFinishedTimeIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobWithGapIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/RestoreModelSnapshotIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/RevertModelSnapshotIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java (100%) rename x-pack/{qa/ml-native-multi-node-tests => plugin/ml/qa/native-multi-node-tests}/src/test/java/org/elasticsearch/xpack/ml/integration/UpdateInterimResultsIT.java (100%) rename x-pack/{qa/ml-no-bootstrap-tests => plugin/ml/qa/no-bootstrap-tests}/build.gradle (100%) rename x-pack/{qa/ml-no-bootstrap-tests => plugin/ml/qa/no-bootstrap-tests}/src/test/java/org/elasticsearch/xpack/ml/utils/NamedPipeHelperNoBootstrapTests.java (100%) rename x-pack/{qa/ml-single-node-tests => plugin/ml/qa/single-node-tests}/build.gradle (100%) rename x-pack/{qa/ml-single-node-tests => plugin/ml/qa/single-node-tests}/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java (100%) diff --git a/x-pack/plugin/ml/build.gradle b/x-pack/plugin/ml/build.gradle index 3602e1b359ec..282ce96fa935 100644 --- a/x-pack/plugin/ml/build.gradle +++ b/x-pack/plugin/ml/build.gradle @@ -103,9 +103,19 @@ task internalClusterTest(type: RandomizedTestingTask, include '**/*IT.class' systemProperty 'es.set.netty.runtime.available.processors', 'false' } + check.dependsOn internalClusterTest internalClusterTest.mustRunAfter test +// add all sub-projects of the qa sub-project +gradle.projectsEvaluated { + project.subprojects + .find { it.path == project.path + ":qa" } + .subprojects + .findAll { it.path.startsWith(project.path + ":qa") } + .each { check.dependsOn it.check } +} + // also add an "alias" task to make typing on the command line easier task icTest { dependsOn internalClusterTest diff --git a/x-pack/qa/ml-basic-multi-node/build.gradle b/x-pack/plugin/ml/qa/basic-multi-node/build.gradle similarity index 100% rename from x-pack/qa/ml-basic-multi-node/build.gradle rename to x-pack/plugin/ml/qa/basic-multi-node/build.gradle diff --git a/x-pack/qa/ml-basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java b/x-pack/plugin/ml/qa/basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java similarity index 100% rename from x-pack/qa/ml-basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java rename to x-pack/plugin/ml/qa/basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java diff --git a/x-pack/plugin/ml/qa/build.gradle b/x-pack/plugin/ml/qa/build.gradle new file mode 100644 index 000000000000..517c93cc1786 --- /dev/null +++ b/x-pack/plugin/ml/qa/build.gradle @@ -0,0 +1,31 @@ +import org.elasticsearch.gradle.test.RestIntegTestTask + +subprojects { + // HACK: please fix this + // we want to add the rest api specs for xpack to qa tests, but we + // need to wait until after the project is evaluated to only apply + // to those that rest tests. this used to be done automatically + // when xpack was a plugin, but now there is no place with xpack as a module. + // instead, we should package these and make them easy to use for rest tests, + // but currently, they must be copied into the resources of the test runner. + project.tasks.withType(RestIntegTestTask) { + File xpackResources = new File(xpackProject('plugin').projectDir, 'src/test/resources') + project.copyRestSpec.from(xpackResources) { + include 'rest-api-spec/api/**' + } + } +} + +gradle.projectsEvaluated { + subprojects { + Task assemble = project.tasks.findByName('assemble') + if (assemble) { + project.tasks.remove(assemble) + project.build.dependsOn.remove('assemble') + } + Task dependenciesInfo = project.tasks.findByName('dependenciesInfo') + if (dependenciesInfo) { + project.precommit.dependsOn.remove('dependenciesInfo') + } + } +} diff --git a/x-pack/qa/ml-disabled/build.gradle b/x-pack/plugin/ml/qa/disabled/build.gradle similarity index 100% rename from x-pack/qa/ml-disabled/build.gradle rename to x-pack/plugin/ml/qa/disabled/build.gradle diff --git a/x-pack/qa/ml-disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java b/x-pack/plugin/ml/qa/disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java similarity index 100% rename from x-pack/qa/ml-disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java rename to x-pack/plugin/ml/qa/disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java diff --git a/x-pack/qa/smoke-test-ml-with-security/build.gradle b/x-pack/plugin/ml/qa/ml-with-security/build.gradle similarity index 100% rename from x-pack/qa/smoke-test-ml-with-security/build.gradle rename to x-pack/plugin/ml/qa/ml-with-security/build.gradle diff --git a/x-pack/qa/smoke-test-ml-with-security/roles.yml b/x-pack/plugin/ml/qa/ml-with-security/roles.yml similarity index 100% rename from x-pack/qa/smoke-test-ml-with-security/roles.yml rename to x-pack/plugin/ml/qa/ml-with-security/roles.yml diff --git a/x-pack/qa/smoke-test-ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityIT.java b/x-pack/plugin/ml/qa/ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityIT.java similarity index 100% rename from x-pack/qa/smoke-test-ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityIT.java rename to x-pack/plugin/ml/qa/ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityIT.java diff --git a/x-pack/qa/smoke-test-ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityInsufficientRoleIT.java b/x-pack/plugin/ml/qa/ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityInsufficientRoleIT.java similarity index 100% rename from x-pack/qa/smoke-test-ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityInsufficientRoleIT.java rename to x-pack/plugin/ml/qa/ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityInsufficientRoleIT.java diff --git a/x-pack/qa/smoke-test-ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityUserRoleIT.java b/x-pack/plugin/ml/qa/ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityUserRoleIT.java similarity index 100% rename from x-pack/qa/smoke-test-ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityUserRoleIT.java rename to x-pack/plugin/ml/qa/ml-with-security/src/test/java/org/elasticsearch/smoketest/MlWithSecurityUserRoleIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/build.gradle b/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/build.gradle rename to x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/BasicRenormalizationIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CategorizationIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CategorizationIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CategorizationIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/CategorizationIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DeleteExpiredDataIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DetectionRulesIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DetectionRulesIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DetectionRulesIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DetectionRulesIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ForecastIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ForecastIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ForecastIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ForecastIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InterimResultsDeletedAfterReopeningJobIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InterimResultsDeletedAfterReopeningJobIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InterimResultsDeletedAfterReopeningJobIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/InterimResultsDeletedAfterReopeningJobIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlNativeAutodetectIntegTestCase.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ModelPlotsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ModelPlotsIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ModelPlotsIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ModelPlotsIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/OverallBucketsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/OverallBucketsIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/OverallBucketsIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/OverallBucketsIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/PersistJobIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/PersistJobIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/PersistJobIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/PersistJobIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobResetsFinishedTimeIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobResetsFinishedTimeIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobResetsFinishedTimeIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobResetsFinishedTimeIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobWithGapIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobWithGapIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobWithGapIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ReopenJobWithGapIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RestoreModelSnapshotIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RestoreModelSnapshotIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RestoreModelSnapshotIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RestoreModelSnapshotIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RevertModelSnapshotIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RevertModelSnapshotIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RevertModelSnapshotIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/RevertModelSnapshotIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/ScheduledEventsIT.java diff --git a/x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/UpdateInterimResultsIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/UpdateInterimResultsIT.java similarity index 100% rename from x-pack/qa/ml-native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/UpdateInterimResultsIT.java rename to x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/UpdateInterimResultsIT.java diff --git a/x-pack/qa/ml-no-bootstrap-tests/build.gradle b/x-pack/plugin/ml/qa/no-bootstrap-tests/build.gradle similarity index 100% rename from x-pack/qa/ml-no-bootstrap-tests/build.gradle rename to x-pack/plugin/ml/qa/no-bootstrap-tests/build.gradle diff --git a/x-pack/qa/ml-no-bootstrap-tests/src/test/java/org/elasticsearch/xpack/ml/utils/NamedPipeHelperNoBootstrapTests.java b/x-pack/plugin/ml/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/xpack/ml/utils/NamedPipeHelperNoBootstrapTests.java similarity index 100% rename from x-pack/qa/ml-no-bootstrap-tests/src/test/java/org/elasticsearch/xpack/ml/utils/NamedPipeHelperNoBootstrapTests.java rename to x-pack/plugin/ml/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/xpack/ml/utils/NamedPipeHelperNoBootstrapTests.java diff --git a/x-pack/qa/ml-single-node-tests/build.gradle b/x-pack/plugin/ml/qa/single-node-tests/build.gradle similarity index 100% rename from x-pack/qa/ml-single-node-tests/build.gradle rename to x-pack/plugin/ml/qa/single-node-tests/build.gradle diff --git a/x-pack/qa/ml-single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java b/x-pack/plugin/ml/qa/single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java similarity index 100% rename from x-pack/qa/ml-single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java rename to x-pack/plugin/ml/qa/single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java From 9623cf6cde3fa6c015933213db16a209b2351ab2 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 21 Aug 2018 12:51:55 -0400 Subject: [PATCH 085/283] Find CCR QA sub-projects automatically (#33027) Today we are by-hand maintaining a list of CCR QA sub-projects that the check task depends on. This commit simplifies this by finding these sub-projects automatically and adding their check task as dependencies of the CCR check task. --- x-pack/plugin/ccr/build.gradle | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/ccr/build.gradle b/x-pack/plugin/ccr/build.gradle index 8be8a4ba4b93..d066769645b5 100644 --- a/x-pack/plugin/ccr/build.gradle +++ b/x-pack/plugin/ccr/build.gradle @@ -32,12 +32,17 @@ task internalClusterTest(type: RandomizedTestingTask, systemProperty 'es.set.netty.runtime.available.processors', 'false' } +check.dependsOn internalClusterTest internalClusterTest.mustRunAfter test -check.dependsOn( - internalClusterTest, - 'qa:multi-cluster:followClusterTest', - 'qa:multi-cluster-with-incompatible-license:followClusterTest', - 'qa:multi-cluster-with-security:followClusterTest') + +// add all sub-projects of the qa sub-project +gradle.projectsEvaluated { + project.subprojects + .find { it.path == project.path + ":qa" } + .subprojects + .findAll { it.path.startsWith(project.path + ":qa") } + .each { check.dependsOn it.check } +} dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" From 28a0df2c7fbaa46fa3dd5df671bdc6fa16fd3711 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 21 Aug 2018 18:12:28 +0100 Subject: [PATCH 086/283] HLRC: Clear ML data after client tests (#33023) This commit duplicates the `MlRestTestStateCleaner` to make sure all ML data is removed after each test. After implementing the job and datafeed APIs in the HLRC, we shall replace this implementation with one using the HLRC itself. Closes #32993 --- .../client/MachineLearningIT.java | 9 +- .../client/MlRestTestStateCleaner.java | 109 ++++++++++++++++++ .../MlClientDocumentationIT.java | 8 ++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/MlRestTestStateCleaner.java diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index 6c4fa6e4514a..2c0fc70b8486 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 @@ -19,7 +19,6 @@ package org.elasticsearch.client; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; -import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; @@ -33,15 +32,21 @@ import org.elasticsearch.protocol.xpack.ml.job.config.DataDescription; import org.elasticsearch.protocol.xpack.ml.job.config.Detector; import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.junit.After; +import java.io.IOException; import java.util.Arrays; import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.is; -@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32993") public class MachineLearningIT extends ESRestHighLevelClientTestCase { + @After + public void cleanUp() throws IOException { + new MlRestTestStateCleaner(logger, client()).clearMlMetadata(); + } + public void testPutJob() throws Exception { String jobId = randomValidJobId(); Job job = buildJob(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MlRestTestStateCleaner.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MlRestTestStateCleaner.java new file mode 100644 index 000000000000..7ad86576245e --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MlRestTestStateCleaner.java @@ -0,0 +1,109 @@ +/* + * 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; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.test.rest.ESRestTestCase; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * This is temporarily duplicated from the server side. + * @TODO Replace with an implementation using the HLRC once + * the APIs for managing datafeeds are implemented. + */ +public class MlRestTestStateCleaner { + + private final Logger logger; + private final RestClient adminClient; + + public MlRestTestStateCleaner(Logger logger, RestClient adminClient) { + this.logger = logger; + this.adminClient = adminClient; + } + + public void clearMlMetadata() throws IOException { + deleteAllDatafeeds(); + deleteAllJobs(); + // indices will be deleted by the ESRestTestCase class + } + + @SuppressWarnings("unchecked") + private void deleteAllDatafeeds() throws IOException { + final Request datafeedsRequest = new Request("GET", "/_xpack/ml/datafeeds"); + datafeedsRequest.addParameter("filter_path", "datafeeds"); + final Response datafeedsResponse = adminClient.performRequest(datafeedsRequest); + final List> datafeeds = + (List>) XContentMapValues.extractValue("datafeeds", ESRestTestCase.entityAsMap(datafeedsResponse)); + if (datafeeds == null) { + return; + } + + try { + adminClient.performRequest(new Request("POST", "/_xpack/ml/datafeeds/_all/_stop")); + } catch (Exception e1) { + logger.warn("failed to stop all datafeeds. Forcing stop", e1); + try { + adminClient.performRequest(new Request("POST", "/_xpack/ml/datafeeds/_all/_stop?force=true")); + } catch (Exception e2) { + logger.warn("Force-closing all data feeds failed", e2); + } + throw new RuntimeException( + "Had to resort to force-stopping datafeeds, something went wrong?", e1); + } + + for (Map datafeed : datafeeds) { + String datafeedId = (String) datafeed.get("datafeed_id"); + adminClient.performRequest(new Request("DELETE", "/_xpack/ml/datafeeds/" + datafeedId)); + } + } + + private void deleteAllJobs() throws IOException { + final Request jobsRequest = new Request("GET", "/_xpack/ml/anomaly_detectors"); + jobsRequest.addParameter("filter_path", "jobs"); + final Response response = adminClient.performRequest(jobsRequest); + @SuppressWarnings("unchecked") + final List> jobConfigs = + (List>) XContentMapValues.extractValue("jobs", ESRestTestCase.entityAsMap(response)); + if (jobConfigs == null) { + return; + } + + try { + adminClient.performRequest(new Request("POST", "/_xpack/ml/anomaly_detectors/_all/_close")); + } catch (Exception e1) { + logger.warn("failed to close all jobs. Forcing closed", e1); + try { + adminClient.performRequest(new Request("POST", "/_xpack/ml/anomaly_detectors/_all/_close?force=true")); + } catch (Exception e2) { + logger.warn("Force-closing all jobs failed", e2); + } + throw new RuntimeException("Had to resort to force-closing jobs, something went wrong?", + e1); + } + + for (Map jobConfig : jobConfigs) { + String jobId = (String) jobConfig.get("job_id"); + adminClient.performRequest(new Request("DELETE", "/_xpack/ml/anomaly_detectors/" + jobId)); + } + } +} 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 05aad96fff6a..6e48036419b7 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 @@ -22,6 +22,7 @@ import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.MachineLearningIT; +import org.elasticsearch.client.MlRestTestStateCleaner; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.unit.TimeValue; @@ -37,7 +38,9 @@ import org.elasticsearch.protocol.xpack.ml.job.config.DataDescription; import org.elasticsearch.protocol.xpack.ml.job.config.Detector; import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.junit.After; +import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.List; @@ -48,6 +51,11 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { + @After + public void cleanUp() throws IOException { + new MlRestTestStateCleaner(logger, client()).clearMlMetadata(); + } + public void testCreateJob() throws Exception { RestHighLevelClient client = highLevelClient(); From fcf8cadd9a5114da8b8536338d058289daa88d99 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 21 Aug 2018 14:48:53 -0400 Subject: [PATCH 087/283] Switch some x-pack tests to new style Requests (#32500) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. This changes all calls in the `x-pack/qa/audit-tests`, `x-pack/qa/ml-disabled`, and `x-pack/qa/multi-node` projects to use the new versions. --- .../ml/integration/MlPluginDisabledIT.java | 59 +++++++++++-------- .../xpack/security/audit/IndexAuditIT.java | 11 ++-- .../GlobalCheckpointSyncActionIT.java | 29 +++++---- 3 files changed, 53 insertions(+), 46 deletions(-) diff --git a/x-pack/plugin/ml/qa/disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java b/x-pack/plugin/ml/qa/disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java index 3bb9566e5bf1..170b4f14486b 100644 --- a/x-pack/plugin/ml/qa/disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java +++ b/x-pack/plugin/ml/qa/disabled/src/test/java/org/elasticsearch/xpack/ml/integration/MlPluginDisabledIT.java @@ -5,16 +5,13 @@ */ package org.elasticsearch.xpack.ml.integration; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; +import org.elasticsearch.client.Request; import org.elasticsearch.client.ResponseException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xpack.ml.MachineLearning; -import java.util.Collections; - import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.containsString; @@ -27,30 +24,40 @@ public class MlPluginDisabledIT extends ESRestTestCase { public void testActionsFail() throws Exception { XContentBuilder xContentBuilder = jsonBuilder(); xContentBuilder.startObject(); - xContentBuilder.field("actions-fail-job", "foo"); - xContentBuilder.field("description", "Analysis of response time by airline"); - - xContentBuilder.startObject("analysis_config"); - xContentBuilder.field("bucket_span", "3600s"); - xContentBuilder.startArray("detectors"); - xContentBuilder.startObject(); - xContentBuilder.field("function", "metric"); - xContentBuilder.field("field_name", "responsetime"); - xContentBuilder.field("by_field_name", "airline"); - xContentBuilder.endObject(); - xContentBuilder.endArray(); - xContentBuilder.endObject(); - - xContentBuilder.startObject("data_description"); - xContentBuilder.field("format", "xcontent"); - xContentBuilder.field("time_field", "time"); - xContentBuilder.field("time_format", "epoch"); - xContentBuilder.endObject(); + { + xContentBuilder.field("actions-fail-job", "foo"); + xContentBuilder.field("description", "Analysis of response time by airline"); + + xContentBuilder.startObject("analysis_config"); + { + xContentBuilder.field("bucket_span", "3600s"); + xContentBuilder.startArray("detectors"); + { + xContentBuilder.startObject(); + { + xContentBuilder.field("function", "metric"); + xContentBuilder.field("field_name", "responsetime"); + xContentBuilder.field("by_field_name", "airline"); + } + xContentBuilder.endObject(); + } + xContentBuilder.endArray(); + } + xContentBuilder.endObject(); + + xContentBuilder.startObject("data_description"); + { + xContentBuilder.field("format", "xcontent"); + xContentBuilder.field("time_field", "time"); + xContentBuilder.field("time_format", "epoch"); + } + xContentBuilder.endObject(); + } xContentBuilder.endObject(); - ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest("put", - MachineLearning.BASE_PATH + "anomaly_detectors/foo", Collections.emptyMap(), - new StringEntity(Strings.toString(xContentBuilder), ContentType.APPLICATION_JSON))); + Request request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/foo"); + request.setJsonEntity(Strings.toString(xContentBuilder)); + ResponseException exception = expectThrows(ResponseException.class, () -> client().performRequest(request)); assertThat(exception.getMessage(), containsString("no handler found for uri [/_xpack/ml/anomaly_detectors/foo] and method [PUT]")); } } diff --git a/x-pack/qa/audit-tests/src/test/java/org/elasticsearch/xpack/security/audit/IndexAuditIT.java b/x-pack/qa/audit-tests/src/test/java/org/elasticsearch/xpack/security/audit/IndexAuditIT.java index c0111e57c744..d1ee4f2d9e10 100644 --- a/x-pack/qa/audit-tests/src/test/java/org/elasticsearch/xpack/security/audit/IndexAuditIT.java +++ b/x-pack/qa/audit-tests/src/test/java/org/elasticsearch/xpack/security/audit/IndexAuditIT.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.security.audit; import com.carrotsearch.hppc.cursors.ObjectCursor; -import org.apache.http.message.BasicHeader; import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesResponse; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; @@ -111,10 +110,12 @@ public NamedWriteableRegistry getNamedWriteableRegistry() { } public void testIndexAuditTrailWorking() throws Exception { - Response response = getRestClient().performRequest("GET", "/", - new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, - UsernamePasswordToken.basicAuthHeaderValue(USER, new SecureString(PASS.toCharArray())))); - assertThat(response.getStatusLine().getStatusCode(), is(200)); + Request request = new Request("GET", "/"); + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, + UsernamePasswordToken.basicAuthHeaderValue(USER, new SecureString(PASS.toCharArray()))); + request.setOptions(options); + Response response = getRestClient().performRequest(request); final AtomicReference lastClusterState = new AtomicReference<>(); final boolean found = awaitSecurityAuditIndex(lastClusterState, QueryBuilders.matchQuery("principal", USER)); diff --git a/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java b/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java index abc784b4cb28..18cd67ff271a 100644 --- a/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java +++ b/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/GlobalCheckpointSyncActionIT.java @@ -5,8 +5,7 @@ */ package org.elasticsearch.multi_node; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; @@ -16,10 +15,6 @@ import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.test.rest.yaml.ObjectPath; -import java.util.Collections; -import java.util.HashMap; -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.equalTo; @@ -59,12 +54,15 @@ public void testGlobalCheckpointSyncActionRunsAsPrivilegedUser() throws Exceptio builder.endObject(); } builder.endObject(); - final StringEntity entity = new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); - client().performRequest("PUT", "test-index", Collections.emptyMap(), entity); + Request createIndexRequest = new Request("PUT", "/test-index"); + createIndexRequest.setJsonEntity(Strings.toString(builder)); + client().performRequest(createIndexRequest); } // wait for the replica to recover - client().performRequest("GET", "/_cluster/health", Collections.singletonMap("wait_for_status", "green")); + Request healthRequest = new Request("GET", "/_cluster/health"); + healthRequest.addParameter("wait_for_status", "green"); + client().performRequest(healthRequest); // index some documents final int numberOfDocuments = randomIntBetween(0, 128); @@ -75,17 +73,18 @@ public void testGlobalCheckpointSyncActionRunsAsPrivilegedUser() throws Exceptio builder.field("foo", i); } builder.endObject(); - final StringEntity entity = new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); - client().performRequest("PUT", "/test-index/test-type/" + i, Collections.emptyMap(), entity); + Request indexRequest = new Request("PUT", "/test-index/test-type/" + i); + indexRequest.setJsonEntity(Strings.toString(builder)); + client().performRequest(indexRequest); } } // we have to wait for the post-operation global checkpoint sync to propagate to the replica assertBusy(() -> { - final Map params = new HashMap<>(2); - params.put("level", "shards"); - params.put("filter_path", "**.seq_no"); - final Response response = client().performRequest("GET", "/test-index/_stats", params); + final Request request = new Request("GET", "/test-index/_stats"); + request.addParameter("level", "shards"); + request.addParameter("filter_path", "**.seq_no"); + final Response response = client().performRequest(request); final ObjectPath path = ObjectPath.createFromResponse(response); // int looks funny here since global checkpoints are longs but the response parser does not know enough to treat them as long final int shard0GlobalCheckpoint = path.evaluate("indices.test-index.shards.0.0.seq_no.global_checkpoint"); From 3973bb4028cd0536eef0f178fb66f984d3a74246 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Tue, 21 Aug 2018 14:59:37 -0400 Subject: [PATCH 088/283] Fix north pole overflow error in GeoHashUtils.bbox() (#32891) Fixes an overflow error in GeoHashUtils.bbox() calculation of a bounding box for geohashes with maximum precision located next to the north pole. --- .../common/geo/GeoHashUtils.java | 23 ++++++++++++++----- .../common/geo/GeoHashTests.java | 5 ++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/geo/GeoHashUtils.java b/server/src/main/java/org/elasticsearch/common/geo/GeoHashUtils.java index 0ee8d095f49a..bf65162d215b 100644 --- a/server/src/main/java/org/elasticsearch/common/geo/GeoHashUtils.java +++ b/server/src/main/java/org/elasticsearch/common/geo/GeoHashUtils.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.Collection; +import static org.apache.lucene.geo.GeoUtils.MAX_LAT_INCL; + /** * Utilities for converting to/from the GeoHash standard * @@ -48,6 +50,8 @@ public class GeoHashUtils { private static final double LAT_SCALE = (0x1L<>>= 4; - // deinterleave and add 1 to lat and lon to get topRight - long lat = BitUtil.deinterleave(ghLong >>> 1) + 1; - long lon = BitUtil.deinterleave(ghLong) + 1; - GeoPoint topRight = GeoPoint.fromGeohash(BitUtil.interleave((int)lon, (int)lat) << 4 | len); - - return new Rectangle(bottomLeft.lat(), topRight.lat(), bottomLeft.lon(), topRight.lon()); + // deinterleave + long lon = BitUtil.deinterleave(ghLong >>> 1); + long lat = BitUtil.deinterleave(ghLong); + if (lat < MAX_LAT_BITS) { + // add 1 to lat and lon to get topRight + GeoPoint topRight = GeoPoint.fromGeohash(BitUtil.interleave((int)(lat + 1), (int)(lon + 1)) << 4 | len); + return new Rectangle(bottomLeft.lat(), topRight.lat(), bottomLeft.lon(), topRight.lon()); + } else { + // We cannot go north of north pole, so just using 90 degrees instead of calculating it using + // add 1 to lon to get lon of topRight, we are going to use 90 for lat + GeoPoint topRight = GeoPoint.fromGeohash(BitUtil.interleave((int)lat, (int)(lon + 1)) << 4 | len); + return new Rectangle(bottomLeft.lat(), MAX_LAT_INCL, bottomLeft.lon(), topRight.lon()); + } } /** diff --git a/server/src/test/java/org/elasticsearch/common/geo/GeoHashTests.java b/server/src/test/java/org/elasticsearch/common/geo/GeoHashTests.java index 87f98389231e..b4a24cfc4fcd 100644 --- a/server/src/test/java/org/elasticsearch/common/geo/GeoHashTests.java +++ b/server/src/test/java/org/elasticsearch/common/geo/GeoHashTests.java @@ -98,6 +98,11 @@ public void testLongGeohashes() { } } + public void testNorthPoleBoundingBox() { + Rectangle bbox = GeoHashUtils.bbox("zzbxfpgzupbx"); // Bounding box with maximum precision touching north pole + assertEquals(90.0, bbox.maxLat, 0.0000001); // Should be 90 degrees + } + public void testInvalidGeohashes() { IllegalArgumentException ex; From f311680176bd68d74308bd5173d9703a8c45f263 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 21 Aug 2018 16:04:16 -0400 Subject: [PATCH 089/283] Monitoring: Clean up MonitoringIT We recently reenabled MonitoringIT to hunt down #29880 but some of its assertions were out of date. This updates the assertions. --- .../xpack/monitoring/integration/MonitoringIT.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java index 0ee5f85bfad2..e44d6da073ef 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java @@ -67,6 +67,7 @@ import static org.elasticsearch.threadpool.ThreadPool.Names.WRITE; import static org.elasticsearch.xpack.core.monitoring.exporter.MonitoringTemplateUtils.TEMPLATE_VERSION; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -336,7 +337,7 @@ private void assertClusterStatsMonitoringDoc(final Map document, final Map clusterStats = (Map) source.get("cluster_stats"); assertThat(clusterStats, notNullValue()); - assertThat(clusterStats.size(), equalTo(4)); + assertThat(clusterStats.size(), equalTo(5)); final Map stackStats = (Map) source.get("stack_stats"); assertThat(stackStats, notNullValue()); @@ -346,7 +347,7 @@ private void assertClusterStatsMonitoringDoc(final Map document, assertThat(apm, notNullValue()); assertThat(apm.size(), equalTo(1)); assertThat(apm.remove("found"), is(apmIndicesExist)); - assertThat(apm.isEmpty(), is(true)); + assertThat(apm.keySet(), empty()); final Map xpackStats = (Map) stackStats.get("xpack"); assertThat(xpackStats, notNullValue()); @@ -358,14 +359,14 @@ private void assertClusterStatsMonitoringDoc(final Map document, final Map clusterState = (Map) source.get("cluster_state"); assertThat(clusterState, notNullValue()); - assertThat(clusterState.size(), equalTo(6)); assertThat(clusterState.remove("nodes_hash"), notNullValue()); assertThat(clusterState.remove("status"), notNullValue()); assertThat(clusterState.remove("version"), notNullValue()); assertThat(clusterState.remove("state_uuid"), notNullValue()); + assertThat(clusterState.remove("cluster_uuid"), notNullValue()); assertThat(clusterState.remove("master_node"), notNullValue()); assertThat(clusterState.remove("nodes"), notNullValue()); - assertThat(clusterState.isEmpty(), is(true)); + assertThat(clusterState.keySet(), empty()); } /** @@ -451,6 +452,11 @@ private void assertNodeStatsMonitoringDoc(final Map document) { return; } + // bulk is not a thread pool in the current version but we allow it to support mixed version clusters + if (filter.startsWith("node_stats.thread_pool.bulk")) { + return; + } + assertThat(filter + " must not be null in the monitoring document", extractValue(filter, source), notNullValue()); }); } From 8b43e21521b4d0f3409439d428eda156d8744cf1 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Tue, 21 Aug 2018 22:12:53 +0200 Subject: [PATCH 090/283] Fix multi fields empty query (#33017) This change fixes empty query removal when all fields remove the search term in `simple_query_string`, `multi_match` and `query_string`. Closes #33009 --- .../index/query/QueryStringQueryBuilder.java | 9 +-- .../index/search/MultiMatchQuery.java | 2 +- .../index/search/QueryStringQueryParser.java | 3 + .../query/MultiMatchQueryBuilderTests.java | 64 +++++++++++++++++++ .../query/QueryStringQueryBuilderTests.java | 53 ++++++++++++++- .../query/SimpleQueryStringBuilderTests.java | 50 ++++++++++++++- 6 files changed, 172 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java index e9d53d8e8294..19687464edca 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java @@ -328,8 +328,9 @@ public Map fields() { /** * @param type Sets how multiple fields should be combined to build textual part queries. */ - public void type(MultiMatchQueryBuilder.Type type) { + public QueryStringQueryBuilder type(MultiMatchQueryBuilder.Type type) { this.type = type; + return this; } /** @@ -388,7 +389,7 @@ public QueryStringQueryBuilder analyzer(String analyzer) { this.analyzer = analyzer; return this; } - + /** * The optional analyzer used to analyze the query string. Note, if a field has search analyzer * defined for it, then it will be used automatically. Defaults to the smart search analyzer. @@ -899,9 +900,9 @@ protected boolean doEquals(QueryStringQueryBuilder other) { Objects.equals(tieBreaker, other.tieBreaker) && Objects.equals(rewrite, other.rewrite) && Objects.equals(minimumShouldMatch, other.minimumShouldMatch) && - Objects.equals(lenient, other.lenient) && + Objects.equals(lenient, other.lenient) && Objects.equals( - timeZone == null ? null : timeZone.getID(), + timeZone == null ? null : timeZone.getID(), other.timeZone == null ? null : other.timeZone.getID()) && Objects.equals(escape, other.escape) && Objects.equals(maxDeterminizedStates, other.maxDeterminizedStates) && diff --git a/server/src/main/java/org/elasticsearch/index/search/MultiMatchQuery.java b/server/src/main/java/org/elasticsearch/index/search/MultiMatchQuery.java index 8b33f2df8b16..89cebf38a401 100644 --- a/server/src/main/java/org/elasticsearch/index/search/MultiMatchQuery.java +++ b/server/src/main/java/org/elasticsearch/index/search/MultiMatchQuery.java @@ -120,7 +120,7 @@ public Query parseGroup(Type type, String field, Float boostValue, Object value, private Query combineGrouped(List groupQuery) { if (groupQuery == null || groupQuery.isEmpty()) { - return new MatchNoDocsQuery("[multi_match] list of group queries was empty"); + return zeroTermsQuery(); } if (groupQuery.size() == 1) { return groupQuery.get(0); diff --git a/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java b/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java index 50406ed58348..04a1bf122e66 100644 --- a/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java +++ b/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java @@ -347,6 +347,9 @@ protected Query getFieldQuery(String field, String queryText, int slop) throws P } queryBuilder.setPhraseSlop(slop); Query query = queryBuilder.parse(MultiMatchQueryBuilder.Type.PHRASE, fields, queryText, null); + if (query == null) { + return null; + } return applySlop(query, slop); } catch (IOException e) { throw new ParseException(e.getMessage()); diff --git a/server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderTests.java index e30cdaca4020..2f69ef7674d4 100644 --- a/server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/MultiMatchQueryBuilderTests.java @@ -21,6 +21,7 @@ import org.apache.lucene.index.Term; import org.apache.lucene.queries.ExtendedCommonTermsQuery; +import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.DisjunctionMaxQuery; @@ -381,6 +382,69 @@ public void testDefaultField() throws Exception { assertEquals(expected, query); } + public void testWithStopWords() throws Exception { + Query query = new MultiMatchQueryBuilder("the quick fox") + .field(STRING_FIELD_NAME) + .analyzer("stop") + .toQuery(createShardContext()); + Query expected = new BooleanQuery.Builder() + .add(new TermQuery(new Term(STRING_FIELD_NAME, "quick")), BooleanClause.Occur.SHOULD) + .add(new TermQuery(new Term(STRING_FIELD_NAME, "fox")), BooleanClause.Occur.SHOULD) + .build(); + assertEquals(expected, query); + + query = new MultiMatchQueryBuilder("the quick fox") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + .toQuery(createShardContext()); + expected = new DisjunctionMaxQuery( + Arrays.asList( + new BooleanQuery.Builder() + .add(new TermQuery(new Term(STRING_FIELD_NAME, "quick")), BooleanClause.Occur.SHOULD) + .add(new TermQuery(new Term(STRING_FIELD_NAME, "fox")), BooleanClause.Occur.SHOULD) + .build(), + new BooleanQuery.Builder() + .add(new TermQuery(new Term(STRING_FIELD_NAME_2, "quick")), BooleanClause.Occur.SHOULD) + .add(new TermQuery(new Term(STRING_FIELD_NAME_2, "fox")), BooleanClause.Occur.SHOULD) + .build() + ), 0f); + assertEquals(expected, query); + + query = new MultiMatchQueryBuilder("the") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + .toQuery(createShardContext()); + expected = new DisjunctionMaxQuery(Arrays.asList(new MatchNoDocsQuery(), new MatchNoDocsQuery()), 0f); + assertEquals(expected, query); + + query = new BoolQueryBuilder() + .should( + new MultiMatchQueryBuilder("the") + .field(STRING_FIELD_NAME) + .analyzer("stop") + ) + .toQuery(createShardContext()); + expected = new BooleanQuery.Builder() + .add(new MatchNoDocsQuery(), BooleanClause.Occur.SHOULD) + .build(); + assertEquals(expected, query); + + query = new BoolQueryBuilder() + .should( + new MultiMatchQueryBuilder("the") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + ) + .toQuery(createShardContext()); + expected = new BooleanQuery.Builder() + .add(new DisjunctionMaxQuery(Arrays.asList(new MatchNoDocsQuery(), new MatchNoDocsQuery()), 0f), BooleanClause.Occur.SHOULD) + .build(); + assertEquals(expected, query); + } + private static IndexMetaData newIndexMeta(String name, Settings oldIndexSettings, Settings indexSettings) { Settings build = Settings.builder().put(oldIndexSettings) .put(indexSettings) diff --git a/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java index 87197b662d14..81895f4c9b88 100644 --- a/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java @@ -1266,11 +1266,58 @@ public void testWithStopWords() throws Exception { .field(STRING_FIELD_NAME) .analyzer("stop") .toQuery(createShardContext()); - BooleanQuery expected = new BooleanQuery.Builder() - .add(new TermQuery(new Term(STRING_FIELD_NAME, "quick")), Occur.SHOULD) - .add(new TermQuery(new Term(STRING_FIELD_NAME, "fox")), Occur.SHOULD) + Query expected = new BooleanQuery.Builder() + .add(new TermQuery(new Term(STRING_FIELD_NAME, "quick")), BooleanClause.Occur.SHOULD) + .add(new TermQuery(new Term(STRING_FIELD_NAME, "fox")), BooleanClause.Occur.SHOULD) .build(); assertEquals(expected, query); + + query = new QueryStringQueryBuilder("the quick fox") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + .toQuery(createShardContext()); + expected = new DisjunctionMaxQuery( + Arrays.asList( + new BooleanQuery.Builder() + .add(new TermQuery(new Term(STRING_FIELD_NAME, "quick")), Occur.SHOULD) + .add(new TermQuery(new Term(STRING_FIELD_NAME, "fox")), Occur.SHOULD) + .build(), + new BooleanQuery.Builder() + .add(new TermQuery(new Term(STRING_FIELD_NAME_2, "quick")), Occur.SHOULD) + .add(new TermQuery(new Term(STRING_FIELD_NAME_2, "fox")), Occur.SHOULD) + .build() + ), 0f); + assertEquals(expected, query); + + query = new QueryStringQueryBuilder("the") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + .toQuery(createShardContext()); + assertEquals(new BooleanQuery.Builder().build(), query); + + query = new BoolQueryBuilder() + .should( + new QueryStringQueryBuilder("the") + .field(STRING_FIELD_NAME) + .analyzer("stop") + ) + .toQuery(createShardContext()); + expected = new BooleanQuery.Builder() + .add(new BooleanQuery.Builder().build(), BooleanClause.Occur.SHOULD) + .build(); + assertEquals(expected, query); + + query = new BoolQueryBuilder() + .should( + new QueryStringQueryBuilder("the") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + ) + .toQuery(createShardContext()); + assertEquals(expected, query); } public void testWithPrefixStopWords() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java index 6cde10308c6e..36da37c44c66 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java @@ -613,11 +613,59 @@ public void testWithStopWords() throws Exception { .field(STRING_FIELD_NAME) .analyzer("stop") .toQuery(createShardContext()); - BooleanQuery expected = new BooleanQuery.Builder() + Query expected = new BooleanQuery.Builder() .add(new TermQuery(new Term(STRING_FIELD_NAME, "quick")), BooleanClause.Occur.SHOULD) .add(new TermQuery(new Term(STRING_FIELD_NAME, "fox")), BooleanClause.Occur.SHOULD) .build(); assertEquals(expected, query); + + query = new SimpleQueryStringBuilder("the quick fox") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + .toQuery(createShardContext()); + expected = new BooleanQuery.Builder() + .add(new DisjunctionMaxQuery( + Arrays.asList( + new TermQuery(new Term(STRING_FIELD_NAME, "quick")), + new TermQuery(new Term(STRING_FIELD_NAME_2, "quick")) + ), 1.0f), BooleanClause.Occur.SHOULD) + .add(new DisjunctionMaxQuery( + Arrays.asList( + new TermQuery(new Term(STRING_FIELD_NAME, "fox")), + new TermQuery(new Term(STRING_FIELD_NAME_2, "fox")) + ), 1.0f), BooleanClause.Occur.SHOULD) + .build(); + assertEquals(expected, query); + + query = new SimpleQueryStringBuilder("the") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + .toQuery(createShardContext()); + assertEquals(new MatchNoDocsQuery(), query); + + query = new BoolQueryBuilder() + .should( + new SimpleQueryStringBuilder("the") + .field(STRING_FIELD_NAME) + .analyzer("stop") + ) + .toQuery(createShardContext()); + expected = new BooleanQuery.Builder() + .add(new MatchNoDocsQuery(), BooleanClause.Occur.SHOULD) + .build(); + assertEquals(expected, query); + + query = new BoolQueryBuilder() + .should( + new SimpleQueryStringBuilder("the") + .field(STRING_FIELD_NAME) + .field(STRING_FIELD_NAME_2) + .analyzer("stop") + ) + .toQuery(createShardContext()); + assertEquals(expected, query); } public void testWithPrefixStopWords() throws Exception { From 15727ae8ed6e9a5abf03009b15e1ec93c051fa50 Mon Sep 17 00:00:00 2001 From: debadair Date: Tue, 21 Aug 2018 13:13:07 -0700 Subject: [PATCH 091/283] [DOCS] Fixed formatting of Example headings. (#33038) --- docs/painless/painless-execute-script.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index 2aca95977869..30320def79b2 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -26,7 +26,7 @@ The only variable that is available is `params`, which can be used to access use The result of the script is always converted to a string. If no context is specified then this context is used by default. -====== Example +*Example* Request: @@ -67,7 +67,7 @@ The following parameters may be specified in `context_setup` for a filter contex document:: Contains the document that will be temporarily indexed in-memory and is accessible from the script. index:: The name of an index containing a mapping that is compatable with the document being indexed. -====== Example +*Example* [source,js] ---------------------------------------------------------------- @@ -125,7 +125,7 @@ document:: Contains the document that will be temporarily indexed in-memory and index:: The name of an index containing a mapping that is compatable with the document being indexed. query:: If `_score` is used in the script then a query can specified that will be used to compute a score. -====== Example +*Example* [source,js] ---------------------------------------------------------------- From 767c69593c67befb843686de8ea51b7bc87728c9 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Tue, 21 Aug 2018 22:15:09 +0200 Subject: [PATCH 092/283] Fix quoted _exists_ query (#33019) This change in the `query_string` query fixes the detection of the special `_exists_` field when it is used with a quoted term. Closes #28922 --- .../index/search/QueryStringQueryParser.java | 12 ++++++++---- .../index/query/QueryStringQueryBuilderTests.java | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java b/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java index 04a1bf122e66..fa2fd033bee0 100644 --- a/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java +++ b/server/src/main/java/org/elasticsearch/index/search/QueryStringQueryParser.java @@ -280,14 +280,14 @@ protected Query newMatchAllDocsQuery() { @Override public Query getFieldQuery(String field, String queryText, boolean quoted) throws ParseException { - if (quoted) { - return getFieldQuery(field, queryText, getPhraseSlop()); - } - if (field != null && EXISTS_FIELD.equals(field)) { return existsQuery(queryText); } + if (quoted) { + return getFieldQuery(field, queryText, getPhraseSlop()); + } + // Detects additional operators '<', '<=', '>', '>=' to handle range query with one side unbounded. // It is required to use a prefix field operator to enable the detection since they are not treated // as logical operator by the query parser (e.g. age:>=10). @@ -333,6 +333,10 @@ public Query getFieldQuery(String field, String queryText, boolean quoted) throw @Override protected Query getFieldQuery(String field, String queryText, int slop) throws ParseException { + if (field != null && EXISTS_FIELD.equals(field)) { + return existsQuery(queryText); + } + Map fields = extractMultiFields(field, true); if (fields.isEmpty()) { return newUnmappedFieldQuery(field); diff --git a/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java index 81895f4c9b88..b0ee32548737 100644 --- a/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java @@ -998,6 +998,18 @@ public void testExistsFieldQuery() throws Exception { } else { assertThat(query, equalTo(new ConstantScoreQuery(new TermQuery(new Term("_field_names", STRING_FIELD_NAME))))); } + + for (boolean quoted : new boolean[] {true, false}) { + String value = (quoted ? "\"" : "") + STRING_FIELD_NAME + (quoted ? "\"" : ""); + queryBuilder = new QueryStringQueryBuilder("_exists_:" + value); + query = queryBuilder.toQuery(context); + if (context.getIndexSettings().getIndexVersionCreated().onOrAfter(Version.V_6_1_0) + && (context.fieldMapper(STRING_FIELD_NAME).omitNorms() == false)) { + assertThat(query, equalTo(new ConstantScoreQuery(new NormsFieldExistsQuery(STRING_FIELD_NAME)))); + } else { + assertThat(query, equalTo(new ConstantScoreQuery(new TermQuery(new Term("_field_names", STRING_FIELD_NAME))))); + } + } QueryShardContext contextNoType = createShardContextWithNoType(); query = queryBuilder.toQuery(contextNoType); assertThat(query, equalTo(new MatchNoDocsQuery())); From 67b5a83a9ac1e28141ceee390e4aabe601192b14 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Tue, 21 Aug 2018 16:33:42 -0700 Subject: [PATCH 093/283] Ensure that _exists queries on keyword fields use norms when they're available. (#33006) --- .../index/mapper/KeywordFieldMapper.java | 17 +++++++++----- .../index/mapper/TypeParsers.java | 5 ++--- .../index/mapper/KeywordFieldMapperTests.java | 16 ++++++++++---- .../index/mapper/KeywordFieldTypeTests.java | 22 +++++++++++++++++-- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java index 9c334f795511..20b4bb37cc7a 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java @@ -28,6 +28,7 @@ import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.Term; import org.apache.lucene.search.DocValuesFieldExistsQuery; +import org.apache.lucene.search.NormsFieldExistsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRef; @@ -166,7 +167,7 @@ public Mapper.Builder parse(String name, Map node, ParserCo builder.ignoreAbove(XContentMapValues.nodeIntegerValue(propNode, -1)); iterator.remove(); } else if (propName.equals("norms")) { - builder.omitNorms(XContentMapValues.nodeBooleanValue(propNode, "norms") == false); + TypeParsers.parseNorms(builder, name, propNode); iterator.remove(); } else if (propName.equals("eager_global_ordinals")) { builder.eagerGlobalOrdinals(XContentMapValues.nodeBooleanValue(propNode, "eager_global_ordinals")); @@ -256,8 +257,10 @@ public void setSplitQueriesOnWhitespace(boolean splitQueriesOnWhitespace) { public Query existsQuery(QueryShardContext context) { if (hasDocValues()) { return new DocValuesFieldExistsQuery(name()); - } else { + } else if (omitNorms()) { return new TermQuery(new Term(FieldNamesFieldMapper.NAME, name())); + } else { + return new NormsFieldExistsQuery(name()); } } @@ -366,17 +369,19 @@ protected void parseCreateField(ParseContext context, List field // convert to utf8 only once before feeding postings/dv/stored fields final BytesRef binaryValue = new BytesRef(value); - if (fieldType().indexOptions() != IndexOptions.NONE || fieldType().stored()) { + if (fieldType().indexOptions() != IndexOptions.NONE || fieldType().stored()) { Field field = new Field(fieldType().name(), binaryValue, fieldType()); fields.add(field); + + if (fieldType().hasDocValues() == false && fieldType().omitNorms()) { + createFieldNamesField(context, fields); + } } + if (fieldType().hasDocValues()) { fields.add(new SortedSetDocValuesField(fieldType().name(), binaryValue)); - } else if (fieldType().stored() || fieldType().indexOptions() != IndexOptions.NONE) { - createFieldNamesField(context, fields); } } - @Override protected String contentType() { return CONTENT_TYPE; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java index a6a5fab0d04f..667f4a736173 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java @@ -122,8 +122,7 @@ private static void parseAnalyzersAndTermVectors(FieldMapper.Builder builder, St } } - public static void parseNorms(FieldMapper.Builder builder, String fieldName, Object propNode, - Mapper.TypeParser.ParserContext parserContext) { + public static void parseNorms(FieldMapper.Builder builder, String fieldName, Object propNode) { builder.omitNorms(XContentMapValues.nodeBooleanValue(propNode, fieldName + ".norms") == false); } @@ -140,7 +139,7 @@ public static void parseTextField(FieldMapper.Builder builder, String name, Map< final String propName = entry.getKey(); final Object propNode = entry.getValue(); if ("norms".equals(propName)) { - parseNorms(builder, name, propNode, parserContext); + parseNorms(builder, name, propNode); iterator.remove(); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java index 56e587dc995d..8e5c81e58f18 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java @@ -321,11 +321,16 @@ public void testBoost() throws IOException { public void testEnableNorms() throws IOException { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") - .startObject("properties").startObject("field").field("type", "keyword").field("norms", true).endObject().endObject() - .endObject().endObject()); + .startObject("properties") + .startObject("field") + .field("type", "keyword") + .field("doc_values", false) + .field("norms", true) + .endObject() + .endObject() + .endObject().endObject()); DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping)); - assertEquals(mapping, mapper.mappingSource().toString()); ParsedDocument doc = mapper.parse(SourceToParse.source("test", "type", "1", BytesReference @@ -336,8 +341,11 @@ public void testEnableNorms() throws IOException { XContentType.JSON)); IndexableField[] fields = doc.rootDoc().getFields("field"); - assertEquals(2, fields.length); + assertEquals(1, fields.length); assertFalse(fields[0].fieldType().omitNorms()); + + IndexableField[] fieldNamesFields = doc.rootDoc().getFields(FieldNamesFieldMapper.NAME); + assertEquals(0, fieldNamesFields.length); } public void testNormalizer() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldTypeTests.java index a291062c7a5b..eae5b4ac7d2a 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldTypeTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.mapper; import com.carrotsearch.randomizedtesting.generators.RandomStrings; - import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.LowerCaseFilter; import org.apache.lucene.analysis.TokenFilter; @@ -28,9 +27,11 @@ import org.apache.lucene.analysis.core.WhitespaceTokenizer; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.Term; -import org.apache.lucene.search.TermInSetQuery; +import org.apache.lucene.search.DocValuesFieldExistsQuery; import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.NormsFieldExistsQuery; import org.apache.lucene.search.RegexpQuery; +import org.apache.lucene.search.TermInSetQuery; import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.lucene.Lucene; @@ -132,6 +133,23 @@ public void testTermsQuery() { assertEquals("Cannot search on field [field] since it is not indexed.", e.getMessage()); } + public void testExistsQuery() { + MappedFieldType ft = createDefaultFieldType(); + ft.setName("field"); + + ft.setHasDocValues(true); + ft.setOmitNorms(true); + assertEquals(new DocValuesFieldExistsQuery("field"), ft.existsQuery(null)); + + ft.setHasDocValues(false); + ft.setOmitNorms(false); + assertEquals(new NormsFieldExistsQuery("field"), ft.existsQuery(null)); + + ft.setHasDocValues(false); + ft.setOmitNorms(true); + assertEquals(new TermQuery(new Term(FieldNamesFieldMapper.NAME, "field")), ft.existsQuery(null)); + } + public void testRegexpQuery() { MappedFieldType ft = createDefaultFieldType(); ft.setName("field"); From 2c81d7f77ec90c94446be3a8fe15d461a6f2d362 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 21 Aug 2018 20:03:28 -0400 Subject: [PATCH 094/283] Build: Rework shadow plugin configuration (#32409) This reworks how we configure the `shadow` plugin in the build. The major change is that we no longer bundle dependencies in the `compile` configuration, instead we bundle dependencies in the new `bundle` configuration. This feels more right because it is a little more "opt in" rather than "opt out" and the name of the `bundle` configuration is a little more obvious. As an neat side effect of this, the `runtimeElements` configuration used when one project depends on another now contains exactly the dependencies needed to run the project so you no longer need to reference projects that use the shadow plugin like this: ``` testCompile project(path: ':client:rest-high-level', configuration: 'shadow') ``` You can instead use the much more normal: ``` testCompile "org.elasticsearch.client:elasticsearch-rest-high-level-client:${version}" ``` --- CONTRIBUTING.md | 14 ++- build.gradle | 39 +++------ .../elasticsearch/gradle/BuildPlugin.groovy | 85 +++++++------------ .../gradle/plugin/PluginBuildPlugin.groovy | 5 +- .../gradle/precommit/JarHellTask.groovy | 4 + .../precommit/ThirdPartyAuditTask.groovy | 6 ++ client/rest-high-level/build.gradle | 14 +-- qa/ccs-unavailable-clusters/build.gradle | 2 +- x-pack/docs/build.gradle | 5 +- x-pack/license-tools/build.gradle | 2 +- x-pack/plugin/core/build.gradle | 22 ++--- x-pack/plugin/deprecation/build.gradle | 2 +- x-pack/plugin/graph/build.gradle | 3 +- x-pack/plugin/logstash/build.gradle | 4 +- x-pack/plugin/ml/build.gradle | 3 +- .../ml/qa/basic-multi-node/build.gradle | 2 +- x-pack/plugin/ml/qa/disabled/build.gradle | 2 +- .../ml/qa/ml-with-security/build.gradle | 3 +- .../qa/native-multi-node-tests/build.gradle | 3 +- .../ml/qa/no-bootstrap-tests/build.gradle | 2 +- .../ml/qa/single-node-tests/build.gradle | 2 +- x-pack/plugin/monitoring/build.gradle | 3 +- x-pack/plugin/rollup/build.gradle | 3 +- x-pack/plugin/security/build.gradle | 3 +- x-pack/plugin/security/cli/build.gradle | 5 +- x-pack/plugin/sql/build.gradle | 3 +- x-pack/plugin/upgrade/build.gradle | 3 +- x-pack/plugin/watcher/build.gradle | 3 +- x-pack/qa/evil-tests/build.gradle | 2 +- x-pack/qa/full-cluster-restart/build.gradle | 6 +- x-pack/qa/kerberos-tests/build.gradle | 2 +- .../build.gradle | 3 +- x-pack/qa/multi-node/build.gradle | 2 +- x-pack/qa/openldap-tests/build.gradle | 3 +- .../reindex-tests-with-security/build.gradle | 3 +- x-pack/qa/rolling-upgrade-basic/build.gradle | 3 +- x-pack/qa/rolling-upgrade/build.gradle | 6 +- x-pack/qa/saml-idp-tests/build.gradle | 3 +- x-pack/qa/security-client-tests/build.gradle | 2 +- .../build.gradle | 2 +- x-pack/qa/security-migrate-tests/build.gradle | 2 +- .../build.gradle | 3 +- .../build.gradle | 2 +- .../build.gradle | 2 +- x-pack/qa/smoke-test-plugins-ssl/build.gradle | 4 +- x-pack/qa/smoke-test-plugins/build.gradle | 2 +- .../build.gradle | 3 +- .../build.gradle | 2 +- x-pack/qa/smoke-test-watcher/build.gradle | 2 +- x-pack/qa/sql/security/build.gradle | 4 +- x-pack/qa/third-party/hipchat/build.gradle | 2 +- x-pack/qa/third-party/jira/build.gradle | 2 +- x-pack/qa/third-party/pagerduty/build.gradle | 2 +- x-pack/qa/third-party/slack/build.gradle | 2 +- x-pack/qa/transport-client-tests/build.gradle | 2 +- x-pack/test/feature-aware/build.gradle | 2 +- x-pack/transport-client/build.gradle | 2 +- 57 files changed, 158 insertions(+), 166 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c591459f01bb..4285c8fd20c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -325,21 +325,19 @@ common configurations in our build and how we use them:
`compile`
Code that is on the classpath at both compile and -runtime. If the [`shadow`][shadow-plugin] plugin is applied to the project then -this code is bundled into the jar produced by the project.
+runtime.
`runtime`
Code that is not on the classpath at compile time but is on the classpath at runtime. We mostly use this configuration to make sure that we do not accidentally compile against dependencies of our dependencies also known as "transitive" dependencies".
-
`compileOnly`
Code that is on the classpath at comile time but that +
`compileOnly`
Code that is on the classpath at compile time but that should not be shipped with the project because it is "provided" by the runtime somehow. Elasticsearch plugins use this configuration to include dependencies that are bundled with Elasticsearch's server.
-
`shadow`
Only available in projects with the shadow plugin. Code -that is on the classpath at both compile and runtime but it *not* bundled into -the jar produced by the project. If you depend on a project with the `shadow` -plugin then you need to depend on this configuration because it will bring -along all of the dependencies you need at runtime.
+
`bundle`
Only available in projects with the shadow plugin, +dependencies with this configuration are bundled into the jar produced by the +build. Since IDEs do not understand this configuration we rig them to treat +dependencies in this configuration as `compile` dependencies.
`testCompile`
Code that is on the classpath for compiling tests that are part of this project but not production code. The canonical example of this is `junit`.
diff --git a/build.gradle b/build.gradle index 0df5b97ae4a2..36d3a543d89b 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ import org.elasticsearch.gradle.LoggedExec import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.VersionCollection import org.elasticsearch.gradle.VersionProperties +import org.elasticsearch.gradle.plugin.PluginBuildPlugin import org.gradle.plugins.ide.eclipse.model.SourceFolder import org.gradle.util.GradleVersion import org.gradle.util.DistributionLocator @@ -304,7 +305,7 @@ subprojects { // org.elasticsearch:elasticsearch must be the last one or all the links for the // other packages (e.g org.elasticsearch.client) will point to server rather than // their own artifacts. - if (project.plugins.hasPlugin(BuildPlugin)) { + if (project.plugins.hasPlugin(BuildPlugin) || project.plugins.hasPlugin(PluginBuildPlugin)) { String artifactsHost = VersionProperties.elasticsearch.isSnapshot() ? "https://snapshots.elastic.co" : "https://artifacts.elastic.co" Closure sortClosure = { a, b -> b.group <=> a.group } Closure depJavadocClosure = { shadowed, dep -> @@ -322,13 +323,6 @@ subprojects { */ project.evaluationDependsOn(upstreamProject.path) project.javadoc.source += upstreamProject.javadoc.source - /* - * Do not add those projects to the javadoc classpath because - * we are going to resolve them with their source instead. - */ - project.javadoc.classpath = project.javadoc.classpath.filter { f -> - false == upstreamProject.configurations.archives.artifacts.files.files.contains(f) - } /* * Instead we need the upstream project's javadoc classpath so * we don't barf on the classes that it references. @@ -345,16 +339,16 @@ subprojects { project.configurations.compile.dependencies .findAll() .toSorted(sortClosure) - .each({ c -> depJavadocClosure(hasShadow, c) }) + .each({ c -> depJavadocClosure(false, c) }) project.configurations.compileOnly.dependencies .findAll() .toSorted(sortClosure) - .each({ c -> depJavadocClosure(hasShadow, c) }) + .each({ c -> depJavadocClosure(false, c) }) if (hasShadow) { - project.configurations.shadow.dependencies + project.configurations.bundle.dependencies .findAll() .toSorted(sortClosure) - .each({ c -> depJavadocClosure(false, c) }) + .each({ c -> depJavadocClosure(true, c) }) } } } @@ -523,25 +517,18 @@ allprojects { allprojects { /* * IntelliJ and Eclipse don't know about the shadow plugin so when we're - * in "IntelliJ mode" or "Eclipse mode" add "runtime" dependencies - * eveywhere where we see a "shadow" dependency which will cause them to - * reference shadowed projects directly rather than rely on the shadowing - * to include them. This is the correct thing for it to do because it - * doesn't run the jar shadowing at all. This isn't needed for the project + * in "IntelliJ mode" or "Eclipse mode" switch "bundle" dependencies into + * regular "compile" dependencies. This isn't needed for the project * itself because the IDE configuration is done by SourceSets but it is * *is* needed for projects that depends on the project doing the shadowing. * Without this they won't properly depend on the shadowed project. */ if (isEclipse || isIdea) { - configurations.all { Configuration configuration -> - dependencies.all { Dependency dep -> - if (dep instanceof ProjectDependency) { - if (dep.getTargetConfiguration() == 'shadow') { - configuration.dependencies.add(project.dependencies.project(path: dep.dependencyProject.path, configuration: 'runtime')) - } - } - } - } + project.plugins.withType(ShadowPlugin).whenPluginAdded { + project.afterEvaluate { + project.configurations.compile.extendsFrom project.configurations.bundle + } + } } } diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index e45ba7ce9dc1..7713d5c64dfc 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -79,8 +79,9 @@ class BuildPlugin implements Plugin { } project.pluginManager.apply('java') project.pluginManager.apply('carrotsearch.randomized-testing') - // these plugins add lots of info to our jars + configureConfigurations(project) configureJars(project) // jar config must be added before info broker + // these plugins add lots of info to our jars project.pluginManager.apply('nebula.info-broker') project.pluginManager.apply('nebula.info-basic') project.pluginManager.apply('nebula.info-java') @@ -91,8 +92,8 @@ class BuildPlugin implements Plugin { globalBuildInfo(project) configureRepositories(project) - configureConfigurations(project) project.ext.versions = VersionProperties.versions + configureSourceSets(project) configureCompile(project) configureJavadoc(project) configureSourcesJar(project) @@ -421,8 +422,10 @@ class BuildPlugin implements Plugin { project.configurations.compile.dependencies.all(disableTransitiveDeps) project.configurations.testCompile.dependencies.all(disableTransitiveDeps) project.configurations.compileOnly.dependencies.all(disableTransitiveDeps) + project.plugins.withType(ShadowPlugin).whenPluginAdded { - project.configurations.shadow.dependencies.all(disableTransitiveDeps) + Configuration bundle = project.configurations.create('bundle') + bundle.dependencies.all(disableTransitiveDeps) } } @@ -556,30 +559,6 @@ class BuildPlugin implements Plugin { publications { nebula(MavenPublication) { artifacts = [ project.tasks.shadowJar ] - artifactId = project.archivesBaseName - /* - * Configure the pom to include the "shadow" as compile dependencies - * because that is how we're using them but remove all other dependencies - * because they've been shaded into the jar. - */ - pom.withXml { XmlProvider xml -> - Node root = xml.asNode() - root.remove(root.dependencies) - Node dependenciesNode = root.appendNode('dependencies') - project.configurations.shadow.allDependencies.each { - if (false == it instanceof SelfResolvingDependency) { - Node dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) - dependencyNode.appendNode('scope', 'compile') - } - } - // Be tidy and remove the element if it is empty - if (dependenciesNode.children.empty) { - root.remove(dependenciesNode) - } - } } } } @@ -587,6 +566,20 @@ class BuildPlugin implements Plugin { } } + /** + * Add dependencies that we are going to bundle to the compile classpath. + */ + static void configureSourceSets(Project project) { + project.plugins.withType(ShadowPlugin).whenPluginAdded { + ['main', 'test'].each {name -> + SourceSet sourceSet = project.sourceSets.findByName(name) + if (sourceSet != null) { + sourceSet.compileClasspath += project.configurations.bundle + } + } + } + } + /** Adds compiler settings to the project */ static void configureCompile(Project project) { if (project.compilerJavaVersion < JavaVersion.VERSION_1_10) { @@ -764,9 +757,16 @@ class BuildPlugin implements Plugin { * better to be safe */ mergeServiceFiles() + /* + * Bundle dependencies of the "bundled" configuration. + */ + configurations = [project.configurations.bundle] } // Make sure we assemble the shadow jar project.tasks.assemble.dependsOn project.tasks.shadowJar + project.artifacts { + apiElements project.tasks.shadowJar + } } } @@ -873,13 +873,8 @@ class BuildPlugin implements Plugin { exclude '**/*$*.class' project.plugins.withType(ShadowPlugin).whenPluginAdded { - /* - * If we make a shaded jar we test against it. - */ + // Test against a shadow jar if we made one classpath -= project.tasks.compileJava.outputs.files - classpath -= project.configurations.compile - classpath -= project.configurations.runtime - classpath += project.configurations.shadow classpath += project.tasks.shadowJar.outputs.files dependsOn project.tasks.shadowJar } @@ -905,26 +900,6 @@ class BuildPlugin implements Plugin { additionalTest.dependsOn(project.tasks.testClasses) project.check.dependsOn(additionalTest) }); - - project.plugins.withType(ShadowPlugin).whenPluginAdded { - /* - * We need somewhere to configure dependencies that we don't wish - * to shade into the jar. The shadow plugin creates a "shadow" - * configuration which is *almost* exactly that. It is never - * bundled into the shaded jar but is used for main source - * compilation. Unfortunately, by default it is not used for - * *test* source compilation and isn't used in tests at all. This - * change makes it available for test compilation. - * - * Note that this isn't going to work properly with qa projects - * but they have no business applying the shadow plugin in the - * firstplace. - */ - SourceSet testSourceSet = project.sourceSets.findByName('test') - if (testSourceSet != null) { - testSourceSet.compileClasspath += project.configurations.shadow - } - } } private static configurePrecommit(Project project) { @@ -936,7 +911,7 @@ class BuildPlugin implements Plugin { it.group.startsWith('org.elasticsearch') == false } - project.configurations.compileOnly project.plugins.withType(ShadowPlugin).whenPluginAdded { - project.dependencyLicenses.dependencies += project.configurations.shadow.fileCollection { + project.dependencyLicenses.dependencies += project.configurations.bundle.fileCollection { it.group.startsWith('org.elasticsearch') == false } } @@ -947,7 +922,7 @@ class BuildPlugin implements Plugin { deps.runtimeConfiguration = project.configurations.runtime project.plugins.withType(ShadowPlugin).whenPluginAdded { deps.runtimeConfiguration = project.configurations.create('infoDeps') - deps.runtimeConfiguration.extendsFrom(project.configurations.runtime, project.configurations.shadow) + deps.runtimeConfiguration.extendsFrom(project.configurations.runtime, project.configurations.bundle) } deps.compileOnlyConfiguration = project.configurations.compileOnly project.afterEvaluate { diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy index 5216f0842742..a14a3a680da1 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy @@ -157,11 +157,10 @@ public class PluginBuildPlugin extends BuildPlugin { from pluginMetadata // metadata (eg custom security policy) /* * If the plugin is using the shadow plugin then we need to bundle - * "shadow" things rather than the default jar and dependencies so - * we don't hit jar hell. + * that shadow jar. */ from { project.plugins.hasPlugin(ShadowPlugin) ? project.shadowJar : project.jar } - from { project.plugins.hasPlugin(ShadowPlugin) ? project.configurations.shadow : project.configurations.runtime - project.configurations.compileOnly } + from project.configurations.runtime - project.configurations.compileOnly // extra files for the plugin to go into the zip from('src/main/packaging') // TODO: move all config/bin/_size/etc into packaging from('src/main') { diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy index 656d5e0d35a9..4299efd95a38 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy @@ -19,6 +19,7 @@ package org.elasticsearch.gradle.precommit +import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin import org.elasticsearch.gradle.LoggedExec import org.gradle.api.file.FileCollection import org.gradle.api.tasks.OutputFile @@ -39,6 +40,9 @@ public class JarHellTask extends LoggedExec { public JarHellTask() { project.afterEvaluate { FileCollection classpath = project.sourceSets.test.runtimeClasspath + if (project.plugins.hasPlugin(ShadowPlugin)) { + classpath += project.configurations.bundle + } inputs.files(classpath) dependsOn(classpath) description = "Runs CheckJarHell on ${classpath}" diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy index d6babbbfbb8b..52b13a566442 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy @@ -18,6 +18,7 @@ */ package org.elasticsearch.gradle.precommit; +import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin import org.apache.tools.ant.BuildEvent; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.BuildListener; @@ -82,6 +83,11 @@ public class ThirdPartyAuditTask extends AntTask { configuration = project.configurations.findByName('testCompile') } assert configuration != null + if (project.plugins.hasPlugin(ShadowPlugin)) { + Configuration original = configuration + configuration = project.configurations.create('thirdPartyAudit') + configuration.extendsFrom(original, project.configurations.bundle) + } if (compileOnly == null) { classpath = configuration } else { diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle index 6f5eab6e1db1..48169faac2fc 100644 --- a/client/rest-high-level/build.gradle +++ b/client/rest-high-level/build.gradle @@ -47,13 +47,13 @@ dependencies { * Everything in the "shadow" configuration is *not* copied into the * shadowJar. */ - shadow "org.elasticsearch:elasticsearch:${version}" - shadow "org.elasticsearch.client:elasticsearch-rest-client:${version}" - shadow "org.elasticsearch.plugin:parent-join-client:${version}" - shadow "org.elasticsearch.plugin:aggs-matrix-stats-client:${version}" - shadow "org.elasticsearch.plugin:rank-eval-client:${version}" - shadow "org.elasticsearch.plugin:lang-mustache-client:${version}" - compile project(':x-pack:protocol') + compile "org.elasticsearch:elasticsearch:${version}" + compile "org.elasticsearch.client:elasticsearch-rest-client:${version}" + compile "org.elasticsearch.plugin:parent-join-client:${version}" + compile "org.elasticsearch.plugin:aggs-matrix-stats-client:${version}" + compile "org.elasticsearch.plugin:rank-eval-client:${version}" + compile "org.elasticsearch.plugin:lang-mustache-client:${version}" + bundle project(':x-pack:protocol') testCompile "org.elasticsearch.client:test:${version}" testCompile "org.elasticsearch.test:framework:${version}" diff --git a/qa/ccs-unavailable-clusters/build.gradle b/qa/ccs-unavailable-clusters/build.gradle index d9de422bb43e..c1f2bc962710 100644 --- a/qa/ccs-unavailable-clusters/build.gradle +++ b/qa/ccs-unavailable-clusters/build.gradle @@ -21,5 +21,5 @@ apply plugin: 'elasticsearch.rest-test' apply plugin: 'elasticsearch.test-with-dependencies' dependencies { - testCompile project(path: ':client:rest-high-level', configuration: 'shadow') + testCompile "org.elasticsearch.client:elasticsearch-rest-high-level-client:${version}" } diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index aab843555819..7ef17715e06a 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -30,7 +30,8 @@ buildRestTests.expectedUnconvertedCandidates = [ ] dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackProject('plugin').path, configuration: 'testArtifacts') } @@ -309,7 +310,7 @@ setups['farequote_datafeed'] = setups['farequote_job'] + ''' "job_id":"farequote", "indexes":"farequote" } -''' +''' setups['ml_filter_safe_domains'] = ''' - do: xpack.ml.put_filter: diff --git a/x-pack/license-tools/build.gradle b/x-pack/license-tools/build.gradle index 183b9ab50e03..4bd17713a2fe 100644 --- a/x-pack/license-tools/build.gradle +++ b/x-pack/license-tools/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'elasticsearch.build' dependencies { - compile project(path: xpackModule('core'), configuration: 'shadow') + compile "org.elasticsearch.plugin:x-pack-core:${version}" compile "org.elasticsearch:elasticsearch:${version}" testCompile "org.elasticsearch.test:framework:${version}" } diff --git a/x-pack/plugin/core/build.gradle b/x-pack/plugin/core/build.gradle index a3b4bea9702f..ef428bdd73df 100644 --- a/x-pack/plugin/core/build.gradle +++ b/x-pack/plugin/core/build.gradle @@ -27,19 +27,19 @@ dependencyLicenses { dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" - compile project(':x-pack:protocol') - shadow "org.apache.httpcomponents:httpclient:${versions.httpclient}" - shadow "org.apache.httpcomponents:httpcore:${versions.httpcore}" - shadow "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}" - shadow "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}" + bundle project(':x-pack:protocol') + compile "org.apache.httpcomponents:httpclient:${versions.httpclient}" + compile "org.apache.httpcomponents:httpcore:${versions.httpcore}" + compile "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}" + compile "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}" - shadow "commons-logging:commons-logging:${versions.commonslogging}" - shadow "commons-codec:commons-codec:${versions.commonscodec}" + compile "commons-logging:commons-logging:${versions.commonslogging}" + compile "commons-codec:commons-codec:${versions.commonscodec}" // security deps - shadow 'com.unboundid:unboundid-ldapsdk:3.2.0' - shadow project(path: ':modules:transport-netty4', configuration: 'runtime') - shadow(project(path: ':plugins:transport-nio', configuration: 'runtime')) { + compile 'com.unboundid:unboundid-ldapsdk:3.2.0' + compile project(path: ':modules:transport-netty4', configuration: 'runtime') + compile(project(path: ':plugins:transport-nio', configuration: 'runtime')) { // TODO: core exclusion should not be necessary, since it is a transitive dep of all plugins exclude group: "org.elasticsearch", module: "elasticsearch-core" } @@ -122,7 +122,7 @@ task testJar(type: Jar) { artifacts { // normal es plugins do not publish the jar but we need to since users need it for Transport Clients and extensions - archives jar + archives shadowJar testArtifacts testJar } diff --git a/x-pack/plugin/deprecation/build.gradle b/x-pack/plugin/deprecation/build.gradle index 3746287d615f..d89eb62e8849 100644 --- a/x-pack/plugin/deprecation/build.gradle +++ b/x-pack/plugin/deprecation/build.gradle @@ -10,7 +10,7 @@ esplugin { archivesBaseName = 'x-pack-deprecation' dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + compileOnly "org.elasticsearch.plugin:x-pack-core:${version}" } run { diff --git a/x-pack/plugin/graph/build.gradle b/x-pack/plugin/graph/build.gradle index 2b0f592b7204..069bfa5fbbe2 100644 --- a/x-pack/plugin/graph/build.gradle +++ b/x-pack/plugin/graph/build.gradle @@ -10,7 +10,8 @@ esplugin { archivesBaseName = 'x-pack-graph' dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') } diff --git a/x-pack/plugin/logstash/build.gradle b/x-pack/plugin/logstash/build.gradle index 2e158a90ac7a..1057a1c8526f 100644 --- a/x-pack/plugin/logstash/build.gradle +++ b/x-pack/plugin/logstash/build.gradle @@ -10,9 +10,9 @@ esplugin { archivesBaseName = 'x-pack-logstash' dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') - } run { diff --git a/x-pack/plugin/ml/build.gradle b/x-pack/plugin/ml/build.gradle index 282ce96fa935..7c3594a06cfd 100644 --- a/x-pack/plugin/ml/build.gradle +++ b/x-pack/plugin/ml/build.gradle @@ -40,7 +40,8 @@ compileJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes,-serial,-try, compileTestJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes,-serial,-try,-unchecked" dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') // This should not be here testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') diff --git a/x-pack/plugin/ml/qa/basic-multi-node/build.gradle b/x-pack/plugin/ml/qa/basic-multi-node/build.gradle index 3df77aadccbd..cc5a2cd68dde 100644 --- a/x-pack/plugin/ml/qa/basic-multi-node/build.gradle +++ b/x-pack/plugin/ml/qa/basic-multi-node/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('ml'), configuration: 'runtime') } diff --git a/x-pack/plugin/ml/qa/disabled/build.gradle b/x-pack/plugin/ml/qa/disabled/build.gradle index e914def3507c..a24036651d50 100644 --- a/x-pack/plugin/ml/qa/disabled/build.gradle +++ b/x-pack/plugin/ml/qa/disabled/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('ml'), configuration: 'runtime') } diff --git a/x-pack/plugin/ml/qa/ml-with-security/build.gradle b/x-pack/plugin/ml/qa/ml-with-security/build.gradle index 84c23add2541..a702973fcb02 100644 --- a/x-pack/plugin/ml/qa/ml-with-security/build.gradle +++ b/x-pack/plugin/ml/qa/ml-with-security/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackProject('plugin').path, configuration: 'testArtifacts') } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle b/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle index b1893b20c465..0c4304b123ea 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/build.gradle @@ -4,7 +4,8 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('ml'), configuration: 'runtime') testCompile project(path: xpackModule('ml'), configuration: 'testArtifacts') diff --git a/x-pack/plugin/ml/qa/no-bootstrap-tests/build.gradle b/x-pack/plugin/ml/qa/no-bootstrap-tests/build.gradle index 7e252afa3022..9eac3fdd37a8 100644 --- a/x-pack/plugin/ml/qa/no-bootstrap-tests/build.gradle +++ b/x-pack/plugin/ml/qa/no-bootstrap-tests/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('ml'), configuration: 'runtime') } diff --git a/x-pack/plugin/ml/qa/single-node-tests/build.gradle b/x-pack/plugin/ml/qa/single-node-tests/build.gradle index b62e37894b3c..88ca4dd118ea 100644 --- a/x-pack/plugin/ml/qa/single-node-tests/build.gradle +++ b/x-pack/plugin/ml/qa/single-node-tests/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('ml'), configuration: 'runtime') } diff --git a/x-pack/plugin/monitoring/build.gradle b/x-pack/plugin/monitoring/build.gradle index a452ef09a20f..e551d577b7bb 100644 --- a/x-pack/plugin/monitoring/build.gradle +++ b/x-pack/plugin/monitoring/build.gradle @@ -13,7 +13,8 @@ esplugin { archivesBaseName = 'x-pack-monitoring' dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') // monitoring deps diff --git a/x-pack/plugin/rollup/build.gradle b/x-pack/plugin/rollup/build.gradle index 649a89bc2cde..75fd22abacc5 100644 --- a/x-pack/plugin/rollup/build.gradle +++ b/x-pack/plugin/rollup/build.gradle @@ -16,7 +16,8 @@ compileTestJava.options.compilerArgs << "-Xlint:-rawtypes" dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') } diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 6db533bbecf9..003263669d51 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -12,7 +12,8 @@ esplugin { archivesBaseName = 'x-pack-security' dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') compileOnly project(path: ':modules:transport-netty4', configuration: 'runtime') compileOnly project(path: ':plugins:transport-nio', configuration: 'runtime') diff --git a/x-pack/plugin/security/cli/build.gradle b/x-pack/plugin/security/cli/build.gradle index 1a00b2a03400..426c48aac80a 100644 --- a/x-pack/plugin/security/cli/build.gradle +++ b/x-pack/plugin/security/cli/build.gradle @@ -4,7 +4,8 @@ archivesBaseName = 'elasticsearch-security-cli' dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') compile 'org.bouncycastle:bcprov-jdk15on:1.59' compile 'org.bouncycastle:bcpkix-jdk15on:1.59' testImplementation 'com.google.jimfs:jimfs:1.1' @@ -21,4 +22,4 @@ dependencyLicenses { if (inFipsJvm) { test.enabled = false -} \ No newline at end of file +} diff --git a/x-pack/plugin/sql/build.gradle b/x-pack/plugin/sql/build.gradle index 039e78c14952..62097e76b97e 100644 --- a/x-pack/plugin/sql/build.gradle +++ b/x-pack/plugin/sql/build.gradle @@ -19,7 +19,8 @@ archivesBaseName = 'x-pack-sql' integTest.enabled = false dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') compileOnly(project(':modules:lang-painless')) { // exclude ASM to not affect featureAware task on Java 10+ exclude group: "org.ow2.asm" diff --git a/x-pack/plugin/upgrade/build.gradle b/x-pack/plugin/upgrade/build.gradle index f95cde7134c5..56ce274dd116 100644 --- a/x-pack/plugin/upgrade/build.gradle +++ b/x-pack/plugin/upgrade/build.gradle @@ -14,7 +14,8 @@ esplugin { archivesBaseName = 'x-pack-upgrade' dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') } diff --git a/x-pack/plugin/watcher/build.gradle b/x-pack/plugin/watcher/build.gradle index a0feab674635..3a9d759c46d1 100644 --- a/x-pack/plugin/watcher/build.gradle +++ b/x-pack/plugin/watcher/build.gradle @@ -25,7 +25,8 @@ dependencyLicenses { dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + compileOnly project(path: xpackModule('core'), configuration: 'default') compileOnly project(path: ':modules:transport-netty4', configuration: 'runtime') compileOnly project(path: ':plugins:transport-nio', configuration: 'runtime') diff --git a/x-pack/qa/evil-tests/build.gradle b/x-pack/qa/evil-tests/build.gradle index 9b6055ffad7d..03f2a5698731 100644 --- a/x-pack/qa/evil-tests/build.gradle +++ b/x-pack/qa/evil-tests/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } test { diff --git a/x-pack/qa/full-cluster-restart/build.gradle b/x-pack/qa/full-cluster-restart/build.gradle index 3cf297012067..ab8f9172b690 100644 --- a/x-pack/qa/full-cluster-restart/build.gradle +++ b/x-pack/qa/full-cluster-restart/build.gradle @@ -11,7 +11,8 @@ apply plugin: 'elasticsearch.build' test.enabled = false dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile (project(path: xpackModule('security'), configuration: 'runtime')) { // Need to drop the guava dependency here or we get a conflict with watcher's guava dependency. // This is total #$%, but the solution is to get the SAML realm (which uses guava) out of security proper @@ -249,7 +250,8 @@ subprojects { check.dependsOn(integTest) dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('watcher'), configuration: 'runtime') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') diff --git a/x-pack/qa/kerberos-tests/build.gradle b/x-pack/qa/kerberos-tests/build.gradle index 59667d9ee780..f680a45bd7f5 100644 --- a/x-pack/qa/kerberos-tests/build.gradle +++ b/x-pack/qa/kerberos-tests/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') } diff --git a/x-pack/qa/multi-cluster-search-security/build.gradle b/x-pack/qa/multi-cluster-search-security/build.gradle index 5d90f974762b..c06ad68d8032 100644 --- a/x-pack/qa/multi-cluster-search-security/build.gradle +++ b/x-pack/qa/multi-cluster-search-security/build.gradle @@ -3,7 +3,8 @@ import org.elasticsearch.gradle.test.RestIntegTestTask apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') } diff --git a/x-pack/qa/multi-node/build.gradle b/x-pack/qa/multi-node/build.gradle index 19729cf367ef..4369287caba3 100644 --- a/x-pack/qa/multi-node/build.gradle +++ b/x-pack/qa/multi-node/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } integTestCluster { diff --git a/x-pack/qa/openldap-tests/build.gradle b/x-pack/qa/openldap-tests/build.gradle index 24cd6184afa6..bb9a97992897 100644 --- a/x-pack/qa/openldap-tests/build.gradle +++ b/x-pack/qa/openldap-tests/build.gradle @@ -5,7 +5,8 @@ apply plugin: 'elasticsearch.standalone-test' apply plugin: 'elasticsearch.vagrantsupport' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') } diff --git a/x-pack/qa/reindex-tests-with-security/build.gradle b/x-pack/qa/reindex-tests-with-security/build.gradle index 097d343b2798..97c0e8e17fee 100644 --- a/x-pack/qa/reindex-tests-with-security/build.gradle +++ b/x-pack/qa/reindex-tests-with-security/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: ':modules:reindex') diff --git a/x-pack/qa/rolling-upgrade-basic/build.gradle b/x-pack/qa/rolling-upgrade-basic/build.gradle index 21ac4414d86b..5774e5d78561 100644 --- a/x-pack/qa/rolling-upgrade-basic/build.gradle +++ b/x-pack/qa/rolling-upgrade-basic/build.gradle @@ -7,7 +7,8 @@ import java.nio.charset.StandardCharsets apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') // to be moved in a later commit } diff --git a/x-pack/qa/rolling-upgrade/build.gradle b/x-pack/qa/rolling-upgrade/build.gradle index b983caa86693..548081a89388 100644 --- a/x-pack/qa/rolling-upgrade/build.gradle +++ b/x-pack/qa/rolling-upgrade/build.gradle @@ -10,7 +10,8 @@ apply plugin: 'elasticsearch.build' test.enabled = false dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('security'), configuration: 'runtime') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') // to be moved in a later commit } @@ -284,7 +285,8 @@ subprojects { check.dependsOn(integTest) dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('watcher')) } diff --git a/x-pack/qa/saml-idp-tests/build.gradle b/x-pack/qa/saml-idp-tests/build.gradle index 752ec6fb3071..9dd5d6d848f9 100644 --- a/x-pack/qa/saml-idp-tests/build.gradle +++ b/x-pack/qa/saml-idp-tests/build.gradle @@ -6,7 +6,8 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') testCompile 'com.google.jimfs:jimfs:1.1' diff --git a/x-pack/qa/security-client-tests/build.gradle b/x-pack/qa/security-client-tests/build.gradle index 97945fb00efc..e676e55a152d 100644 --- a/x-pack/qa/security-client-tests/build.gradle +++ b/x-pack/qa/security-client-tests/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackProject('transport-client').path, configuration: 'runtime') } diff --git a/x-pack/qa/security-example-spi-extension/build.gradle b/x-pack/qa/security-example-spi-extension/build.gradle index 7aeed3ad62de..aef4fc33f6ab 100644 --- a/x-pack/qa/security-example-spi-extension/build.gradle +++ b/x-pack/qa/security-example-spi-extension/build.gradle @@ -8,7 +8,7 @@ esplugin { } dependencies { - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + compileOnly "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackProject('transport-client').path, configuration: 'runtime') } diff --git a/x-pack/qa/security-migrate-tests/build.gradle b/x-pack/qa/security-migrate-tests/build.gradle index 3a8a0cf10055..abc3564ca13f 100644 --- a/x-pack/qa/security-migrate-tests/build.gradle +++ b/x-pack/qa/security-migrate-tests/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('security'), configuration: 'runtime') testCompile project(path: xpackProject('transport-client').path, configuration: 'runtime') } diff --git a/x-pack/qa/security-setup-password-tests/build.gradle b/x-pack/qa/security-setup-password-tests/build.gradle index adb159acf6f6..c0801a38b570 100644 --- a/x-pack/qa/security-setup-password-tests/build.gradle +++ b/x-pack/qa/security-setup-password-tests/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('security'), configuration: 'runtime') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') } diff --git a/x-pack/qa/smoke-test-graph-with-security/build.gradle b/x-pack/qa/smoke-test-graph-with-security/build.gradle index 9cdfaffccfbc..f0f819b46d47 100644 --- a/x-pack/qa/smoke-test-graph-with-security/build.gradle +++ b/x-pack/qa/smoke-test-graph-with-security/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } // bring in graph rest test suite diff --git a/x-pack/qa/smoke-test-monitoring-with-watcher/build.gradle b/x-pack/qa/smoke-test-monitoring-with-watcher/build.gradle index 8ce0cde76575..7813ff3d3d56 100644 --- a/x-pack/qa/smoke-test-monitoring-with-watcher/build.gradle +++ b/x-pack/qa/smoke-test-monitoring-with-watcher/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('watcher')) testCompile project(path: xpackModule('monitoring')) } diff --git a/x-pack/qa/smoke-test-plugins-ssl/build.gradle b/x-pack/qa/smoke-test-plugins-ssl/build.gradle index 53533bd9b87f..4f338d07fb53 100644 --- a/x-pack/qa/smoke-test-plugins-ssl/build.gradle +++ b/x-pack/qa/smoke-test-plugins-ssl/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } String outputDir = "${buildDir}/generated-resources/${project.name}" @@ -138,4 +138,4 @@ processTestResources { inputs.properties(expansions) MavenFilteringHack.filter(it, expansions) } -} \ No newline at end of file +} diff --git a/x-pack/qa/smoke-test-plugins/build.gradle b/x-pack/qa/smoke-test-plugins/build.gradle index b66903af18bf..3b7661eeeb05 100644 --- a/x-pack/qa/smoke-test-plugins/build.gradle +++ b/x-pack/qa/smoke-test-plugins/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } ext.pluginsCount = 0 diff --git a/x-pack/qa/smoke-test-security-with-mustache/build.gradle b/x-pack/qa/smoke-test-security-with-mustache/build.gradle index d921c5f5b660..48b525ba3dae 100644 --- a/x-pack/qa/smoke-test-security-with-mustache/build.gradle +++ b/x-pack/qa/smoke-test-security-with-mustache/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here + testCompile project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: ':modules:lang-mustache', configuration: 'runtime') } diff --git a/x-pack/qa/smoke-test-watcher-with-security/build.gradle b/x-pack/qa/smoke-test-watcher-with-security/build.gradle index a843641be801..50e217b28b27 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/build.gradle +++ b/x-pack/qa/smoke-test-watcher-with-security/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } // bring in watcher rest test suite diff --git a/x-pack/qa/smoke-test-watcher/build.gradle b/x-pack/qa/smoke-test-watcher/build.gradle index dc87248df617..5923afcacad9 100644 --- a/x-pack/qa/smoke-test-watcher/build.gradle +++ b/x-pack/qa/smoke-test-watcher/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('watcher'), configuration: 'runtime') testCompile project(path: ':modules:lang-mustache', configuration: 'runtime') testCompile project(path: ':modules:lang-painless', configuration: 'runtime') diff --git a/x-pack/qa/sql/security/build.gradle b/x-pack/qa/sql/security/build.gradle index f02886f80a10..15f7734f9422 100644 --- a/x-pack/qa/sql/security/build.gradle +++ b/x-pack/qa/sql/security/build.gradle @@ -1,5 +1,5 @@ dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } Project mainProject = project @@ -20,7 +20,7 @@ subprojects { } dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" } integTestCluster { diff --git a/x-pack/qa/third-party/hipchat/build.gradle b/x-pack/qa/third-party/hipchat/build.gradle index 03b6c3196984..2b2ee7fcbbf8 100644 --- a/x-pack/qa/third-party/hipchat/build.gradle +++ b/x-pack/qa/third-party/hipchat/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('watcher'), configuration: 'runtime') } diff --git a/x-pack/qa/third-party/jira/build.gradle b/x-pack/qa/third-party/jira/build.gradle index 3814c8e9a538..283f9688699b 100644 --- a/x-pack/qa/third-party/jira/build.gradle +++ b/x-pack/qa/third-party/jira/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('watcher'), configuration: 'runtime') } diff --git a/x-pack/qa/third-party/pagerduty/build.gradle b/x-pack/qa/third-party/pagerduty/build.gradle index c0f337e160e0..12758989d0f2 100644 --- a/x-pack/qa/third-party/pagerduty/build.gradle +++ b/x-pack/qa/third-party/pagerduty/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('watcher'), configuration: 'runtime') } diff --git a/x-pack/qa/third-party/slack/build.gradle b/x-pack/qa/third-party/slack/build.gradle index 431752765f3a..f1bcd98cff69 100644 --- a/x-pack/qa/third-party/slack/build.gradle +++ b/x-pack/qa/third-party/slack/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackModule('watcher'), configuration: 'runtime') } diff --git a/x-pack/qa/transport-client-tests/build.gradle b/x-pack/qa/transport-client-tests/build.gradle index a94ad8fd5926..3ece6dd1147c 100644 --- a/x-pack/qa/transport-client-tests/build.gradle +++ b/x-pack/qa/transport-client-tests/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'elasticsearch.standalone-rest-test' apply plugin: 'elasticsearch.rest-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile project(path: xpackProject('transport-client').path, configuration: 'runtime') } diff --git a/x-pack/test/feature-aware/build.gradle b/x-pack/test/feature-aware/build.gradle index f6a1f6cb16f2..11b0e67183c8 100644 --- a/x-pack/test/feature-aware/build.gradle +++ b/x-pack/test/feature-aware/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'elasticsearch.build' dependencies { compile 'org.ow2.asm:asm:6.2' compile "org.elasticsearch:elasticsearch:${version}" - compile project(path: xpackModule('core'), configuration: 'shadow') + compile "org.elasticsearch.plugin:x-pack-core:${version}" testCompile "org.elasticsearch.test:framework:${version}" } diff --git a/x-pack/transport-client/build.gradle b/x-pack/transport-client/build.gradle index 7155dad5ee60..2e350ef98ff5 100644 --- a/x-pack/transport-client/build.gradle +++ b/x-pack/transport-client/build.gradle @@ -10,7 +10,7 @@ archivesBaseName = 'x-pack-transport' dependencies { // this "api" dependency looks weird, but it is correct, as it contains // all of x-pack for now, and transport client will be going away in the future. - compile project(path: xpackModule('core'), configuration: 'shadow') + compile "org.elasticsearch.plugin:x-pack-core:${version}" compile "org.elasticsearch.client:transport:${version}" testCompile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}" testCompile "junit:junit:${versions.junit}" From 07b3ff9fe711c076e12ac066472f6a624ec78bd4 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 22 Aug 2018 11:26:53 +1000 Subject: [PATCH 095/283] Add beta label to MSI on install Elasticsearch page (#28126) The main installation instructions page for the Windows MSI installer includes a header at the top to indicate that the installer is in beta, but the Installing Elasticsearch page does not. This commit adds the beta label to the MSI entry within the installation options. --- docs/reference/setup/install.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/setup/install.asciidoc b/docs/reference/setup/install.asciidoc index c0ebfb60fa7b..26a207824af0 100644 --- a/docs/reference/setup/install.asciidoc +++ b/docs/reference/setup/install.asciidoc @@ -41,6 +41,8 @@ Elasticsearch website or from our RPM repository. `msi`:: +beta[] ++ The `msi` package is suitable for installation on Windows 64-bit systems with at least .NET 4.5 framework installed, and is the easiest choice for getting started with Elasticsearch on Windows. MSIs may be downloaded from the Elasticsearch website. From e2ea83d2174ad43027bf8756e493e36d4c8a738c Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 21 Aug 2018 21:02:28 -0500 Subject: [PATCH 096/283] HLRC: Add ML Get Job (#32960) * HLRC: Adding GET ML Job info API * HLRC: Adding GET Job ML API * Fixing QueryPage license header * Adding serialization tests, addressing minor issues * Renaming querypage, changing the dependency on it * Making things immutable * Fixing build failure due to method rename --- .../client/MLRequestConverters.java | 19 +++ .../client/MachineLearningClient.java | 43 +++++ .../client/MLRequestConvertersTests.java | 19 +++ .../client/MachineLearningIT.java | 41 +++++ .../MlClientDocumentationIT.java | 62 ++++++++ docs/java-rest/high-level/ml/get-job.asciidoc | 57 +++++++ .../high-level/supported-apis.asciidoc | 2 + .../xpack/ml/AbstractResultResponse.java | 62 ++++++++ .../protocol/xpack/ml/GetJobRequest.java | 148 ++++++++++++++++++ .../protocol/xpack/ml/GetJobResponse.java | 89 +++++++++++ .../protocol/xpack/ml/GetJobRequestTests.java | 69 ++++++++ .../xpack/ml/GetJobResponseTests.java | 53 +++++++ .../xpack/ml/job/config/JobTests.java | 8 +- 13 files changed, 670 insertions(+), 2 deletions(-) create mode 100644 docs/java-rest/high-level/ml/get-job.asciidoc create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/AbstractResultResponse.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobResponse.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobRequestTests.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobResponseTests.java 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 7178a9c7fc3e..9a4e825be7c6 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 @@ -20,12 +20,14 @@ package org.elasticsearch.client; import org.apache.http.client.methods.HttpDelete; +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.RequestConverters.EndpointBuilder; import org.elasticsearch.common.Strings; import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.GetJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -50,6 +52,23 @@ static Request putJob(PutJobRequest putJobRequest) throws IOException { return request; } + static Request getJob(GetJobRequest getJobRequest) { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(Strings.collectionToCommaDelimitedString(getJobRequest.getJobIds())) + .build(); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + + RequestConverters.Params params = new RequestConverters.Params(request); + if (getJobRequest.isAllowNoJobs() != null) { + params.putParam("allow_no_jobs", Boolean.toString(getJobRequest.isAllowNoJobs())); + } + + return request; + } + static Request openJob(OpenJobRequest openJobRequest) 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 2073d613ac66..90acabfbdd8a 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 @@ -23,6 +23,8 @@ import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; +import org.elasticsearch.protocol.xpack.ml.GetJobRequest; +import org.elasticsearch.protocol.xpack.ml.GetJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -84,6 +86,47 @@ public void putJobAsync(PutJobRequest request, RequestOptions options, ActionLis Collections.emptySet()); } + /** + * Gets one or more Machine Learning job configuration info. + * + *

+ * For additional info + * see + *

+ * @param request {@link GetJobRequest} request containing a list of jobId(s) and additional options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return {@link GetJobResponse} response object containing + * the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} objects and the number of jobs found + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public GetJobResponse getJob(GetJobRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::getJob, + options, + GetJobResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Gets one or more Machine Learning job configuration info, asynchronously. + * + *

+ * For additional info + * see + *

+ * @param request {@link GetJobRequest} request containing a list of jobId(s) and additional 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 with {@link GetJobResponse} upon request completion + */ + public void getJobAsync(GetJobRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::getJob, + options, + GetJobResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Deletes the given Machine Learning Job *

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 a313b99a54f5..9ed09d06b72f 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 @@ -20,12 +20,14 @@ package org.elasticsearch.client; import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.GetJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; import org.elasticsearch.protocol.xpack.ml.job.config.AnalysisConfig; @@ -54,6 +56,23 @@ public void testPutJob() throws IOException { } } + public void testGetJob() { + GetJobRequest getJobRequest = new GetJobRequest(); + + Request request = MLRequestConverters.getJob(getJobRequest); + + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors", request.getEndpoint()); + assertFalse(request.getParameters().containsKey("allow_no_jobs")); + + getJobRequest = new GetJobRequest("job1", "jobs*"); + getJobRequest.setAllowNoJobs(true); + request = MLRequestConverters.getJob(getJobRequest); + + assertEquals("/_xpack/ml/anomaly_detectors/job1,jobs*", request.getEndpoint()); + assertEquals(Boolean.toString(true), request.getParameters().get("allow_no_jobs")); + } + public void testOpenJob() throws Exception { String jobId = "some-job-id"; OpenJobRequest openJobRequest = new OpenJobRequest(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 2c0fc70b8486..cec5dd7ccf8f 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 @@ -24,6 +24,8 @@ import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; +import org.elasticsearch.protocol.xpack.ml.GetJobRequest; +import org.elasticsearch.protocol.xpack.ml.GetJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -37,7 +39,11 @@ import java.io.IOException; import java.util.Arrays; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; public class MachineLearningIT extends ESRestHighLevelClientTestCase { @@ -59,6 +65,41 @@ public void testPutJob() throws Exception { assertThat(createdJob.getJobType(), is(Job.ANOMALY_DETECTOR_JOB_TYPE)); } + public void testGetJob() throws Exception { + String jobId1 = randomValidJobId(); + String jobId2 = randomValidJobId(); + + Job job1 = buildJob(jobId1); + Job job2 = buildJob(jobId2); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job1), RequestOptions.DEFAULT); + machineLearningClient.putJob(new PutJobRequest(job2), RequestOptions.DEFAULT); + + GetJobRequest request = new GetJobRequest(jobId1, jobId2); + + // Test getting specific jobs + GetJobResponse response = execute(request, machineLearningClient::getJob, machineLearningClient::getJobAsync); + + assertEquals(2, response.count()); + assertThat(response.jobs(), hasSize(2)); + assertThat(response.jobs().stream().map(Job::getId).collect(Collectors.toList()), containsInAnyOrder(jobId1, jobId2)); + + // Test getting all jobs explicitly + request = GetJobRequest.getAllJobsRequest(); + response = execute(request, machineLearningClient::getJob, machineLearningClient::getJobAsync); + + assertTrue(response.count() >= 2L); + assertTrue(response.jobs().size() >= 2L); + assertThat(response.jobs().stream().map(Job::getId).collect(Collectors.toList()), hasItems(jobId1, jobId2)); + + // Test getting all jobs implicitly + response = execute(new GetJobRequest(), machineLearningClient::getJob, machineLearningClient::getJobAsync); + + assertTrue(response.count() >= 2L); + assertTrue(response.jobs().size() >= 2L); + assertThat(response.jobs().stream().map(Job::getId).collect(Collectors.toList()), hasItems(jobId1, jobId2)); + } + public void testDeleteJob() throws Exception { String jobId = randomValidJobId(); Job job = buildJob(jobId); 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 6e48036419b7..73531bae5532 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 @@ -30,6 +30,8 @@ import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; +import org.elasticsearch.protocol.xpack.ml.GetJobRequest; +import org.elasticsearch.protocol.xpack.ml.GetJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -46,8 +48,11 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { @@ -134,6 +139,63 @@ public void onFailure(Exception e) { } } + 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); + + Job secondJob = MachineLearningIT.buildJob("get-machine-learning-job2"); + client.machineLearning().putJob(new PutJobRequest(secondJob), RequestOptions.DEFAULT); + + { + //tag::x-pack-ml-get-job-request + GetJobRequest request = new GetJobRequest("get-machine-learning-job1", "get-machine-learning-job*"); //<1> + request.setAllowNoJobs(true); //<2> + //end::x-pack-ml-get-job-request + + //tag::x-pack-ml-get-job-execute + GetJobResponse response = client.machineLearning().getJob(request, RequestOptions.DEFAULT); + long numberOfJobs = response.count(); //<1> + List jobs = response.jobs(); //<2> + //end::x-pack-ml-get-job-execute + + assertEquals(2, response.count()); + assertThat(response.jobs(), hasSize(2)); + assertThat(response.jobs().stream().map(Job::getId).collect(Collectors.toList()), + containsInAnyOrder(job.getId(), secondJob.getId())); + } + { + GetJobRequest request = new GetJobRequest("get-machine-learning-job1", "get-machine-learning-job*"); + + // tag::x-pack-ml-get-job-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(GetJobResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::x-pack-ml-get-job-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-job-execute-async + client.machineLearning().getJobAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::x-pack-ml-get-job-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testDeleteJob() throws Exception { RestHighLevelClient client = highLevelClient(); diff --git a/docs/java-rest/high-level/ml/get-job.asciidoc b/docs/java-rest/high-level/ml/get-job.asciidoc new file mode 100644 index 000000000000..4ecf70e8e653 --- /dev/null +++ b/docs/java-rest/high-level/ml/get-job.asciidoc @@ -0,0 +1,57 @@ +[[java-rest-high-x-pack-ml-get-job]] +=== Get Job API + +The Get Job API provides the ability to get {ml} jobs in the cluster. +It accepts a `GetJobRequest` object and responds +with a `GetJobResponse` object. + +[[java-rest-high-x-pack-ml-get-job-request]] +==== Get Job Request + +A `GetJobRequest` object gets can have any number of `jobId` or `groupName` +entries. However, they all must be non-null. An empty list is the same as +requesting for all jobs. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-job-request] +-------------------------------------------------- +<1> Constructing a new request referencing existing `jobIds`, can contain wildcards +<2> Whether to ignore if a wildcard expression matches no jobs. + (This includes `_all` string or when no jobs have been specified) + +[[java-rest-high-x-pack-ml-get-job-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-job-execute] +-------------------------------------------------- +<1> `getCount()` from the `GetJobResponse` indicates the number of jobs found +<2> `getJobs()` is the collection of {ml} `Job` objects found + +[[java-rest-high-x-pack-ml-get-job-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-job-execute-async] +-------------------------------------------------- +<1> The `GetJobRequest` 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 `GetJobResponse` may +look like + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-job-listener] +-------------------------------------------------- +<1> `onResponse` is called back when the action is completed successfully +<2> `onFailure` is called back when some unexpected error occurs diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index b3de26e56bd0..c7b46b399622 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -205,11 +205,13 @@ include::licensing/delete-license.asciidoc[] The Java High Level REST Client supports the following Machine Learning APIs: * <> +* <> * <> * <> * <> include::ml/put-job.asciidoc[] +include::ml/get-job.asciidoc[] include::ml/delete-job.asciidoc[] include::ml/open-job.asciidoc[] include::ml/close-job.asciidoc[] diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/AbstractResultResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/AbstractResultResponse.java new file mode 100644 index 000000000000..64f350933c9c --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/AbstractResultResponse.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.protocol.xpack.ml; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Abstract class that provides a list of results and their count. + */ +public abstract class AbstractResultResponse extends ActionResponse implements ToXContentObject { + + public static final ParseField COUNT = new ParseField("count"); + + private final ParseField resultsField; + protected final List results; + protected final long count; + + AbstractResultResponse(ParseField resultsField, List results, long count) { + this.resultsField = Objects.requireNonNull(resultsField, + "[results_field] must not be null"); + this.results = Collections.unmodifiableList(results); + this.count = count; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(COUNT.getPreferredName(), count); + builder.field(resultsField.getPreferredName(), results); + builder.endObject(); + return builder; + } + + public long count() { + return count; + } +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java new file mode 100644 index 000000000000..b0377c86fdc7 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java @@ -0,0 +1,148 @@ +/* + * 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.protocol.xpack.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Request object to get {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} objects with the matching `jobId`s or + * `groupName`s. + * + * `_all` explicitly gets all the jobs in the cluster + * An empty request (no `jobId`s) implicitly gets all the jobs in the cluster + */ +public class GetJobRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField JOB_IDS = new ParseField("job_ids"); + public static final ParseField ALLOW_NO_JOBS = new ParseField("allow_no_jobs"); + + private static final String ALL_JOBS = "_all"; + private final List jobIds; + private Boolean allowNoJobs; + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "get_job_request", + true, a -> new GetJobRequest(a[0] == null ? new ArrayList<>() : (List) a[0])); + + static { + PARSER.declareStringArray(ConstructingObjectParser.optionalConstructorArg(), JOB_IDS); + PARSER.declareBoolean(GetJobRequest::setAllowNoJobs, ALLOW_NO_JOBS); + } + + /** + * Helper method to create a query that will get ALL jobs + * @return new {@link GetJobRequest} object searching for the jobId "_all" + */ + public static GetJobRequest getAllJobsRequest() { + return new GetJobRequest(ALL_JOBS); + } + + /** + * Get the specified {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} configurations via their unique jobIds + * @param jobIds must not contain any null values + */ + public GetJobRequest(String... jobIds) { + this(Arrays.asList(jobIds)); + } + + GetJobRequest(List jobIds) { + if (jobIds.stream().anyMatch(Objects::isNull)) { + throw new NullPointerException("jobIds must not contain null values"); + } + this.jobIds = new ArrayList<>(jobIds); + } + + /** + * All the jobIds for which to get configuration information + */ + public List getJobIds() { + return jobIds; + } + + + /** + * See {@link GetJobRequest#isAllowNoJobs()} + * @param allowNoJobs + */ + public void setAllowNoJobs(boolean allowNoJobs) { + this.allowNoJobs = allowNoJobs; + } + + /** + * Whether to ignore if a wildcard expression matches no jobs. + * + * If this is `false`, then an error is returned when a wildcard (or `_all`) does not match any jobs + */ + public Boolean isAllowNoJobs() { + return allowNoJobs; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public int hashCode() { + return Objects.hash(jobIds, allowNoJobs); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || other.getClass() != getClass()) { + return false; + } + + GetJobRequest that = (GetJobRequest) other; + return Objects.equals(jobIds, that.jobIds) && + Objects.equals(allowNoJobs, that.allowNoJobs); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + if (jobIds.isEmpty() == false) { + builder.field(JOB_IDS.getPreferredName(), jobIds); + } + + if (allowNoJobs != null) { + builder.field(ALLOW_NO_JOBS.getPreferredName(), allowNoJobs); + } + + builder.endObject(); + return builder; + } +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobResponse.java new file mode 100644 index 000000000000..4db542dc1526 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobResponse.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.protocol.xpack.ml; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.ml.job.config.Job; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * Contains a {@link List} of the found {@link Job} objects and the total count found + */ +public class GetJobResponse extends AbstractResultResponse { + + public static final ParseField RESULTS_FIELD = new ParseField("jobs"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("jobs_response", true, + a -> new GetJobResponse((List) a[0], (long) a[1])); + + static { + PARSER.declareObjectArray(constructorArg(), Job.PARSER, RESULTS_FIELD); + PARSER.declareLong(constructorArg(), AbstractResultResponse.COUNT); + } + + GetJobResponse(List jobBuilders, long count) { + super(RESULTS_FIELD, jobBuilders.stream().map(Job.Builder::build).collect(Collectors.toList()), count); + } + + /** + * The collection of {@link Job} objects found in the query + */ + public List jobs() { + return results; + } + + public static GetJobResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public int hashCode() { + return Objects.hash(results, count); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + GetJobResponse other = (GetJobResponse) obj; + return Objects.equals(results, other.results) && count == other.count; + } + + @Override + public final String toString() { + return Strings.toString(this); + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobRequestTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobRequestTests.java new file mode 100644 index 000000000000..b94b704fbf6e --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobRequestTests.java @@ -0,0 +1,69 @@ +/* + * 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.protocol.xpack.ml; + +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 GetJobRequestTests extends AbstractXContentTestCase { + + public void testAllJobsRequest() { + GetJobRequest request = GetJobRequest.getAllJobsRequest(); + + assertEquals(request.getJobIds().size(), 1); + assertEquals(request.getJobIds().get(0), "_all"); + } + + public void testNewWithJobId() { + Exception exception = expectThrows(NullPointerException.class, () -> new GetJobRequest("job",null)); + assertEquals(exception.getMessage(), "jobIds must not contain null values"); + } + + @Override + protected GetJobRequest createTestInstance() { + int jobCount = randomIntBetween(0, 10); + List jobIds = new ArrayList<>(jobCount); + + for (int i = 0; i < jobCount; i++) { + jobIds.add(randomAlphaOfLength(10)); + } + + GetJobRequest request = new GetJobRequest(jobIds); + + if (randomBoolean()) { + request.setAllowNoJobs(randomBoolean()); + } + + return request; + } + + @Override + protected GetJobRequest doParseInstance(XContentParser parser) throws IOException { + return GetJobRequest.PARSER.parse(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobResponseTests.java new file mode 100644 index 000000000000..79d4d678b929 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobResponseTests.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.protocol.xpack.ml; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.elasticsearch.protocol.xpack.ml.job.config.JobTests; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GetJobResponseTests extends AbstractXContentTestCase { + + @Override + protected GetJobResponse createTestInstance() { + + int count = randomIntBetween(1, 5); + List results = new ArrayList<>(count); + for(int i = 0; i < count; i++) { + results.add(JobTests.createRandomizedJobBuilder()); + } + + return new GetJobResponse(results, count); + } + + @Override + protected GetJobResponse doParseInstance(XContentParser parser) throws IOException { + return GetJobResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/JobTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/JobTests.java index 7ba4946efa75..61931743403e 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/JobTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/JobTests.java @@ -210,7 +210,7 @@ public static AnalysisConfig.Builder createAnalysisConfig() { return new AnalysisConfig.Builder(Arrays.asList(d1.build(), d2.build())); } - public static Job createRandomizedJob() { + public static Job.Builder createRandomizedJobBuilder() { String jobId = randomValidJobId(); Job.Builder builder = new Job.Builder(jobId); if (randomBoolean()) { @@ -265,7 +265,11 @@ public static Job createRandomizedJob() { if (randomBoolean()) { builder.setResultsIndexName(randomValidJobId()); } - return builder.build(); + return builder; + } + + public static Job createRandomizedJob() { + return createRandomizedJobBuilder().build(); } @Override From 9f588c953f514a3fbdef0e4d675614203ef1e8ee Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Wed, 22 Aug 2018 15:23:39 +1000 Subject: [PATCH 097/283] [TEST] Split tests and skip file permission test on Windows (#32781) Changes to split tests for keytab file test cases instead of randomized testing for testing branches in the code in the same test. On windows platform, for keytab file permission test, we required additional security permissions for the test framework. As this was the only test that required those permissions, skipping that test on windows platform. The same scenario gets tested in *nix environments. Closes#32768 --- .../authc/kerberos/KerberosRealmTests.java | 97 +++++++++---------- 1 file changed, 46 insertions(+), 51 deletions(-) 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 80e61a5545fe..9e6fafc481db 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 @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.security.authc.kerberos; +import org.apache.lucene.util.Constants; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; @@ -13,28 +14,27 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData; import org.ietf.jgss.GSSException; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.AclEntry; -import java.nio.file.attribute.AclEntryPermission; -import java.nio.file.attribute.AclEntryType; -import java.nio.file.attribute.AclFileAttributeView; -import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; -import java.nio.file.attribute.UserPrincipal; import java.util.Arrays; -import java.util.List; +import java.util.EnumSet; import java.util.Locale; import java.util.Set; @@ -110,52 +110,47 @@ public void testLookupUser() { assertThat(future.actionGet(), is(nullValue())); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32768") - public void testKerberosRealmWithInvalidKeytabPathConfigurations() throws IOException { - final String keytabPathCase = randomFrom("keytabPathAsDirectory", "keytabFileDoesNotExist", "keytabPathWithNoReadPermissions"); - final String expectedErrorMessage; - final String keytabPath; - switch (keytabPathCase) { - case "keytabPathAsDirectory": - final String dirName = randomAlphaOfLength(5); - Files.createDirectory(dir.resolve(dirName)); - keytabPath = dir.resolve(dirName).toString(); - expectedErrorMessage = "configured service key tab file [" + keytabPath + "] is a directory"; - break; - case "keytabFileDoesNotExist": - keytabPath = dir.resolve(randomAlphaOfLength(5) + ".keytab").toString(); - expectedErrorMessage = "configured service key tab file [" + keytabPath + "] does not exist"; - break; - case "keytabPathWithNoReadPermissions": - final String fileName = randomAlphaOfLength(5); - final Path keytabFilePath = Files.createTempFile(dir, fileName, ".keytab"); - Files.write(keytabFilePath, randomAlphaOfLength(5).getBytes(StandardCharsets.UTF_8)); - final Set supportedAttributes = keytabFilePath.getFileSystem().supportedFileAttributeViews(); - if (supportedAttributes.contains("posix")) { - final PosixFileAttributeView fileAttributeView = Files.getFileAttributeView(keytabFilePath, PosixFileAttributeView.class); - fileAttributeView.setPermissions(PosixFilePermissions.fromString("---------")); - } else if (supportedAttributes.contains("acl")) { - final UserPrincipal principal = Files.getOwner(keytabFilePath); - final AclFileAttributeView view = Files.getFileAttributeView(keytabFilePath, AclFileAttributeView.class); - final AclEntry entry = AclEntry.newBuilder() - .setType(AclEntryType.DENY) - .setPrincipal(principal) - .setPermissions(AclEntryPermission.READ_DATA, AclEntryPermission.READ_ATTRIBUTES).build(); - final List acl = view.getAcl(); - acl.add(0, entry); - view.setAcl(acl); - } else { - throw new UnsupportedOperationException( - String.format(Locale.ROOT, "Don't know how to make file [%s] non-readable on a file system with attributes [%s]", - keytabFilePath, supportedAttributes)); + public void testKerberosRealmThrowsErrorWhenKeytabPathIsConfiguredAsDirectory() throws IOException { + final String dirName = randomAlphaOfLength(5); + Files.createDirectory(dir.resolve(dirName)); + final String keytabPath = dir.resolve(dirName).toString(); + final String expectedErrorMessage = "configured service key tab file [" + keytabPath + "] is a directory"; + + assertKerberosRealmConstructorFails(keytabPath, expectedErrorMessage); + } + + public void testKerberosRealmThrowsErrorWhenKeytabFileDoesNotExist() throws IOException { + final String keytabPath = dir.resolve(randomAlphaOfLength(5) + ".keytab").toString(); + final String expectedErrorMessage = "configured service key tab file [" + keytabPath + "] does not exist"; + + assertKerberosRealmConstructorFails(keytabPath, expectedErrorMessage); + } + + public void testKerberosRealmThrowsErrorWhenKeytabFileHasNoReadPermissions() throws IOException { + assumeFalse("Not running this test on Windows, as it requires additional access permissions for test framework.", + Constants.WINDOWS); + final Set supportedAttributes = dir.getFileSystem().supportedFileAttributeViews(); + final String keytabFileName = randomAlphaOfLength(5) + ".keytab"; + final Path keytabPath; + if (supportedAttributes.contains("posix")) { + final Set filePerms = PosixFilePermissions.fromString("---------"); + final FileAttribute> fileAttributes = PosixFilePermissions.asFileAttribute(filePerms); + try (SeekableByteChannel byteChannel = Files.newByteChannel(dir.resolve(keytabFileName), + EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), fileAttributes)) { + byteChannel.write(ByteBuffer.wrap(randomByteArrayOfLength(10))); } - keytabPath = keytabFilePath.toString(); - expectedErrorMessage = "configured service key tab file [" + keytabPath + "] must have read permission"; - break; - default: - throw new IllegalArgumentException("Unknown test case :" + keytabPathCase); + keytabPath = dir.resolve(keytabFileName); + } else { + throw new UnsupportedOperationException( + String.format(Locale.ROOT, "Don't know how to make file [%s] non-readable on a file system with attributes [%s]", + dir.resolve(keytabFileName), supportedAttributes)); } + final String expectedErrorMessage = "configured service key tab file [" + keytabPath + "] must have read permission"; + + assertKerberosRealmConstructorFails(keytabPath.toString(), expectedErrorMessage); + } + private void assertKerberosRealmConstructorFails(final String keytabPath, final String expectedErrorMessage) { settings = KerberosTestCase.buildKerberosRealmSettings(keytabPath, 100, "10m", true, randomBoolean()); config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); From 0a4b55c9c0f1818d2199cc8468b70b9cc98e5ae8 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 22 Aug 2018 08:37:50 +0300 Subject: [PATCH 098/283] [DOCS] Add RequestedAuthnContext Documentation (#32946) Add documentation for #31238 - Add documentation for the req_authn_context_class_ref setting - Add a section in SAML Guide regarding the use of SAML Authentication Context. --- .../settings/security-settings.asciidoc | 9 +++++ .../authentication/saml-guide.asciidoc | 40 +++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 74deee3473fd..f1d8b555d562 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -861,6 +861,15 @@ The maximum amount of skew that can be tolerated between the IdP's clock and the {es} node's clock. Defaults to `3m` (3 minutes). +`req_authn_context_class_ref`:: +A comma separated list of Authentication Context Class Reference values to be +included in the Requested Authentication Context when requesting the IdP to +authenticate the current user. The Authentication Context of the corresponding +authentication response should contain at least one of the requested values. ++ +For more information, see +{stack-ov}/saml-guide-authentication.html#req-authn-context[Requesting specific authentication methods]. + [float] [[ref-saml-signing-settings]] ===== SAML realm signing settings diff --git a/x-pack/docs/en/security/authentication/saml-guide.asciidoc b/x-pack/docs/en/security/authentication/saml-guide.asciidoc index 633140f1238e..4facceff81cd 100644 --- a/x-pack/docs/en/security/authentication/saml-guide.asciidoc +++ b/x-pack/docs/en/security/authentication/saml-guide.asciidoc @@ -76,12 +76,13 @@ binding. There are five configuration steps to enable SAML authentication in {es}: -. Enable SSL/TLS for HTTP -. Enable the Token Service -. Create one or more SAML realms -. Configure role mappings +. <> +. <> +. <> +. <> . Generate a SAML Metadata file for use by your Identity Provider _(optional)_ +[[saml-enable-http]] ==== Enable TLS for HTTP If your {es} cluster is operating in production mode, then you must @@ -91,6 +92,7 @@ authentication. For more information, see {ref}/configuring-tls.html#tls-http[Encrypting HTTP Client Communications]. +[[saml-enable-token]] ==== Enable the token service The {es} SAML implementation makes use of the {es} Token Service. This service @@ -356,6 +358,35 @@ address such as `admin@staff.example.com.attacker.net`. It is important that you make sure your regular expressions are as precise as possible so that you do not inadvertently open an avenue for user impersonation attacks. +[[req-authn-context]] +==== Requesting specific authentication methods + +It is sometimes necessary for a SAML SP to be able to impose specific +restrictions regarding the authentication that will take place at an IdP, +in order to assess the level of confidence that it can place in +the corresponding authentication response. The restrictions might have to do +with the authentication method (password, client certificates, etc), the +user identification method during registration, and other details. {es} implements +https://docs.oasis-open.org/security/saml/v2.0/saml-authn-context-2.0-os.pdf[SAML 2.0 Authentication Context], which can be used for this purpose as defined in SAML 2.0 Core +Specification. + +In short, the SAML SP defines a set of Authentication Context Class Reference +values, which describe the restrictions to be imposed on the IdP, and sends these +in the Authentication Request. The IdP attempts to grant these restrictions. +If it cannot grant them, the authentication attempt fails. If the user is +successfully authenticated, the Authentication Statement of the SAML Response +contains an indication of the restrictions that were satisfied. + +You can define the Authentication Context Class Reference values by using the `req_authn_context_class_ref` option in the SAML realm configuration. See +{ref}/security-settings.html#ref-saml-settings[SAML realm settings]. + +{es} supports only the `exact` comparison method for the Authentication Context. +When it receives the Authentication Response from the IdP, {es} examines the +value of the Authentication Context Class Reference that is part of the +Authentication Statement of the SAML Assertion. If it matches one of the +requested values, the authentication is considered successful. Otherwise, the +authentication attempt fails. + [[saml-logout]] ==== SAML logout @@ -573,6 +604,7 @@ The passphrase for the keystore, if the file is encypted. This is a {ref}/secure-settings.html[secure setting] that must be set with the `elasticsearch-keystore` tool. +[[saml-sp-metadata]] === Generating SP metadata Some Identity Providers support importing a metadata file from the Service From 82d10b484a20514e70f6d773544ffea62e57f711 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Wed, 22 Aug 2018 09:05:22 +0300 Subject: [PATCH 099/283] Run forbidden api checks with runtimeJavaVersion (#32947) Run forbidden APIs checks with runtime hava version --- .../gradle/precommit/PrecommitTasks.groovy | 108 +++++------- ...ExportElasticsearchBuildResourcesTask.java | 3 +- .../precommit/ForbiddenApisCliTask.java | 154 ++++++++++++++++++ client/rest-high-level/build.gradle | 6 +- client/rest/build.gradle | 13 +- client/sniffer/build.gradle | 7 +- client/test/build.gradle | 8 +- client/transport/build.gradle | 6 +- .../tools/java-version-checker/build.gradle | 6 +- distribution/tools/launchers/build.gradle | 12 +- libs/cli/build.gradle | 5 +- libs/core/build.gradle | 4 +- libs/dissect/build.gradle | 4 +- libs/grok/build.gradle | 4 +- libs/nio/build.gradle | 5 +- libs/secure-sm/build.gradle | 5 +- libs/x-content/build.gradle | 4 +- plugins/analysis-icu/build.gradle | 4 +- qa/vagrant/build.gradle | 6 +- test/framework/build.gradle | 7 +- test/logger-usage/build.gradle | 4 +- .../ml/log-structure-finder/build.gradle | 4 +- x-pack/plugin/security/build.gradle | 3 +- x-pack/plugin/sql/jdbc/build.gradle | 2 +- x-pack/plugin/sql/sql-action/build.gradle | 5 +- x-pack/plugin/sql/sql-cli/build.gradle | 8 +- x-pack/plugin/sql/sql-client/build.gradle | 2 +- x-pack/plugin/sql/sql-proto/build.gradle | 5 +- x-pack/qa/sql/build.gradle | 4 +- x-pack/transport-client/build.gradle | 5 +- 30 files changed, 241 insertions(+), 172 deletions(-) create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index 42dc29df058c..b63b1f40d804 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -18,18 +18,12 @@ */ package org.elasticsearch.gradle.precommit -import de.thetaphi.forbiddenapis.gradle.CheckForbiddenApis -import de.thetaphi.forbiddenapis.gradle.ForbiddenApisPlugin import org.elasticsearch.gradle.ExportElasticsearchBuildResourcesTask -import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.Task -import org.gradle.api.file.FileCollection +import org.gradle.api.artifacts.Configuration import org.gradle.api.plugins.JavaBasePlugin import org.gradle.api.plugins.quality.Checkstyle -import org.gradle.api.tasks.JavaExec -import org.gradle.api.tasks.StopExecutionException - /** * Validation tasks which should be run before committing. These run before tests. */ @@ -38,8 +32,8 @@ class PrecommitTasks { /** Adds a precommit task, which depends on non-test verification tasks. */ public static Task create(Project project, boolean includeDependencyLicenses) { List precommitTasks = [ - configureForbiddenApis(project), configureCheckstyle(project), + configureForbiddenApisCli(project), configureNamingConventions(project), project.tasks.create('forbiddenPatterns', ForbiddenPatternsTask.class), project.tasks.create('licenseHeaders', LicenseHeadersTask.class), @@ -48,9 +42,6 @@ class PrecommitTasks { project.tasks.create('thirdPartyAudit', ThirdPartyAuditTask.class) ] - // Configure it but don't add it as a dependency yet - configureForbiddenApisCli(project) - // tasks with just tests don't need dependency licenses, so this flag makes adding // the task optional if (includeDependencyLicenses) { @@ -84,77 +75,60 @@ class PrecommitTasks { return project.tasks.create(precommitOptions) } - private static Task configureForbiddenApis(Project project) { - project.pluginManager.apply(ForbiddenApisPlugin.class) - project.forbiddenApis { - failOnUnsupportedJava = false - bundledSignatures = ['jdk-unsafe', 'jdk-deprecated', 'jdk-non-portable', 'jdk-system-out'] - signaturesURLs = [getClass().getResource('/forbidden/jdk-signatures.txt'), - getClass().getResource('/forbidden/es-all-signatures.txt')] - suppressAnnotations = ['**.SuppressForbidden'] - } - project.tasks.withType(CheckForbiddenApis) { - // we do not use the += operator to add signatures, as conventionMappings of Gradle do not work when it's configured using withType: - if (name.endsWith('Test')) { - signaturesURLs = project.forbiddenApis.signaturesURLs + - [ getClass().getResource('/forbidden/es-test-signatures.txt'), getClass().getResource('/forbidden/http-signatures.txt') ] - } else { - signaturesURLs = project.forbiddenApis.signaturesURLs + - [ getClass().getResource('/forbidden/es-server-signatures.txt') ] - } - // forbidden apis doesn't support Java 11, so stop at 10 - String targetMajorVersion = (project.compilerJavaVersion.compareTo(JavaVersion.VERSION_1_10) > 0 ? - JavaVersion.VERSION_1_10 : - project.compilerJavaVersion).getMajorVersion() - targetCompatibility = Integer.parseInt(targetMajorVersion) >= 9 ?targetMajorVersion : "1.${targetMajorVersion}" - } - Task forbiddenApis = project.tasks.findByName('forbiddenApis') - forbiddenApis.group = "" // clear group, so this does not show up under verification tasks - - return forbiddenApis - } - private static Task configureForbiddenApisCli(Project project) { - project.configurations.create("forbiddenApisCliJar") + Configuration forbiddenApisConfiguration = project.configurations.create("forbiddenApisCliJar") project.dependencies { - forbiddenApisCliJar 'de.thetaphi:forbiddenapis:2.5' + forbiddenApisCliJar ('de.thetaphi:forbiddenapis:2.5') } - Task forbiddenApisCli = project.tasks.create('forbiddenApisCli') + Task forbiddenApisCli = project.tasks.create('forbiddenApis') project.sourceSets.forEach { sourceSet -> forbiddenApisCli.dependsOn( - project.tasks.create(sourceSet.getTaskName('forbiddenApisCli', null), JavaExec) { + project.tasks.create(sourceSet.getTaskName('forbiddenApis', null), ForbiddenApisCliTask) { ExportElasticsearchBuildResourcesTask buildResources = project.tasks.getByName('buildResources') dependsOn(buildResources) - classpath = project.files( - project.configurations.forbiddenApisCliJar, + execAction = { spec -> + spec.classpath = project.files( + project.configurations.forbiddenApisCliJar, + sourceSet.compileClasspath, + sourceSet.runtimeClasspath + ) + spec.executable = "${project.runtimeJavaHome}/bin/java" + } + inputs.files( + forbiddenApisConfiguration, sourceSet.compileClasspath, sourceSet.runtimeClasspath ) - main = 'de.thetaphi.forbiddenapis.cli.CliMain' - executable = "${project.runtimeJavaHome}/bin/java" - args "-b", 'jdk-unsafe-1.8' - args "-b", 'jdk-deprecated-1.8' - args "-b", 'jdk-non-portable' - args "-b", 'jdk-system-out' - args "-f", buildResources.copy("forbidden/jdk-signatures.txt") - args "-f", buildResources.copy("forbidden/es-all-signatures.txt") - args "--suppressannotation", '**.SuppressForbidden' + + targetCompatibility = project.compilerJavaVersion + bundledSignatures = [ + "jdk-unsafe", "jdk-deprecated", "jdk-non-portable", "jdk-system-out" + ] + signaturesFiles = project.files( + buildResources.copy("forbidden/jdk-signatures.txt"), + buildResources.copy("forbidden/es-all-signatures.txt") + ) + suppressAnnotations = ['**.SuppressForbidden'] if (sourceSet.name == 'test') { - args "-f", buildResources.copy("forbidden/es-test-signatures.txt") - args "-f", buildResources.copy("forbidden/http-signatures.txt") + signaturesFiles += project.files( + buildResources.copy("forbidden/es-test-signatures.txt"), + buildResources.copy("forbidden/http-signatures.txt") + ) } else { - args "-f", buildResources.copy("forbidden/es-server-signatures.txt") + signaturesFiles += project.files(buildResources.copy("forbidden/es-server-signatures.txt")) } dependsOn sourceSet.classesTaskName - doFirst { - // Forbidden APIs expects only existing dirs, and requires at least one - FileCollection existingOutputs = sourceSet.output.classesDirs - .filter { it.exists() } - if (existingOutputs.isEmpty()) { - throw new StopExecutionException("${sourceSet.name} has no outputs") - } - existingOutputs.forEach { args "-d", it } + classesDirs = sourceSet.output.classesDirs + ext.replaceSignatureFiles = { String... names -> + signaturesFiles = project.files( + names.collect { buildResources.copy("forbidden/${it}.txt") } + ) + } + ext.addSignatureFiles = { String... names -> + signaturesFiles += project.files( + names.collect { buildResources.copy("forbidden/${it}.txt") } + ) } } ) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTask.java index 03c18f54e67e..4af104093a5c 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTask.java @@ -35,6 +35,7 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -105,7 +106,7 @@ public void doExport() { if (is == null) { throw new GradleException("Can't export `" + resourcePath + "` from build-tools: not found"); } - Files.copy(is, destination); + Files.copy(is, destination, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new GradleException("Can't write resource `" + resourcePath + "` to " + destination, e); } diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java new file mode 100644 index 000000000000..e33f16709641 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java @@ -0,0 +1,154 @@ +/* + * 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.gradle.precommit; + +import de.thetaphi.forbiddenapis.cli.CliMain; +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.JavaVersion; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.SkipWhenEmpty; +import org.gradle.api.tasks.TaskAction; +import org.gradle.process.JavaExecSpec; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class ForbiddenApisCliTask extends DefaultTask { + + private FileCollection signaturesFiles; + private List signatures = new ArrayList<>(); + private Set bundledSignatures = new LinkedHashSet<>(); + private Set suppressAnnotations = new LinkedHashSet<>(); + private JavaVersion targetCompatibility; + private FileCollection classesDirs; + private Action execAction; + + public JavaVersion getTargetCompatibility() { + return targetCompatibility; + } + + public void setTargetCompatibility(JavaVersion targetCompatibility) { + this.targetCompatibility = targetCompatibility; + } + + public Action getExecAction() { + return execAction; + } + + public void setExecAction(Action execAction) { + this.execAction = execAction; + } + + @OutputFile + public File getMarkerFile() { + return new File( + new File(getProject().getBuildDir(), "precommit"), + getName() + ); + } + + @InputFiles + @SkipWhenEmpty + public FileCollection getClassesDirs() { + return classesDirs.filter(File::exists); + } + + public void setClassesDirs(FileCollection classesDirs) { + this.classesDirs = classesDirs; + } + + @InputFiles + public FileCollection getSignaturesFiles() { + return signaturesFiles; + } + + public void setSignaturesFiles(FileCollection signaturesFiles) { + this.signaturesFiles = signaturesFiles; + } + + @Input + public List getSignatures() { + return signatures; + } + + public void setSignatures(List signatures) { + this.signatures = signatures; + } + + @Input + public Set getBundledSignatures() { + return bundledSignatures; + } + + public void setBundledSignatures(Set bundledSignatures) { + this.bundledSignatures = bundledSignatures; + } + + @Input + public Set getSuppressAnnotations() { + return suppressAnnotations; + } + + public void setSuppressAnnotations(Set suppressAnnotations) { + this.suppressAnnotations = suppressAnnotations; + } + + @TaskAction + public void runForbiddenApisAndWriteMarker() throws IOException { + getProject().javaexec((JavaExecSpec spec) -> { + execAction.execute(spec); + spec.setMain(CliMain.class.getName()); + // build the command line + getSignaturesFiles().forEach(file -> spec.args("-f", file.getAbsolutePath())); + getSuppressAnnotations().forEach(annotation -> spec.args("--suppressannotation", annotation)); + getBundledSignatures().forEach(bundled -> { + // there's no option for target compatibility so we have to interpret it + final String prefix; + if (bundled.equals("jdk-system-out") || + bundled.equals("jdk-reflection") || + bundled.equals("jdk-non-portable")) { + prefix = ""; + } else { + prefix = "-" + ( + getTargetCompatibility().compareTo(JavaVersion.VERSION_1_9) >= 0 ? + getTargetCompatibility().getMajorVersion() : + "1." + getTargetCompatibility().getMajorVersion()) + ; + } + spec.args("-b", bundled + prefix); + } + ); + getClassesDirs().forEach(dir -> + spec.args("-d", dir) + ); + }); + Files.write(getMarkerFile().toPath(), Collections.emptyList()); + } + +} diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle index 48169faac2fc..9acfc630f94f 100644 --- a/client/rest-high-level/build.gradle +++ b/client/rest-high-level/build.gradle @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks import org.elasticsearch.gradle.test.RestIntegTestTask import org.gradle.api.internal.provider.Providers @@ -75,8 +73,8 @@ dependencyLicenses { forbiddenApisMain { // core does not depend on the httpclient for compile so we add the signatures here. We don't add them for test as they are already // specified - signaturesURLs += [PrecommitTasks.getResource('/forbidden/http-signatures.txt')] - signaturesURLs += [file('src/main/resources/forbidden/rest-high-level-signatures.txt').toURI().toURL()] + addSignatureFiles 'http-signatures' + signaturesFiles += files('src/main/resources/forbidden/rest-high-level-signatures.txt') } integTestCluster { diff --git a/client/rest/build.gradle b/client/rest/build.gradle index fc2ab0bc4c05..273836a31f0c 100644 --- a/client/rest/build.gradle +++ b/client/rest/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.precommit.ForbiddenApisCliTask + /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -16,9 +18,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' @@ -53,10 +52,9 @@ dependencies { testCompile "org.elasticsearch:mocksocket:${versions.mocksocket}" } -forbiddenApisMain { +tasks.withType(ForbiddenApisCliTask) { //client does not depend on server, so only jdk and http signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt'), - PrecommitTasks.getResource('/forbidden/http-signatures.txt')] + replaceSignatureFiles ('jdk-signatures', 'http-signatures') } forbiddenPatterns { @@ -67,9 +65,6 @@ forbiddenApisTest { //we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage bundledSignatures -= 'jdk-non-portable' bundledSignatures += 'jdk-internal' - //client does not depend on server, so only jdk signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt'), - PrecommitTasks.getResource('/forbidden/http-signatures.txt')] } // JarHell is part of es server, which we don't want to pull in diff --git a/client/sniffer/build.gradle b/client/sniffer/build.gradle index 41146e0b7ec0..6ba69c5713c5 100644 --- a/client/sniffer/build.gradle +++ b/client/sniffer/build.gradle @@ -16,9 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' @@ -55,7 +52,7 @@ dependencies { forbiddenApisMain { //client does not depend on server, so only jdk signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } forbiddenApisTest { @@ -63,7 +60,7 @@ forbiddenApisTest { bundledSignatures -= 'jdk-non-portable' bundledSignatures += 'jdk-internal' //client does not depend on server, so only jdk signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } dependencyLicenses { diff --git a/client/test/build.gradle b/client/test/build.gradle index cc69a1828dc8..e66d2be57f1e 100644 --- a/client/test/build.gradle +++ b/client/test/build.gradle @@ -16,10 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks -import org.gradle.api.JavaVersion - apply plugin: 'elasticsearch.build' targetCompatibility = JavaVersion.VERSION_1_7 @@ -36,7 +32,7 @@ dependencies { forbiddenApisMain { //client does not depend on core, so only jdk signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } forbiddenApisTest { @@ -44,7 +40,7 @@ forbiddenApisTest { bundledSignatures -= 'jdk-non-portable' bundledSignatures += 'jdk-internal' //client does not depend on core, so only jdk signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } // JarHell is part of es server, which we don't want to pull in diff --git a/client/transport/build.gradle b/client/transport/build.gradle index 944a038edd97..269a37105fb1 100644 --- a/client/transport/build.gradle +++ b/client/transport/build.gradle @@ -16,9 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' @@ -47,8 +44,7 @@ dependencyLicenses { forbiddenApisTest { // we don't use the core test-framework, no lucene classes present so we don't want the es-test-signatures to // be pulled in - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt'), - PrecommitTasks.getResource('/forbidden/es-all-signatures.txt')] + replaceSignatureFiles 'jdk-signatures', 'es-all-signatures' } namingConventions { diff --git a/distribution/tools/java-version-checker/build.gradle b/distribution/tools/java-version-checker/build.gradle index ad9b56fec050..6d18b79d4bdd 100644 --- a/distribution/tools/java-version-checker/build.gradle +++ b/distribution/tools/java-version-checker/build.gradle @@ -1,11 +1,11 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' targetCompatibility = JavaVersion.VERSION_1_7 // java_version_checker do not depend on core so only JDK signatures should be checked -forbiddenApisMain.signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] +forbiddenApisMain { + replaceSignatureFiles 'jdk-signatures' +} test.enabled = false namingConventions.enabled = false diff --git a/distribution/tools/launchers/build.gradle b/distribution/tools/launchers/build.gradle index a774691b2eb1..ca1aa6bcac9d 100644 --- a/distribution/tools/launchers/build.gradle +++ b/distribution/tools/launchers/build.gradle @@ -17,8 +17,9 @@ * under the License. */ -import org.elasticsearch.gradle.precommit.PrecommitTasks -import org.gradle.api.JavaVersion + + +import org.elasticsearch.gradle.precommit.ForbiddenApisCliTask apply plugin: 'elasticsearch.build' @@ -31,10 +32,9 @@ dependencies { archivesBaseName = 'elasticsearch-launchers' -// java_version_checker do not depend on core so only JDK signatures should be checked -List jdkSignatures = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] -forbiddenApisMain.signaturesURLs = jdkSignatures -forbiddenApisTest.signaturesURLs = jdkSignatures +tasks.withType(ForbiddenApisCliTask) { + replaceSignatureFiles 'jdk-signatures' +} namingConventions { testClass = 'org.elasticsearch.tools.launchers.LaunchersTestCase' diff --git a/libs/cli/build.gradle b/libs/cli/build.gradle index 00d6d96ef0d5..b1f3b338255c 100644 --- a/libs/cli/build.gradle +++ b/libs/cli/build.gradle @@ -16,9 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' apply plugin: 'nebula.optional-base' apply plugin: 'nebula.maven-base-publish' @@ -34,5 +31,5 @@ test.enabled = false jarHell.enabled = false forbiddenApisMain { - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } diff --git a/libs/core/build.gradle b/libs/core/build.gradle index 2017c2a418ac..cc5c1e20fc16 100644 --- a/libs/core/build.gradle +++ b/libs/core/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -91,7 +89,7 @@ dependencies { forbiddenApisMain { // :libs:core does not depend on server // TODO: Need to decide how we want to handle for forbidden signatures with the changes to server - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } if (isEclipse) { diff --git a/libs/dissect/build.gradle b/libs/dissect/build.gradle index c09a2a4ebd1b..853c78646c25 100644 --- a/libs/dissect/build.gradle +++ b/libs/dissect/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -33,7 +31,7 @@ dependencies { } forbiddenApisMain { - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } if (isEclipse) { diff --git a/libs/grok/build.gradle b/libs/grok/build.gradle index 61437be6aff1..37b494624edd 100644 --- a/libs/grok/build.gradle +++ b/libs/grok/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -34,7 +32,7 @@ dependencies { } forbiddenApisMain { - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } if (isEclipse) { diff --git a/libs/nio/build.gradle b/libs/nio/build.gradle index 43c9a133a3f3..f6a6ff652450 100644 --- a/libs/nio/build.gradle +++ b/libs/nio/build.gradle @@ -16,9 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' @@ -62,5 +59,5 @@ if (isEclipse) { forbiddenApisMain { // nio does not depend on core, so only jdk signatures should be checked // es-all is not checked as we connect and accept sockets - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } diff --git a/libs/secure-sm/build.gradle b/libs/secure-sm/build.gradle index 93fdfd01c8f0..3baf3513b120 100644 --- a/libs/secure-sm/build.gradle +++ b/libs/secure-sm/build.gradle @@ -16,9 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' @@ -47,7 +44,7 @@ dependencies { } forbiddenApisMain { - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } if (isEclipse) { diff --git a/libs/x-content/build.gradle b/libs/x-content/build.gradle index c8b37108ff93..0ec4e0d6ad31 100644 --- a/libs/x-content/build.gradle +++ b/libs/x-content/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -57,7 +55,7 @@ dependencies { forbiddenApisMain { // x-content does not depend on server // TODO: Need to decide how we want to handle for forbidden signatures with the changes to core - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } if (isEclipse) { diff --git a/plugins/analysis-icu/build.gradle b/plugins/analysis-icu/build.gradle index 1883e3bf1b9d..676fd4481315 100644 --- a/plugins/analysis-icu/build.gradle +++ b/plugins/analysis-icu/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.precommit.ForbiddenApisCliTask + /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -22,7 +24,7 @@ esplugin { classname 'org.elasticsearch.plugin.analysis.icu.AnalysisICUPlugin' } -forbiddenApis { +tasks.withType(ForbiddenApisCliTask) { signatures += [ "com.ibm.icu.text.Collator#getInstance() @ Don't use default locale, use getInstance(ULocale) instead" ] diff --git a/qa/vagrant/build.gradle b/qa/vagrant/build.gradle index 4a0c91469629..4c3b48cbac94 100644 --- a/qa/vagrant/build.gradle +++ b/qa/vagrant/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -69,9 +67,7 @@ esvagrant { } forbiddenApisMain { - signaturesURLs = [ - PrecommitTasks.getResource('/forbidden/jdk-signatures.txt') - ] + replaceSignatureFiles 'jdk-signatures' } // we don't have additional tests for the tests themselves diff --git a/test/framework/build.gradle b/test/framework/build.gradle index ab513a1b0bb2..8179e3d096a1 100644 --- a/test/framework/build.gradle +++ b/test/framework/build.gradle @@ -16,9 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks; - dependencies { compile "org.elasticsearch.client:elasticsearch-rest-client:${version}" compile "org.elasticsearch.client:elasticsearch-rest-client-sniffer:${version}" @@ -41,9 +38,7 @@ compileTestJava.options.compilerArgs << '-Xlint:-rawtypes' // the main files are actually test files, so use the appropriate forbidden api sigs forbiddenApisMain { - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt'), - PrecommitTasks.getResource('/forbidden/es-all-signatures.txt'), - PrecommitTasks.getResource('/forbidden/es-test-signatures.txt')] + replaceSignatureFiles 'jdk-signatures', 'es-all-signatures', 'es-test-signatures' } // TODO: should we have licenses for our test deps? diff --git a/test/logger-usage/build.gradle b/test/logger-usage/build.gradle index c16dab6a625c..0f02283e5373 100644 --- a/test/logger-usage/build.gradle +++ b/test/logger-usage/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with @@ -29,7 +27,7 @@ loggerUsageCheck.enabled = false forbiddenApisMain.enabled = true // disabled by parent project forbiddenApisMain { - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] // does not depend on core, only jdk signatures + replaceSignatureFiles 'jdk-signatures' // does not depend on core, only jdk signatures } jarHell.enabled = true // disabled by parent project diff --git a/x-pack/plugin/ml/log-structure-finder/build.gradle b/x-pack/plugin/ml/log-structure-finder/build.gradle index 9048a1c46860..f5dff6dc8464 100644 --- a/x-pack/plugin/ml/log-structure-finder/build.gradle +++ b/x-pack/plugin/ml/log-structure-finder/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' archivesBaseName = 'x-pack-log-structure-finder' @@ -31,6 +29,6 @@ artifacts { forbiddenApisMain { // log-structure-finder does not depend on server, so cannot forbid server methods - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 003263669d51..5198c3da6698 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -158,8 +158,7 @@ forbiddenPatterns { } forbiddenApisMain { - signaturesURLs += file('forbidden/ldap-signatures.txt').toURI().toURL() - signaturesURLs += file('forbidden/xml-signatures.txt').toURI().toURL() + signaturesFiles += files('forbidden/ldap-signatures.txt', 'forbidden/xml-signatures.txt') } // classes are missing, e.g. com.ibm.icu.lang.UCharacter diff --git a/x-pack/plugin/sql/jdbc/build.gradle b/x-pack/plugin/sql/jdbc/build.gradle index a0d9b24c5072..1a7d6115e155 100644 --- a/x-pack/plugin/sql/jdbc/build.gradle +++ b/x-pack/plugin/sql/jdbc/build.gradle @@ -8,7 +8,7 @@ archivesBaseName = "x-pack-sql-jdbc" forbiddenApisMain { // does not depend on core, so only jdk and http signatures should be checked - signaturesURLs = [this.class.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } dependencies { diff --git a/x-pack/plugin/sql/sql-action/build.gradle b/x-pack/plugin/sql/sql-action/build.gradle index bf79fd824ef8..345318d20b80 100644 --- a/x-pack/plugin/sql/sql-action/build.gradle +++ b/x-pack/plugin/sql/sql-action/build.gradle @@ -2,9 +2,6 @@ /* * This project contains transport-level requests and responses that are shared between x-pack plugin and qa tests */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' description = 'Request and response objects shared by the cli, jdbc ' + @@ -34,7 +31,7 @@ dependencies { forbiddenApisMain { //sql does not depend on server, so only jdk signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } dependencyLicenses { diff --git a/x-pack/plugin/sql/sql-cli/build.gradle b/x-pack/plugin/sql/sql-cli/build.gradle index b90b07abad3d..0b2559c6a84a 100644 --- a/x-pack/plugin/sql/sql-cli/build.gradle +++ b/x-pack/plugin/sql/sql-cli/build.gradle @@ -1,3 +1,4 @@ +import org.elasticsearch.gradle.precommit.ForbiddenApisCliTask /* * This project is named sql-cli because it is in the "org.elasticsearch.plugin" @@ -74,11 +75,8 @@ artifacts { } -forbiddenApisMain { - signaturesURLs += file('src/forbidden/cli-signatures.txt').toURI().toURL() -} -forbiddenApisTest { - signaturesURLs += file('src/forbidden/cli-signatures.txt').toURI().toURL() +tasks.withType(ForbiddenApisCliTask) { + signaturesFiles += files('src/forbidden/cli-signatures.txt') } thirdPartyAudit.excludes = [ diff --git a/x-pack/plugin/sql/sql-client/build.gradle b/x-pack/plugin/sql/sql-client/build.gradle index fbc411e44596..c4ee030d4568 100644 --- a/x-pack/plugin/sql/sql-client/build.gradle +++ b/x-pack/plugin/sql/sql-client/build.gradle @@ -26,7 +26,7 @@ dependencyLicenses { forbiddenApisMain { // does not depend on core, so only jdk and http signatures should be checked - signaturesURLs = [this.class.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } forbiddenApisTest { diff --git a/x-pack/plugin/sql/sql-proto/build.gradle b/x-pack/plugin/sql/sql-proto/build.gradle index 7f26176e3c7a..7d28336bfc51 100644 --- a/x-pack/plugin/sql/sql-proto/build.gradle +++ b/x-pack/plugin/sql/sql-proto/build.gradle @@ -2,9 +2,6 @@ /* * This project contains XContent protocol classes shared between server and http client */ - -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' description = 'Request and response objects shared by the cli, jdbc ' + @@ -25,7 +22,7 @@ dependencies { forbiddenApisMain { //sql does not depend on server, so only jdk signatures should be checked - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')] + replaceSignatureFiles 'jdk-signatures' } dependencyLicenses { diff --git a/x-pack/qa/sql/build.gradle b/x-pack/qa/sql/build.gradle index 17a1d5acdc99..baaf0451e51f 100644 --- a/x-pack/qa/sql/build.gradle +++ b/x-pack/qa/sql/build.gradle @@ -1,4 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks import org.elasticsearch.gradle.test.RunTask description = 'Integration tests for SQL' @@ -29,8 +28,7 @@ dependenciesInfo.enabled = false // the main files are actually test files, so use the appropriate forbidden api sigs forbiddenApisMain { - signaturesURLs = [PrecommitTasks.getResource('/forbidden/es-all-signatures.txt'), - PrecommitTasks.getResource('/forbidden/es-test-signatures.txt')] + replaceSignatureFiles 'es-all-signatures', 'es-test-signatures' } thirdPartyAudit.excludes = [ diff --git a/x-pack/transport-client/build.gradle b/x-pack/transport-client/build.gradle index 2e350ef98ff5..a96f4146fbf6 100644 --- a/x-pack/transport-client/build.gradle +++ b/x-pack/transport-client/build.gradle @@ -1,5 +1,3 @@ -import org.elasticsearch.gradle.precommit.PrecommitTasks - apply plugin: 'elasticsearch.build' apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' @@ -22,8 +20,7 @@ dependencyLicenses.enabled = false forbiddenApisTest { // we don't use the core test-framework, no lucene classes present so we don't want the es-test-signatures to // be pulled in - signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt'), - PrecommitTasks.getResource('/forbidden/es-all-signatures.txt')] + replaceSignatureFiles 'jdk-signatures', 'es-all-signatures' } namingConventions { From ffb1a5d5b795e40fb7b9afff2a04fb0fc3166d5c Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Wed, 22 Aug 2018 08:45:08 +0200 Subject: [PATCH 100/283] Expose `max_concurrent_shard_requests` in `_msearch` (#33016) Today `_msearch` doesn't allow modifying the `max_concurrent_shard_requests` per sub search request. This change adds support for setting this parameter on all sub-search requests in an `_msearch`. Relates to #31877 --- docs/reference/search/multi-search.asciidoc | 10 ++++++ .../resources/rest-api-spec/api/msearch.json | 5 +++ .../rest-api-spec/test/msearch/10_basic.yml | 32 +++++++++++++++++++ .../action/search/RestMultiSearchAction.java | 11 +++++++ 4 files changed, 58 insertions(+) diff --git a/docs/reference/search/multi-search.asciidoc b/docs/reference/search/multi-search.asciidoc index c68cf0daaf55..8771915dee69 100644 --- a/docs/reference/search/multi-search.asciidoc +++ b/docs/reference/search/multi-search.asciidoc @@ -86,6 +86,16 @@ The msearch's `max_concurrent_searches` request parameter can be used to control the maximum number of concurrent searches the multi search api will execute. This default is based on the number of data nodes and the default search thread pool size. +The request parameter `max_concurrent_shard_requests` can be used to control the +maximum number of concurrent shard requests the each sub search request will execute. +This parameter should be used to protect a single request from overloading a cluster +(e.g., a default request will hit all indices in a cluster which could cause shard request rejections +if the number of shards per node is high). This default is based on the number of +data nodes in the cluster but at most `256`.In certain scenarios parallelism isn't achieved through +concurrent request such that this protection will result in poor performance. For +instance in an environment where only a very low number of concurrent search requests are expected +it might help to increase this value to a higher number. + [float] [[msearch-security]] === Security diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/msearch.json b/rest-api-spec/src/main/resources/rest-api-spec/api/msearch.json index 090c429fd82c..13281a2a232f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/msearch.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/msearch.json @@ -33,6 +33,11 @@ "type" : "number", "description" : "A threshold that enforces a pre-filter roundtrip to prefilter search shards based on query rewriting if the number of shards the search request expands to exceeds the threshold. This filter roundtrip can limit the number of shards significantly if for instance a shard can not match any documents based on it's rewrite method ie. if date filters are mandatory to match but the shard bounds and the query are disjoint.", "default" : 128 + }, + "max_concurrent_shard_requests" : { + "type" : "number", + "description" : "The number of concurrent shard requests each sub search executes concurrently. This value should be used to limit the impact of the search on the cluster in order to limit the number of concurrent shard requests", + "default" : "The default grows with the number of nodes in the cluster but is at most 256." } } }, diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/msearch/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/msearch/10_basic.yml index 536e2bfaf949..fb884ddfca2c 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/msearch/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/msearch/10_basic.yml @@ -61,3 +61,35 @@ setup: - match: { responses.3.error.root_cause.0.reason: "/no.such.index/" } - match: { responses.3.error.root_cause.0.index: index_3 } - match: { responses.4.hits.total: 4 } + +--- +"Least impact smoke test": +# only passing these parameters to make sure they are consumed + - do: + max_concurrent_shard_requests: 1 + max_concurrent_searches: 1 + msearch: + body: + - index: index_* + - query: + match: {foo: foo} + - index: index_2 + - query: + match_all: {} + - index: index_1 + - query: + match: {foo: foo} + - index: index_3 + - query: + match_all: {} + - type: test + - query: + match_all: {} + + - match: { responses.0.hits.total: 2 } + - match: { responses.1.hits.total: 1 } + - match: { responses.2.hits.total: 1 } + - match: { responses.3.error.root_cause.0.type: index_not_found_exception } + - match: { responses.3.error.root_cause.0.reason: "/no.such.index/" } + - match: { responses.3.error.root_cause.0.index: index_3 } + - match: { responses.4.hits.total: 4 } diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java index 2a60262b32f5..6239015dae41 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java @@ -86,6 +86,14 @@ public static MultiSearchRequest parseRequest(RestRequest restRequest, boolean a int preFilterShardSize = restRequest.paramAsInt("pre_filter_shard_size", SearchRequest.DEFAULT_PRE_FILTER_SHARD_SIZE); + final Integer maxConcurrentShardRequests; + if (restRequest.hasParam("max_concurrent_shard_requests")) { + // only set if we have the parameter since we auto adjust the max concurrency on the coordinator + // based on the number of nodes in the cluster + maxConcurrentShardRequests = restRequest.paramAsInt("max_concurrent_shard_requests", Integer.MIN_VALUE); + } else { + maxConcurrentShardRequests = null; + } parseMultiLineRequest(restRequest, multiRequest.indicesOptions(), allowExplicitIndex, (searchRequest, parser) -> { searchRequest.source(SearchSourceBuilder.fromXContent(parser, false)); @@ -96,6 +104,9 @@ public static MultiSearchRequest parseRequest(RestRequest restRequest, boolean a for (SearchRequest request : requests) { // preserve if it's set on the request request.setPreFilterShardSize(Math.min(preFilterShardSize, request.getPreFilterShardSize())); + if (maxConcurrentShardRequests != null) { + request.setMaxConcurrentShardRequests(maxConcurrentShardRequests); + } } return multiRequest; } From ab000323fa7be57858c5470516877c69d00f058f Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 22 Aug 2018 09:09:08 +0100 Subject: [PATCH 101/283] Allow extension of CapturingTransport by subclasses (#33012) Today, CapturingTransport#createCapturingTransportService creates a transport service with a connection manager with reasonable default behaviours, but overriding this behaviour in a consumer is a litle tricky. Additionally, the default behaviour for opening a connection duplicates the content of the CapturingTransport#openConnection() method. This change removes this duplication by delegating to openConnection() and introduces overridable nodeConnected() and onSendRequest() methods so that consumers can alter this behaviour more easily. Relates #32246 in which we test the mechanisms for opening connections to unknown (and possibly unreachable) nodes. --- .../test/transport/CapturingTransport.java | 87 ++++++++----------- 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java index 60133a16a10a..132a07d5b7f4 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java @@ -51,7 +51,6 @@ import org.elasticsearch.transport.TransportStats; import java.io.IOException; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -66,11 +65,13 @@ import static org.apache.lucene.util.LuceneTestCase.rarely; -/** A transport class that doesn't send anything but rather captures all requests for inspection from tests */ +/** + * A transport class that doesn't send anything but rather captures all requests for inspection from tests + */ public class CapturingTransport implements Transport { private volatile Map requestHandlers = Collections.emptyMap(); - final Object requestHandlerMutex = new Object(); + private final Object requestHandlerMutex = new Object(); private final ResponseHandlers responseHandlers = new ResponseHandlers(); private TransportMessageListener listener; @@ -80,7 +81,7 @@ public static class CapturedRequest { public final String action; public final TransportRequest request; - public CapturedRequest(DiscoveryNode node, long requestId, String action, TransportRequest request) { + CapturedRequest(DiscoveryNode node, long requestId, String action, TransportRequest request) { this.node = node; this.requestId = requestId; this.action = action; @@ -96,41 +97,15 @@ public TransportService createCapturingTransportService(Settings settings, Threa @Nullable ClusterSettings clusterSettings, Set taskHeaders) { StubbableConnectionManager connectionManager = new StubbableConnectionManager(new ConnectionManager(settings, this, threadPool), settings, this, threadPool); - connectionManager.setDefaultNodeConnectedBehavior((cm, discoveryNode) -> true); - connectionManager.setDefaultConnectBehavior((cm, discoveryNode) -> new Connection() { - @Override - public DiscoveryNode getNode() { - return discoveryNode; - } - - @Override - public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) - throws TransportException { - requests.put(requestId, Tuple.tuple(discoveryNode, action)); - capturedRequests.add(new CapturedRequest(discoveryNode, requestId, action, request)); - } - - @Override - public void addCloseListener(ActionListener listener) { - - } - - @Override - public boolean isClosed() { - return false; - } - - @Override - public void close() { - - } - }); + connectionManager.setDefaultNodeConnectedBehavior((cm, discoveryNode) -> nodeConnected(discoveryNode)); + connectionManager.setDefaultConnectBehavior((cm, discoveryNode) -> openConnection(discoveryNode, null)); return new TransportService(settings, this, threadPool, interceptor, localNodeFactory, clusterSettings, taskHeaders, connectionManager); - } - /** returns all requests captured so far. Doesn't clear the captured request list. See {@link #clear()} */ + /** + * returns all requests captured so far. Doesn't clear the captured request list. See {@link #clear()} + */ public CapturedRequest[] capturedRequests() { return capturedRequests.toArray(new CapturedRequest[0]); } @@ -178,12 +153,16 @@ public Map> getCapturedRequestsByTargetNodeAndClea return groupRequestsByTargetNode(requests); } - /** clears captured requests */ + /** + * clears captured requests + */ public void clear() { capturedRequests.clear(); } - /** simulate a response for the given requestId */ + /** + * simulate a response for the given requestId + */ public void handleResponse(final long requestId, final TransportResponse response) { responseHandlers.onResponseReceived(requestId, listener).handleResponse(response); } @@ -194,7 +173,7 @@ public void handleResponse(final long requestId, final TransportResponse respons * * @param requestId the id corresponding to the captured send * request - * @param t the failure to wrap + * @param t the failure to wrap */ public void handleLocalError(final long requestId, final Throwable t) { Tuple request = requests.get(requestId); @@ -208,7 +187,7 @@ public void handleLocalError(final long requestId, final Throwable t) { * * @param requestId the id corresponding to the captured send * request - * @param t the failure to wrap + * @param t the failure to wrap */ public void handleRemoteError(final long requestId, final Throwable t) { final RemoteTransportException remoteException; @@ -234,7 +213,7 @@ public void handleRemoteError(final long requestId, final Throwable t) { * * @param requestId the id corresponding to the captured send * request - * @param e the failure + * @param e the failure */ public void handleError(final long requestId, final TransportException e) { responseHandlers.onResponseReceived(requestId, listener).handleException(e); @@ -251,13 +230,11 @@ public DiscoveryNode getNode() { @Override public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) throws TransportException { - requests.put(requestId, Tuple.tuple(node, action)); - capturedRequests.add(new CapturedRequest(node, requestId, action, request)); + onSendRequest(requestId, action, request, node); } @Override public void addCloseListener(ActionListener listener) { - } @Override @@ -267,11 +244,19 @@ public boolean isClosed() { @Override public void close() { - } }; } + protected void onSendRequest(long requestId, String action, TransportRequest request, DiscoveryNode node) { + requests.put(requestId, Tuple.tuple(node, action)); + capturedRequests.add(new CapturedRequest(node, requestId, action, request)); + } + + protected boolean nodeConnected(DiscoveryNode discoveryNode) { + return true; + } + @Override public TransportStats getStats() { throw new UnsupportedOperationException(); @@ -288,7 +273,7 @@ public Map profileBoundAddresses() { } @Override - public TransportAddress[] addressesFromString(String address, int perAddressLimit) throws UnknownHostException { + public TransportAddress[] addressesFromString(String address, int perAddressLimit) { return new TransportAddress[0]; } @@ -299,22 +284,23 @@ public Lifecycle.State lifecycleState() { @Override public void addLifecycleListener(LifecycleListener listener) { - } @Override public void removeLifecycleListener(LifecycleListener listener) { - } @Override - public void start() {} + public void start() { + } @Override - public void stop() {} + public void stop() { + } @Override - public void close() {} + public void close() { + } @Override public List getLocalAddresses() { @@ -330,6 +316,7 @@ public void registerRequestHandler(RequestHan requestHandlers = MapBuilder.newMapBuilder(requestHandlers).put(reg.getAction(), reg).immutableMap(); } } + @Override public ResponseHandlers getResponseHandlers() { return responseHandlers; From 43f6f435f5b28e7a65c24d85fec0690c1d2e58b5 Mon Sep 17 00:00:00 2001 From: Sergey Date: Wed, 22 Aug 2018 12:27:37 +0300 Subject: [PATCH 102/283] [DOCS] Update remote-info.asciidoc (#32978) --- docs/reference/cluster/remote-info.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cluster/remote-info.asciidoc b/docs/reference/cluster/remote-info.asciidoc index 3dfcc201e7ac..2866d798b281 100644 --- a/docs/reference/cluster/remote-info.asciidoc +++ b/docs/reference/cluster/remote-info.asciidoc @@ -25,7 +25,7 @@ the configured remote cluster alias. `num_nodes_connected`:: The number of connected nodes in the remote cluster. -`max_connection_per_cluster`:: +`max_connections_per_cluster`:: The maximum number of connections maintained for the remote cluster. `initial_connect_timeout`:: From b02150a5ed8bd4f2dc393bfd3b6de0518149c468 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Wed, 22 Aug 2018 06:54:08 -0500 Subject: [PATCH 103/283] HLRC: close job refactor (#33031) * HLRC: close job refactor * Changing refactor to make job_id a string * Changing set entity methodology --- .../client/MLRequestConverters.java | 17 +++--------- .../client/MLRequestConvertersTests.java | 21 ++++++++------- .../protocol/xpack/ml/CloseJobRequest.java | 26 ++++++++++++------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index 9a4e825be7c6..1c2b6f79eaf6 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 @@ -78,11 +78,11 @@ static Request openJob(OpenJobRequest openJobRequest) throws IOException { .addPathPartAsIs("_open") .build(); Request request = new Request(HttpPost.METHOD_NAME, endpoint); - request.setJsonEntity(openJobRequest.toString()); + request.setEntity(createEntity(openJobRequest, REQUEST_BODY_CONTENT_TYPE)); return request; } - static Request closeJob(CloseJobRequest closeJobRequest) { + static Request closeJob(CloseJobRequest closeJobRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") .addPathPartAsIs("ml") @@ -91,18 +91,7 @@ static Request closeJob(CloseJobRequest closeJobRequest) { .addPathPartAsIs("_close") .build(); Request request = new Request(HttpPost.METHOD_NAME, endpoint); - - RequestConverters.Params params = new RequestConverters.Params(request); - if (closeJobRequest.isForce() != null) { - params.putParam("force", Boolean.toString(closeJobRequest.isForce())); - } - if (closeJobRequest.isAllowNoJobs() != null) { - params.putParam("allow_no_jobs", Boolean.toString(closeJobRequest.isAllowNoJobs())); - } - if (closeJobRequest.getTimeout() != null) { - params.putParam("timeout", closeJobRequest.getTimeout().getStringRep()); - } - + request.setEntity(createEntity(closeJobRequest, REQUEST_BODY_CONTENT_TYPE)); return request; } 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 9ed09d06b72f..0d95c3596585 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 @@ -81,21 +81,17 @@ public void testOpenJob() throws Exception { Request request = MLRequestConverters.openJob(openJobRequest); assertEquals(HttpPost.METHOD_NAME, request.getMethod()); assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_open", request.getEndpoint()); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - request.getEntity().writeTo(bos); - assertEquals(bos.toString("UTF-8"), "{\"job_id\":\""+ jobId +"\",\"timeout\":\"10m\"}"); + assertEquals(requestEntityToString(request), "{\"job_id\":\""+ jobId +"\",\"timeout\":\"10m\"}"); } - public void testCloseJob() { + public void testCloseJob() throws Exception { String jobId = "somejobid"; CloseJobRequest closeJobRequest = new CloseJobRequest(jobId); Request request = MLRequestConverters.closeJob(closeJobRequest); assertEquals(HttpPost.METHOD_NAME, request.getMethod()); assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_close", request.getEndpoint()); - assertFalse(request.getParameters().containsKey("force")); - assertFalse(request.getParameters().containsKey("allow_no_jobs")); - assertFalse(request.getParameters().containsKey("timeout")); + assertEquals("{\"job_id\":\"somejobid\"}", requestEntityToString(request)); closeJobRequest = new CloseJobRequest(jobId, "otherjobs*"); closeJobRequest.setForce(true); @@ -104,9 +100,8 @@ public void testCloseJob() { request = MLRequestConverters.closeJob(closeJobRequest); assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + ",otherjobs*/_close", request.getEndpoint()); - assertEquals(Boolean.toString(true), request.getParameters().get("force")); - assertEquals(Boolean.toString(false), request.getParameters().get("allow_no_jobs")); - assertEquals("10m", request.getParameters().get("timeout")); + assertEquals("{\"job_id\":\"somejobid,otherjobs*\",\"timeout\":\"10m\",\"force\":true,\"allow_no_jobs\":false}", + requestEntityToString(request)); } public void testDeleteJob() { @@ -130,4 +125,10 @@ private static Job createValidJob(String jobId) { jobBuilder.setAnalysisConfig(analysisConfig); return jobBuilder.build(); } + + private static String requestEntityToString(Request request) throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + request.getEntity().writeTo(bos); + return bos.toString("UTF-8"); + } } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java index 1df5a02889e2..3d54bfb9488a 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java @@ -21,8 +21,10 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; 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.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -35,7 +37,7 @@ public class CloseJobRequest extends ActionRequest implements ToXContentObject { - public static final ParseField JOB_IDS = new ParseField("job_ids"); + public static final ParseField JOB_ID = new ParseField("job_id"); public static final ParseField TIMEOUT = new ParseField("timeout"); public static final ParseField FORCE = new ParseField("force"); public static final ParseField ALLOW_NO_JOBS = new ParseField("allow_no_jobs"); @@ -46,7 +48,9 @@ public class CloseJobRequest extends ActionRequest implements ToXContentObject { true, a -> new CloseJobRequest((List) a[0])); static { - PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), JOB_IDS); + PARSER.declareField(ConstructingObjectParser.constructorArg(), + p -> Arrays.asList(Strings.commaDelimitedListToStringArray(p.text())), + JOB_ID, ObjectParser.ValueType.STRING_ARRAY); PARSER.declareString((obj, val) -> obj.setTimeout(TimeValue.parseTimeValue(val, TIMEOUT.getPreferredName())), TIMEOUT); PARSER.declareBoolean(CloseJobRequest::setForce, FORCE); PARSER.declareBoolean(CloseJobRequest::setAllowNoJobs, ALLOW_NO_JOBS); @@ -132,7 +136,7 @@ public void setForce(boolean force) { * This includes `_all` string or when no jobs have been specified */ public Boolean isAllowNoJobs() { - return allowNoJobs; + return this.allowNoJobs; } /** @@ -149,7 +153,7 @@ public ActionRequestValidationException validate() { @Override public int hashCode() { - return Objects.hash(jobIds, timeout, allowNoJobs, force); + return Objects.hash(jobIds, timeout, force, allowNoJobs); } @Override @@ -165,16 +169,14 @@ public boolean equals(Object other) { CloseJobRequest that = (CloseJobRequest) other; return Objects.equals(jobIds, that.jobIds) && Objects.equals(timeout, that.timeout) && - Objects.equals(allowNoJobs, that.allowNoJobs) && - Objects.equals(force, that.force); + Objects.equals(force, that.force) && + Objects.equals(allowNoJobs, that.allowNoJobs); } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - - builder.field(JOB_IDS.getPreferredName(), jobIds); - + builder.field(JOB_ID.getPreferredName(), Strings.collectionToCommaDelimitedString(jobIds)); if (timeout != null) { builder.field(TIMEOUT.getPreferredName(), timeout.getStringRep()); } @@ -184,8 +186,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (allowNoJobs != null) { builder.field(ALLOW_NO_JOBS.getPreferredName(), allowNoJobs); } - builder.endObject(); return builder; } + + @Override + public String toString() { + return Strings.toString(this); + } } From 262d3c0783d771ea6b196ae2d9e38f44ec33d89c Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Wed, 22 Aug 2018 07:57:44 -0400 Subject: [PATCH 104/283] Allow engine to recover from translog upto a seqno (#33032) This change allows an engine to recover from its local translog up to the given seqno. The extended API can be used in these use cases: When a replica starts following a new primary, it resets its index to the safe commit, then replays its local translog up to the current global checkpoint (see #32867). When a replica starts a peer-recovery, it can initialize the start_sequence_number to the persisted global checkpoint instead of the local checkpoint of the safe commit. A replica will then replay its local translog up to that global checkpoint before accepting remote translog from the primary. This change will increase the chance of operation-based recovery. I will make this in a follow-up. Relates #32867 --- .../elasticsearch/index/engine/Engine.java | 6 +- .../index/engine/InternalEngine.java | 11 +-- .../elasticsearch/index/shard/IndexShard.java | 2 +- .../index/translog/Translog.java | 70 +++++++++++++-- .../index/engine/InternalEngineTests.java | 79 ++++++++++++----- .../index/shard/RefreshListenersTests.java | 2 +- .../index/translog/TranslogTests.java | 86 ++++++++++++++++++- .../index/engine/EngineTestCase.java | 2 +- 8 files changed, 217 insertions(+), 41 deletions(-) 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 31da7afc51a1..4d95cf89ef00 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -1623,10 +1623,12 @@ public interface Warmer { public abstract int fillSeqNoGaps(long primaryTerm) throws IOException; /** - * Performs recovery from the transaction log. + * Performs recovery from the transaction log up to {@code recoverUpToSeqNo} (inclusive). * This operation will close the engine if the recovery fails. + * + * @param recoverUpToSeqNo the upper bound, inclusive, of sequence number to be recovered */ - public abstract Engine recoverFromTranslog() throws IOException; + public abstract Engine recoverFromTranslog(long recoverUpToSeqNo) throws IOException; /** * Do not replay translog operations, but make the engine be ready. 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 982b220f25b2..c4c6792bf46a 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -364,7 +364,7 @@ private void bootstrapAppendOnlyInfoFromWriter(IndexWriter writer) { } @Override - public InternalEngine recoverFromTranslog() throws IOException { + public InternalEngine recoverFromTranslog(long recoverUpToSeqNo) throws IOException { flushLock.lock(); try (ReleasableLock lock = readLock.acquire()) { ensureOpen(); @@ -372,7 +372,7 @@ public InternalEngine recoverFromTranslog() throws IOException { throw new IllegalStateException("Engine has already been recovered"); } try { - recoverFromTranslogInternal(); + recoverFromTranslogInternal(recoverUpToSeqNo); } catch (Exception e) { try { pendingTranslogRecovery.set(true); // just play safe and never allow commits on this see #ensureCanFlush @@ -394,11 +394,12 @@ public void skipTranslogRecovery() { pendingTranslogRecovery.set(false); // we are good - now we can commit } - private void recoverFromTranslogInternal() throws IOException { + private void recoverFromTranslogInternal(long recoverUpToSeqNo) throws IOException { Translog.TranslogGeneration translogGeneration = translog.getGeneration(); final int opsRecovered; - final long translogGen = Long.parseLong(lastCommittedSegmentInfos.getUserData().get(Translog.TRANSLOG_GENERATION_KEY)); - try (Translog.Snapshot snapshot = translog.newSnapshotFromGen(translogGen)) { + final long translogFileGen = Long.parseLong(lastCommittedSegmentInfos.getUserData().get(Translog.TRANSLOG_GENERATION_KEY)); + try (Translog.Snapshot snapshot = translog.newSnapshotFromGen( + new Translog.TranslogGeneration(translog.getTranslogUUID(), translogFileGen), recoverUpToSeqNo)) { opsRecovered = config().getTranslogRecoveryRunner().run(this, snapshot); } catch (Exception e) { throw new EngineException(shardId, "failed to recover from translog", e); 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 ffce0e6ea8be..e030c95b56e3 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -1305,7 +1305,7 @@ int runTranslogRecovery(Engine engine, Translog.Snapshot snapshot) throws IOExce **/ public void openEngineAndRecoverFromTranslog() throws IOException { innerOpenEngineAndTranslog(); - getEngine().recoverFromTranslog(); + getEngine().recoverFromTranslog(Long.MAX_VALUE); } /** diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index 72c6210535f9..618aa546e425 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -577,21 +577,27 @@ public long getLastSyncedGlobalCheckpoint() { */ public Snapshot newSnapshot() throws IOException { try (ReleasableLock ignored = readLock.acquire()) { - return newSnapshotFromGen(getMinFileGeneration()); + return newSnapshotFromGen(new TranslogGeneration(translogUUID, getMinFileGeneration()), Long.MAX_VALUE); } } - public Snapshot newSnapshotFromGen(long minGeneration) throws IOException { + public Snapshot newSnapshotFromGen(TranslogGeneration fromGeneration, long upToSeqNo) throws IOException { try (ReleasableLock ignored = readLock.acquire()) { ensureOpen(); - if (minGeneration < getMinFileGeneration()) { - throw new IllegalArgumentException("requested snapshot generation [" + minGeneration + "] is not available. " + + final long fromFileGen = fromGeneration.translogFileGeneration; + if (fromFileGen < getMinFileGeneration()) { + throw new IllegalArgumentException("requested snapshot generation [" + fromFileGen + "] is not available. " + "Min referenced generation is [" + getMinFileGeneration() + "]"); } TranslogSnapshot[] snapshots = Stream.concat(readers.stream(), Stream.of(current)) - .filter(reader -> reader.getGeneration() >= minGeneration) + .filter(reader -> reader.getGeneration() >= fromFileGen && reader.getCheckpoint().minSeqNo <= upToSeqNo) .map(BaseTranslogReader::newSnapshot).toArray(TranslogSnapshot[]::new); - return newMultiSnapshot(snapshots); + final Snapshot snapshot = newMultiSnapshot(snapshots); + if (upToSeqNo == Long.MAX_VALUE) { + return snapshot; + } else { + return new SeqNoFilterSnapshot(snapshot, Long.MIN_VALUE, upToSeqNo); + } } } @@ -926,7 +932,59 @@ default int overriddenOperations() { * Returns the next operation in the snapshot or null if we reached the end. */ Translog.Operation next() throws IOException; + } + + /** + * A filtered snapshot consisting of only operations whose sequence numbers are in the given range + * between {@code fromSeqNo} (inclusive) and {@code toSeqNo} (inclusive). This filtered snapshot + * shares the same underlying resources with the {@code delegate} snapshot, therefore we should not + * use the {@code delegate} after passing it to this filtered snapshot. + */ + static final class SeqNoFilterSnapshot implements Snapshot { + private final Snapshot delegate; + private int filteredOpsCount; + private final long fromSeqNo; // inclusive + private final long toSeqNo; // inclusive + SeqNoFilterSnapshot(Snapshot delegate, long fromSeqNo, long toSeqNo) { + assert fromSeqNo <= toSeqNo : "from_seq_no[" + fromSeqNo + "] > to_seq_no[" + toSeqNo + "]"; + this.delegate = delegate; + this.fromSeqNo = fromSeqNo; + this.toSeqNo = toSeqNo; + } + + @Override + public int totalOperations() { + return delegate.totalOperations(); + } + + @Override + public int skippedOperations() { + return filteredOpsCount + delegate.skippedOperations(); + } + + @Override + public int overriddenOperations() { + return delegate.overriddenOperations(); + } + + @Override + public Operation next() throws IOException { + Translog.Operation op; + while ((op = delegate.next()) != null) { + if (fromSeqNo <= op.seqNo() && op.seqNo() <= toSeqNo) { + return op; + } else { + filteredOpsCount++; + } + } + return null; + } + + @Override + public void close() throws IOException { + delegate.close(); + } } /** 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 f6df22242883..76e05ba1e0b5 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -649,7 +649,7 @@ public IndexSearcher wrap(IndexSearcher searcher) throws EngineException { trimUnsafeCommits(engine.config()); engine = new InternalEngine(engine.config()); assertTrue(engine.isRecovering()); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); Engine.Searcher searcher = wrapper.wrap(engine.acquireSearcher("test")); assertThat(counter.get(), equalTo(2)); searcher.close(); @@ -666,7 +666,7 @@ public void testFlushIsDisabledDuringTranslogRecovery() throws IOException { engine = new InternalEngine(engine.config()); expectThrows(IllegalStateException.class, () -> engine.flush(true, true)); assertTrue(engine.isRecovering()); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); assertFalse(engine.isRecovering()); doc = testParsedDocument("2", null, testDocumentWithTextField(), SOURCE, null); engine.index(indexForDoc(doc)); @@ -696,7 +696,7 @@ public void testTranslogMultipleOperationsSameDocument() throws IOException { } trimUnsafeCommits(engine.config()); try (Engine recoveringEngine = new InternalEngine(engine.config())){ - recoveringEngine.recoverFromTranslog(); + recoveringEngine.recoverFromTranslog(Long.MAX_VALUE); try (Engine.Searcher searcher = recoveringEngine.acquireSearcher("test")) { final TotalHitCountCollector collector = new TotalHitCountCollector(); searcher.searcher().search(new MatchAllDocsQuery(), collector); @@ -732,7 +732,7 @@ protected void commitIndexWriter(IndexWriter writer, Translog translog, String s } }; assertThat(getTranslog(recoveringEngine).stats().getUncommittedOperations(), equalTo(docs)); - recoveringEngine.recoverFromTranslog(); + recoveringEngine.recoverFromTranslog(Long.MAX_VALUE); assertTrue(committed.get()); } finally { IOUtils.close(recoveringEngine); @@ -766,7 +766,7 @@ public void testTranslogRecoveryWithMultipleGenerations() throws IOException { initialEngine.close(); trimUnsafeCommits(initialEngine.config()); recoveringEngine = new InternalEngine(initialEngine.config()); - recoveringEngine.recoverFromTranslog(); + recoveringEngine.recoverFromTranslog(Long.MAX_VALUE); try (Engine.Searcher searcher = recoveringEngine.acquireSearcher("test")) { TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), docs); assertEquals(docs, topDocs.totalHits); @@ -776,6 +776,43 @@ public void testTranslogRecoveryWithMultipleGenerations() throws IOException { } } + public void testRecoveryFromTranslogUpToSeqNo() throws IOException { + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + try (Store store = createStore()) { + EngineConfig config = config(defaultSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get); + final long maxSeqNo; + try (InternalEngine engine = createEngine(config)) { + final int docs = randomIntBetween(1, 100); + for (int i = 0; i < docs; i++) { + final String id = Integer.toString(i); + final ParsedDocument doc = testParsedDocument(id, null, testDocumentWithTextField(), SOURCE, null); + engine.index(indexForDoc(doc)); + if (rarely()) { + engine.rollTranslogGeneration(); + } else if (rarely()) { + engine.flush(randomBoolean(), true); + } + } + maxSeqNo = engine.getLocalCheckpointTracker().getMaxSeqNo(); + globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getLocalCheckpoint())); + engine.syncTranslog(); + } + trimUnsafeCommits(config); + try (InternalEngine engine = new InternalEngine(config)) { + engine.recoverFromTranslog(Long.MAX_VALUE); + assertThat(engine.getLocalCheckpoint(), equalTo(maxSeqNo)); + assertThat(engine.getLocalCheckpointTracker().getMaxSeqNo(), equalTo(maxSeqNo)); + } + trimUnsafeCommits(config); + try (InternalEngine engine = new InternalEngine(config)) { + long upToSeqNo = randomLongBetween(globalCheckpoint.get(), maxSeqNo); + engine.recoverFromTranslog(upToSeqNo); + assertThat(engine.getLocalCheckpoint(), equalTo(upToSeqNo)); + assertThat(engine.getLocalCheckpointTracker().getMaxSeqNo(), equalTo(upToSeqNo)); + } + } + } + public void testConcurrentGetAndFlush() throws Exception { ParsedDocument doc = testParsedDocument("1", null, testDocumentWithTextField(), B_1, null); engine.index(indexForDoc(doc)); @@ -1153,7 +1190,7 @@ public void testSyncedFlushSurvivesEngineRestart() throws IOException { } trimUnsafeCommits(config); engine = new InternalEngine(config); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); assertEquals(engine.getLastCommittedSegmentInfos().getUserData().get(Engine.SYNC_COMMIT_ID), syncId); } @@ -1172,7 +1209,7 @@ public void testSyncedFlushVanishesOnReplay() throws IOException { engine.close(); trimUnsafeCommits(config); engine = new InternalEngine(config); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); assertNull("Sync ID must be gone since we have a document to replay", engine.getLastCommittedSegmentInfos().getUserData().get(Engine.SYNC_COMMIT_ID)); } @@ -2126,7 +2163,7 @@ public void testSeqNoAndCheckpoints() throws IOException { trimUnsafeCommits(initialEngine.engineConfig); try (InternalEngine recoveringEngine = new InternalEngine(initialEngine.config())){ - recoveringEngine.recoverFromTranslog(); + recoveringEngine.recoverFromTranslog(Long.MAX_VALUE); assertEquals(primarySeqNo, recoveringEngine.getSeqNoStats(-1).getMaxSeqNo()); assertThat( @@ -2447,7 +2484,7 @@ public void testCurrentTranslogIDisCommitted() throws IOException { try (InternalEngine engine = createEngine(config)) { engine.index(firstIndexRequest); globalCheckpoint.set(engine.getLocalCheckpoint()); - expectThrows(IllegalStateException.class, () -> engine.recoverFromTranslog()); + expectThrows(IllegalStateException.class, () -> engine.recoverFromTranslog(Long.MAX_VALUE)); Map userData = engine.getLastCommittedSegmentInfos().getUserData(); assertEquals("1", userData.get(Translog.TRANSLOG_GENERATION_KEY)); assertEquals(engine.getTranslog().getTranslogUUID(), userData.get(Translog.TRANSLOG_UUID_KEY)); @@ -2469,7 +2506,7 @@ public void testCurrentTranslogIDisCommitted() throws IOException { assertEquals("3", userData.get(Translog.TRANSLOG_GENERATION_KEY)); } assertEquals(engine.getTranslog().getTranslogUUID(), userData.get(Translog.TRANSLOG_UUID_KEY)); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); userData = engine.getLastCommittedSegmentInfos().getUserData(); assertEquals("3", userData.get(Translog.TRANSLOG_GENERATION_KEY)); assertEquals(engine.getTranslog().getTranslogUUID(), userData.get(Translog.TRANSLOG_UUID_KEY)); @@ -2486,7 +2523,7 @@ public void testCurrentTranslogIDisCommitted() throws IOException { Map userData = engine.getLastCommittedSegmentInfos().getUserData(); assertEquals("1", userData.get(Translog.TRANSLOG_GENERATION_KEY)); assertEquals(engine.getTranslog().getTranslogUUID(), userData.get(Translog.TRANSLOG_UUID_KEY)); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); assertEquals(2, engine.getTranslog().currentFileGeneration()); assertEquals(0L, engine.getTranslog().stats().getUncommittedOperations()); } @@ -2500,7 +2537,7 @@ public void testCurrentTranslogIDisCommitted() throws IOException { Map userData = engine.getLastCommittedSegmentInfos().getUserData(); assertEquals("1", userData.get(Translog.TRANSLOG_GENERATION_KEY)); assertEquals(engine.getTranslog().getTranslogUUID(), userData.get(Translog.TRANSLOG_UUID_KEY)); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); userData = engine.getLastCommittedSegmentInfos().getUserData(); assertEquals("no changes - nothing to commit", "1", userData.get(Translog.TRANSLOG_GENERATION_KEY)); assertEquals(engine.getTranslog().getTranslogUUID(), userData.get(Translog.TRANSLOG_UUID_KEY)); @@ -2606,7 +2643,7 @@ protected void commitIndexWriter(IndexWriter writer, Translog translog, String s } } }) { - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); final ParsedDocument doc1 = testParsedDocument("1", null, testDocumentWithTextField(), SOURCE, null); engine.index(indexForDoc(doc1)); globalCheckpoint.set(engine.getLocalCheckpoint()); @@ -2617,7 +2654,7 @@ protected void commitIndexWriter(IndexWriter writer, Translog translog, String s try (InternalEngine engine = new InternalEngine(config(indexSettings, store, translogPath, newMergePolicy(), null, null, globalCheckpointSupplier))) { - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); assertVisibleCount(engine, 1); final long committedGen = Long.valueOf( engine.getLastCommittedSegmentInfos().getUserData().get(Translog.TRANSLOG_GENERATION_KEY)); @@ -2683,7 +2720,7 @@ public void testTranslogReplay() throws IOException { engine.close(); trimUnsafeCommits(copy(engine.config(), inSyncGlobalCheckpointSupplier)); engine = new InternalEngine(copy(engine.config(), inSyncGlobalCheckpointSupplier)); // we need to reuse the engine config unless the parser.mappingModified won't work - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); assertVisibleCount(engine, numDocs, false); parser = (TranslogHandler) engine.config().getTranslogRecoveryRunner(); @@ -3384,7 +3421,7 @@ public void testEngineMaxTimestampIsInitialized() throws IOException { } try (Store store = createStore(newFSDirectory(storeDir)); Engine engine = new InternalEngine(configSupplier.apply(store))) { assertEquals(IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, engine.segmentsStats(false).getMaxUnsafeAutoIdTimestamp()); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); assertEquals(timestamp1, engine.segmentsStats(false).getMaxUnsafeAutoIdTimestamp()); final ParsedDocument doc = testParsedDocument("1", null, testDocumentWithTextField(), new BytesArray("{}".getBytes(Charset.defaultCharset())), null); @@ -3667,7 +3704,7 @@ public void testSequenceNumberAdvancesToMaxSeqOnEngineOpenOnPrimary() throws Bro } trimUnsafeCommits(initialEngine.config()); try (Engine recoveringEngine = new InternalEngine(initialEngine.config())) { - recoveringEngine.recoverFromTranslog(); + recoveringEngine.recoverFromTranslog(Long.MAX_VALUE); recoveringEngine.fillSeqNoGaps(2); assertThat(recoveringEngine.getLocalCheckpoint(), greaterThanOrEqualTo((long) (docs - 1))); } @@ -3774,7 +3811,7 @@ protected long doGenerateSeqNoForOperation(Operation operation) { throw new UnsupportedOperationException(); } }; - noOpEngine.recoverFromTranslog(); + noOpEngine.recoverFromTranslog(Long.MAX_VALUE); final int gapsFilled = noOpEngine.fillSeqNoGaps(primaryTerm.get()); final String reason = randomAlphaOfLength(16); noOpEngine.noOp(new Engine.NoOp(maxSeqNo + 1, primaryTerm.get(), LOCAL_TRANSLOG_RECOVERY, System.nanoTime(), reason)); @@ -3986,7 +4023,7 @@ public void testFillUpSequenceIdGapsOnRecovery() throws IOException { trimUnsafeCommits(copy(replicaEngine.config(), globalCheckpoint::get)); recoveringEngine = new InternalEngine(copy(replicaEngine.config(), globalCheckpoint::get)); assertEquals(numDocsOnReplica, getTranslog(recoveringEngine).stats().getUncommittedOperations()); - recoveringEngine.recoverFromTranslog(); + recoveringEngine.recoverFromTranslog(Long.MAX_VALUE); assertEquals(maxSeqIDOnReplica, recoveringEngine.getSeqNoStats(-1).getMaxSeqNo()); assertEquals(checkpointOnReplica, recoveringEngine.getLocalCheckpoint()); assertEquals((maxSeqIDOnReplica + 1) - numDocsOnReplica, recoveringEngine.fillSeqNoGaps(2)); @@ -4022,7 +4059,7 @@ public void testFillUpSequenceIdGapsOnRecovery() throws IOException { if (flushed) { assertThat(recoveringEngine.getTranslogStats().getUncommittedOperations(), equalTo(0)); } - recoveringEngine.recoverFromTranslog(); + recoveringEngine.recoverFromTranslog(Long.MAX_VALUE); assertEquals(maxSeqIDOnReplica, recoveringEngine.getSeqNoStats(-1).getMaxSeqNo()); assertEquals(maxSeqIDOnReplica, recoveringEngine.getLocalCheckpoint()); assertEquals(0, recoveringEngine.fillSeqNoGaps(3)); @@ -4215,7 +4252,7 @@ protected void commitIndexWriter(IndexWriter writer, Translog translog, String s super.commitIndexWriter(writer, translog, syncId); } }) { - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); int numDocs = scaledRandomIntBetween(10, 100); for (int docId = 0; docId < numDocs; docId++) { ParseContext.Document document = testDocumentWithTextField(); diff --git a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java index 2d1c1d4e15af..774b272121a5 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java @@ -132,7 +132,7 @@ indexSettings, null, store, newMergePolicy(), iwc.getAnalyzer(), iwc.getSimilari TimeValue.timeValueMinutes(5), Collections.singletonList(listeners), Collections.emptyList(), null, (e, s) -> 0, new NoneCircuitBreakerService(), () -> SequenceNumbers.NO_OPS_PERFORMED, () -> primaryTerm); engine = new InternalEngine(config); - engine.recoverFromTranslog(); + engine.recoverFromTranslog(Long.MAX_VALUE); listeners.setCurrentRefreshLocationSupplier(engine::getTranslogLastWriteLocation); } diff --git a/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java b/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java index 4ec479334ba6..a0e0c481e5f8 100644 --- a/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java +++ b/server/src/test/java/org/elasticsearch/index/translog/TranslogTests.java @@ -360,7 +360,8 @@ public void testSimpleOperations() throws IOException { } markCurrentGenAsCommitted(translog); - try (Translog.Snapshot snapshot = translog.newSnapshotFromGen(firstId + 1)) { + try (Translog.Snapshot snapshot = translog.newSnapshotFromGen( + new Translog.TranslogGeneration(translog.getTranslogUUID(), firstId + 1), randomNonNegativeLong())) { assertThat(snapshot, SnapshotMatchers.size(0)); assertThat(snapshot.totalOperations(), equalTo(0)); } @@ -645,6 +646,82 @@ public void testSnapshotOnClosedTranslog() throws IOException { } } + public void testSnapshotFromMinGen() throws Exception { + Map> operationsByGen = new HashMap<>(); + try (Translog.Snapshot snapshot = translog.newSnapshotFromGen( + new Translog.TranslogGeneration(translog.getTranslogUUID(), 1), randomNonNegativeLong())) { + assertThat(snapshot, SnapshotMatchers.size(0)); + } + int iters = between(1, 10); + for (int i = 0; i < iters; i++) { + long currentGeneration = translog.currentFileGeneration(); + operationsByGen.putIfAbsent(currentGeneration, new ArrayList<>()); + int numOps = between(0, 20); + for (int op = 0; op < numOps; op++) { + long seqNo = randomLongBetween(0, 1000); + addToTranslogAndList(translog, operationsByGen.get(currentGeneration), new Translog.Index("test", + Long.toString(seqNo), seqNo, primaryTerm.get(), new byte[]{1})); + } + long minGen = randomLongBetween(translog.getMinFileGeneration(), translog.currentFileGeneration()); + try (Translog.Snapshot snapshot = translog.newSnapshotFromGen( + new Translog.TranslogGeneration(translog.getTranslogUUID(), minGen), Long.MAX_VALUE)) { + List expectedOps = operationsByGen.entrySet().stream() + .filter(e -> e.getKey() >= minGen) + .flatMap(e -> e.getValue().stream()) + .collect(Collectors.toList()); + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedOps)); + } + long upToSeqNo = randomLongBetween(0, 2000); + try (Translog.Snapshot snapshot = translog.newSnapshotFromGen( + new Translog.TranslogGeneration(translog.getTranslogUUID(), minGen), upToSeqNo)) { + List expectedOps = operationsByGen.entrySet().stream() + .filter(e -> e.getKey() >= minGen) + .flatMap(e -> e.getValue().stream().filter(op -> op.seqNo() <= upToSeqNo)) + .collect(Collectors.toList()); + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedOps)); + } + translog.rollGeneration(); + } + } + + public void testSeqNoFilterSnapshot() throws Exception { + final int generations = between(2, 20); + for (int gen = 0; gen < generations; gen++) { + List batch = LongStream.rangeClosed(0, between(0, 100)).boxed().collect(Collectors.toList()); + Randomness.shuffle(batch); + for (long seqNo : batch) { + Translog.Index op = new Translog.Index("doc", randomAlphaOfLength(10), seqNo, primaryTerm.get(), new byte[]{1}); + translog.add(op); + } + translog.rollGeneration(); + } + List operations = new ArrayList<>(); + try (Translog.Snapshot snapshot = translog.newSnapshot()) { + Translog.Operation op; + while ((op = snapshot.next()) != null) { + operations.add(op); + } + } + try (Translog.Snapshot snapshot = translog.newSnapshot()) { + Translog.Snapshot filter = new Translog.SeqNoFilterSnapshot(snapshot, between(200, 300), between(300, 400)); // out range + assertThat(filter, SnapshotMatchers.size(0)); + assertThat(filter.totalOperations(), equalTo(snapshot.totalOperations())); + assertThat(filter.overriddenOperations(), equalTo(snapshot.overriddenOperations())); + assertThat(filter.skippedOperations(), equalTo(snapshot.totalOperations())); + } + try (Translog.Snapshot snapshot = translog.newSnapshot()) { + int fromSeqNo = between(-2, 500); + int toSeqNo = between(fromSeqNo, 500); + List selectedOps = operations.stream() + .filter(op -> fromSeqNo <= op.seqNo() && op.seqNo() <= toSeqNo).collect(Collectors.toList()); + Translog.Snapshot filter = new Translog.SeqNoFilterSnapshot(snapshot, fromSeqNo, toSeqNo); + assertThat(filter, SnapshotMatchers.containsOperationsInAnyOrder(selectedOps)); + assertThat(filter.totalOperations(), equalTo(snapshot.totalOperations())); + assertThat(filter.overriddenOperations(), equalTo(snapshot.overriddenOperations())); + assertThat(filter.skippedOperations(), equalTo(snapshot.skippedOperations() + operations.size() - selectedOps.size())); + } + } + public void assertFileIsPresent(Translog translog, long id) { if (Files.exists(translog.location().resolve(Translog.getFilename(id)))) { return; @@ -1304,7 +1381,7 @@ public void testBasicRecovery() throws IOException { translog = new Translog(config, translogGeneration.translogUUID, translog.getDeletionPolicy(), () -> SequenceNumbers.NO_OPS_PERFORMED, primaryTerm::get); assertEquals("lastCommitted must be 1 less than current", translogGeneration.translogFileGeneration + 1, translog.currentFileGeneration()); assertFalse(translog.syncNeeded()); - try (Translog.Snapshot snapshot = translog.newSnapshotFromGen(translogGeneration.translogFileGeneration)) { + try (Translog.Snapshot snapshot = translog.newSnapshotFromGen(translogGeneration, Long.MAX_VALUE)) { for (int i = minUncommittedOp; i < translogOperations; i++) { assertEquals("expected operation" + i + " to be in the previous translog but wasn't", translog.currentFileGeneration() - 1, locations.get(i).generation); @@ -1735,7 +1812,7 @@ public void testOpenForeignTranslog() throws IOException { } this.translog = new Translog(config, translogUUID, deletionPolicy, () -> SequenceNumbers.NO_OPS_PERFORMED, primaryTerm::get); - try (Translog.Snapshot snapshot = this.translog.newSnapshotFromGen(translogGeneration.translogFileGeneration)) { + try (Translog.Snapshot snapshot = this.translog.newSnapshotFromGen(translogGeneration, Long.MAX_VALUE)) { for (int i = firstUncommitted; i < translogOperations; i++) { Translog.Operation next = snapshot.next(); assertNotNull("" + i, next); @@ -2557,7 +2634,8 @@ public void testWithRandomException() throws IOException { generationUUID = Translog.createEmptyTranslog(config.getTranslogPath(), SequenceNumbers.NO_OPS_PERFORMED, shardId, primaryTerm.get()); } try (Translog translog = new Translog(config, generationUUID, deletionPolicy, () -> SequenceNumbers.NO_OPS_PERFORMED, primaryTerm::get); - Translog.Snapshot snapshot = translog.newSnapshotFromGen(minGenForRecovery)) { + Translog.Snapshot snapshot = translog.newSnapshotFromGen( + new Translog.TranslogGeneration(generationUUID, minGenForRecovery), Long.MAX_VALUE)) { assertEquals(syncedDocs.size(), snapshot.totalOperations()); for (int i = 0; i < syncedDocs.size(); i++) { Translog.Operation next = snapshot.next(); 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 2a84a8f42461..b5ba5f18b395 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 @@ -375,7 +375,7 @@ private InternalEngine createEngine(@Nullable IndexWriterFactory indexWriterFact } InternalEngine internalEngine = createInternalEngine(indexWriterFactory, localCheckpointTrackerSupplier, seqNoForOperation, config); - internalEngine.recoverFromTranslog(); + internalEngine.recoverFromTranslog(Long.MAX_VALUE); return internalEngine; } From ead198bf2e76d907ed218b5aaddc60d53b7add72 Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Wed, 22 Aug 2018 14:13:27 +0200 Subject: [PATCH 105/283] Add settings updater for 2 affix settings (#33050) Today we can only have non-affix settings updated and consumed _together_. Yet, there are use-cases where two affix settings depend on each other which makes using the hard without consuming updates together. Unfortunately, there is not straight forward way to have N settings updated together in a type-safe way having 2 still serves a large portion of use-cases. --- .../settings/AbstractScopedSettings.java | 71 +++++++++++++- .../common/settings/Setting.java | 2 +- .../common/settings/ScopedSettingsTests.java | 96 ++++++++++++++++++- .../common/settings/SettingTests.java | 6 +- 4 files changed, 168 insertions(+), 7 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 8847c8138a70..9f1d7d8a3953 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java @@ -199,7 +199,7 @@ public synchronized void addSettingsUpdateConsumer(Setting setting, Consu * Also automatically adds empty consumers for all settings in order to activate logging */ public synchronized void addSettingsUpdateConsumer(Consumer consumer, List> settings) { - addSettingsUpdater(Setting.groupedSettingsUpdater(consumer, logger, settings)); + addSettingsUpdater(Setting.groupedSettingsUpdater(consumer, settings)); } /** @@ -208,11 +208,78 @@ public synchronized void addSettingsUpdateConsumer(Consumer consumer, */ public synchronized void addAffixUpdateConsumer(Setting.AffixSetting setting, BiConsumer consumer, BiConsumer validator) { + ensureSettingIsRegistered(setting); + addSettingsUpdater(setting.newAffixUpdater(consumer, logger, validator)); + } + + /** + * Adds a affix settings consumer that accepts the values for two settings. The consumer is only notified if one or both settings change + * and if the provided validator succeeded. + *

+ * Note: Only settings registered in {@link SettingsModule} can be changed dynamically. + *

+ * This method registers a compound updater that is useful if two settings are depending on each other. + * The consumer is always provided with both values even if only one of the two changes. + */ + public synchronized void addAffixUpdateConsumer(Setting.AffixSetting settingA, Setting.AffixSetting settingB, + BiConsumer> consumer, + BiConsumer> validator) { + // it would be awesome to have a generic way to do that ie. a set of settings that map to an object with a builder + // down the road this would be nice to have! + ensureSettingIsRegistered(settingA); + ensureSettingIsRegistered(settingB); + SettingUpdater, A>> affixUpdaterA = settingA.newAffixUpdater((a,b)-> {}, logger, (a,b)-> {}); + SettingUpdater, B>> affixUpdaterB = settingB.newAffixUpdater((a,b)-> {}, logger, (a,b)-> {}); + + addSettingsUpdater(new SettingUpdater>>() { + + @Override + public boolean hasChanged(Settings current, Settings previous) { + return affixUpdaterA.hasChanged(current, previous) || affixUpdaterB.hasChanged(current, previous); + } + + @Override + public Map> getValue(Settings current, Settings previous) { + Map> map = new HashMap<>(); + BiConsumer aConsumer = (key, value) -> { + assert map.containsKey(key) == false : "duplicate key: " + key; + map.put(key, new Tuple<>(value, settingB.getConcreteSettingForNamespace(key).get(current))); + }; + BiConsumer bConsumer = (key, value) -> { + Tuple abTuple = map.get(key); + if (abTuple != null) { + map.put(key, new Tuple<>(abTuple.v1(), value)); + } else { + assert settingA.getConcreteSettingForNamespace(key).get(current).equals(settingA.getConcreteSettingForNamespace + (key).get(previous)) : "expected: " + settingA.getConcreteSettingForNamespace(key).get(current) + + " but was " + settingA.getConcreteSettingForNamespace(key).get(previous); + map.put(key, new Tuple<>(settingA.getConcreteSettingForNamespace(key).get(current), value)); + } + }; + SettingUpdater, A>> affixUpdaterA = settingA.newAffixUpdater(aConsumer, logger, (a,b) ->{}); + SettingUpdater, B>> affixUpdaterB = settingB.newAffixUpdater(bConsumer, logger, (a,b) ->{}); + affixUpdaterA.apply(current, previous); + affixUpdaterB.apply(current, previous); + for (Map.Entry> entry : map.entrySet()) { + validator.accept(entry.getKey(), entry.getValue()); + } + return Collections.unmodifiableMap(map); + } + + @Override + public void apply(Map> values, Settings current, Settings previous) { + for (Map.Entry> entry : values.entrySet()) { + consumer.accept(entry.getKey(), entry.getValue()); + } + } + }); + } + + private void ensureSettingIsRegistered(Setting.AffixSetting setting) { final Setting registeredSetting = this.complexMatchers.get(setting.getKey()); if (setting != registeredSetting) { throw new IllegalArgumentException("Setting is not registered for key [" + setting.getKey() + "]"); } - addSettingsUpdater(setting.newAffixUpdater(consumer, logger, validator)); } /** 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 94edb5a297af..b98c2753d701 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -547,7 +547,7 @@ public String toString() { }; } - static AbstractScopedSettings.SettingUpdater groupedSettingsUpdater(Consumer consumer, Logger logger, + static AbstractScopedSettings.SettingUpdater groupedSettingsUpdater(Consumer consumer, final List> configuredSettings) { return new AbstractScopedSettings.SettingUpdater() { 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 2376d5663402..1580c1a37978 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.routing.allocation.decider.FilterAllocationDecider; import org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Setting.Property; @@ -180,6 +181,99 @@ public void testDependentSettings() { service.validate(Settings.builder().put("foo.test.bar", 7).build(), false); } + public void testTupleAffixUpdateConsumer() { + String prefix = randomAlphaOfLength(3) + "foo."; + String intSuffix = randomAlphaOfLength(3); + String listSuffix = randomAlphaOfLength(4); + Setting.AffixSetting intSetting = Setting.affixKeySetting(prefix, intSuffix, + (k) -> Setting.intSetting(k, 1, Property.Dynamic, Property.NodeScope)); + Setting.AffixSetting> listSetting = Setting.affixKeySetting(prefix, listSuffix, + (k) -> Setting.listSetting(k, Arrays.asList("1"), Integer::parseInt, Property.Dynamic, Property.NodeScope)); + AbstractScopedSettings service = new ClusterSettings(Settings.EMPTY,new HashSet<>(Arrays.asList(intSetting, listSetting))); + Map, Integer>> results = new HashMap<>(); + Function listBuilder = g -> (prefix + g + "." + listSuffix); + Function intBuilder = g -> (prefix + g + "." + intSuffix); + String group1 = randomAlphaOfLength(3); + String group2 = randomAlphaOfLength(4); + String group3 = randomAlphaOfLength(5); + BiConsumer, Integer>> listConsumer = results::put; + + service.addAffixUpdateConsumer(listSetting, intSetting, listConsumer, (s, k) -> { + if (k.v1().isEmpty() && k.v2() == 2) { + throw new IllegalArgumentException("boom"); + } + }); + assertEquals(0, results.size()); + service.applySettings(Settings.builder() + .put(intBuilder.apply(group1), 2) + .put(intBuilder.apply(group2), 7) + .putList(listBuilder.apply(group1), "16", "17") + .putList(listBuilder.apply(group2), "18", "19", "20") + .build()); + assertEquals(2, results.get(group1).v2().intValue()); + assertEquals(7, results.get(group2).v2().intValue()); + assertEquals(Arrays.asList(16, 17), results.get(group1).v1()); + assertEquals(Arrays.asList(18, 19, 20), results.get(group2).v1()); + assertEquals(2, results.size()); + + results.clear(); + + service.applySettings(Settings.builder() + .put(intBuilder.apply(group1), 2) + .put(intBuilder.apply(group2), 7) + .putList(listBuilder.apply(group1), "16", "17") + .putNull(listBuilder.apply(group2)) // removed + .build()); + + assertNull(group1 + " wasn't changed", results.get(group1)); + assertEquals(1, results.get(group2).v1().size()); + assertEquals(Arrays.asList(1), results.get(group2).v1()); + assertEquals(7, results.get(group2).v2().intValue()); + assertEquals(1, results.size()); + results.clear(); + + service.applySettings(Settings.builder() + .put(intBuilder.apply(group1), 2) + .put(intBuilder.apply(group2), 7) + .putList(listBuilder.apply(group1), "16", "17") + .putList(listBuilder.apply(group3), "5", "6") // added + .build()); + assertNull(group1 + " wasn't changed", results.get(group1)); + assertNull(group2 + " wasn't changed", results.get(group2)); + + assertEquals(2, results.get(group3).v1().size()); + assertEquals(Arrays.asList(5, 6), results.get(group3).v1()); + assertEquals(1, results.get(group3).v2().intValue()); + assertEquals(1, results.size()); + results.clear(); + + service.applySettings(Settings.builder() + .put(intBuilder.apply(group1), 4) // modified + .put(intBuilder.apply(group2), 7) + .putList(listBuilder.apply(group1), "16", "17") + .putList(listBuilder.apply(group3), "5", "6") + .build()); + assertNull(group2 + " wasn't changed", results.get(group2)); + assertNull(group3 + " wasn't changed", results.get(group3)); + + assertEquals(2, results.get(group1).v1().size()); + assertEquals(Arrays.asList(16, 17), results.get(group1).v1()); + assertEquals(4, results.get(group1).v2().intValue()); + assertEquals(1, results.size()); + results.clear(); + + IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> + service.applySettings(Settings.builder() + .put(intBuilder.apply(group1), 2) // modified to trip validator + .put(intBuilder.apply(group2), 7) + .putList(listBuilder.apply(group1)) // modified to trip validator + .putList(listBuilder.apply(group3), "5", "6") + .build()) + ); + assertEquals("boom", iae.getMessage()); + assertEquals(0, results.size()); + } + public void testAddConsumerAffix() { Setting.AffixSetting intSetting = Setting.affixKeySetting("foo.", "bar", (k) -> Setting.intSetting(k, 1, Property.Dynamic, Property.NodeScope)); @@ -893,7 +987,7 @@ public void testInternalIndexSettingsFailsValidation() { public void testInternalIndexSettingsSkipValidation() { final Setting internalIndexSetting = Setting.simpleString("index.internal", Property.InternalIndex, Property.IndexScope); - final IndexScopedSettings indexScopedSettings = + final IndexScopedSettings indexScopedSettings = new IndexScopedSettings(Settings.EMPTY, Collections.singleton(internalIndexSetting)); // nothing should happen, validation should not throw an exception final Settings settings = Settings.builder().put("index.internal", "internal").build(); 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 c32037f44525..7063e53f7891 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java @@ -757,7 +757,7 @@ public void testTimeValue() { public void testSettingsGroupUpdater() { Setting intSetting = Setting.intSetting("prefix.foo", 1, Property.NodeScope, Property.Dynamic); Setting intSetting2 = Setting.intSetting("prefix.same", 1, Property.NodeScope, Property.Dynamic); - AbstractScopedSettings.SettingUpdater updater = Setting.groupedSettingsUpdater(s -> {}, logger, + AbstractScopedSettings.SettingUpdater updater = Setting.groupedSettingsUpdater(s -> {}, Arrays.asList(intSetting, intSetting2)); Settings current = Settings.builder().put("prefix.foo", 123).put("prefix.same", 5555).build(); @@ -768,7 +768,7 @@ public void testSettingsGroupUpdater() { public void testSettingsGroupUpdaterRemoval() { Setting intSetting = Setting.intSetting("prefix.foo", 1, Property.NodeScope, Property.Dynamic); Setting intSetting2 = Setting.intSetting("prefix.same", 1, Property.NodeScope, Property.Dynamic); - AbstractScopedSettings.SettingUpdater updater = Setting.groupedSettingsUpdater(s -> {}, logger, + AbstractScopedSettings.SettingUpdater updater = Setting.groupedSettingsUpdater(s -> {}, Arrays.asList(intSetting, intSetting2)); Settings current = Settings.builder().put("prefix.same", 5555).build(); @@ -783,7 +783,7 @@ public void testSettingsGroupUpdaterWithAffixSetting() { Setting.AffixSetting affixSetting = Setting.affixKeySetting("prefix.foo.", "suffix", key -> Setting.simpleString(key,Property.NodeScope, Property.Dynamic)); - AbstractScopedSettings.SettingUpdater updater = Setting.groupedSettingsUpdater(s -> {}, logger, + AbstractScopedSettings.SettingUpdater updater = Setting.groupedSettingsUpdater(s -> {}, Arrays.asList(intSetting, prefixKeySetting, affixSetting)); Settings.Builder currentSettingsBuilder = Settings.builder() From 67bfb765ee8b7958a4d7a26b17f653ae8955878d Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 22 Aug 2018 10:18:07 -0400 Subject: [PATCH 106/283] Refactor Netty4Utils#maybeDie (#33021) In our Netty layer we have had to take extra precautions against Netty catching throwables which prevents them from reaching the uncaught exception handler. This code has taken on additional uses in NIO layer and now in the scheduler engine because there are other components in stack traces that could catch throwables and suppress them from reaching the uncaught exception handler. This commit is a simple cleanup of the iterative evolution of this code to refactor all uses into a single method in ExceptionsHelper. --- .../http/netty4/Netty4HttpChannel.java | 6 +- .../http/netty4/Netty4HttpRequestHandler.java | 5 +- .../http/netty4/Netty4HttpServerChannel.java | 4 +- .../netty4/Netty4HttpServerTransport.java | 5 +- .../netty4/Netty4MessageChannelHandler.java | 2 +- .../transport/netty4/Netty4TcpChannel.java | 5 +- .../netty4/Netty4TcpServerChannel.java | 3 +- .../transport/netty4/Netty4Transport.java | 11 +-- .../transport/netty4/Netty4Utils.java | 34 -------- .../http/nio/HttpReadWriteHandler.java | 2 +- .../elasticsearch/http/nio/NettyAdaptor.java | 4 +- .../elasticsearch/http/nio/NettyListener.java | 2 +- .../qa/die_with_dignity/DieWithDignityIT.java | 12 +-- .../org/elasticsearch/ExceptionsHelper.java | 79 ++++++++++--------- .../xpack/core/scheduler/SchedulerEngine.java | 8 +- 15 files changed, 76 insertions(+), 106 deletions(-) diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpChannel.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpChannel.java index 981a417449f1..73135c2a1456 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpChannel.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpChannel.java @@ -21,11 +21,11 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelPromise; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.concurrent.CompletableContext; import org.elasticsearch.http.HttpChannel; import org.elasticsearch.http.HttpResponse; -import org.elasticsearch.transport.netty4.Netty4Utils; import java.net.InetSocketAddress; @@ -42,7 +42,7 @@ public class Netty4HttpChannel implements HttpChannel { } else { Throwable cause = f.cause(); if (cause instanceof Error) { - Netty4Utils.maybeDie(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); closeContext.completeExceptionally(new Exception(cause)); } else { closeContext.completeExceptionally((Exception) cause); @@ -59,7 +59,7 @@ public void sendResponse(HttpResponse response, ActionListener listener) { listener.onResponse(null); } else { final Throwable cause = f.cause(); - Netty4Utils.maybeDie(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); if (cause instanceof Error) { listener.onFailure(new Exception(cause)); } else { diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java index ab078ad10d33..472e34d09fc4 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpRequestHandler.java @@ -27,7 +27,6 @@ import io.netty.handler.codec.http.FullHttpRequest; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.http.HttpPipelinedRequest; -import org.elasticsearch.transport.netty4.Netty4Utils; @ChannelHandler.Sharable class Netty4HttpRequestHandler extends SimpleChannelInboundHandler> { @@ -58,7 +57,7 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpPipelinedRequest listener) listener.onResponse(null); } else { final Throwable cause = f.cause(); - Netty4Utils.maybeDie(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); if (cause instanceof Error) { listener.onFailure(new Exception(cause)); } else { diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpServerChannel.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpServerChannel.java index 873a6c33fba1..9ef3f296f060 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpServerChannel.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpServerChannel.java @@ -20,6 +20,7 @@ package org.elasticsearch.transport.netty4; import io.netty.channel.Channel; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.concurrent.CompletableContext; import org.elasticsearch.transport.TcpServerChannel; @@ -41,7 +42,7 @@ public class Netty4TcpServerChannel implements TcpServerChannel { } else { Throwable cause = f.cause(); if (cause instanceof Error) { - Netty4Utils.maybeDie(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); closeContext.completeExceptionally(new Exception(cause)); } else { closeContext.completeExceptionally((Exception) cause); diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java index 7eb34bcdcd3a..0edd12a44e8c 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java @@ -38,6 +38,7 @@ import io.netty.util.concurrent.Future; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.SuppressForbidden; @@ -228,7 +229,7 @@ protected Netty4TcpChannel initiateChannel(DiscoveryNode node, ActionListener channels) throws IOEx } } - /** - * If the specified cause is an unrecoverable error, this method will rethrow the cause on a separate thread so that it can not be - * caught and bubbles up to the uncaught exception handler. - * - * @param cause the throwable to test - */ - public static void maybeDie(final Throwable cause) { - final Logger logger = ESLoggerFactory.getLogger(Netty4Utils.class); - final Optional maybeError = ExceptionsHelper.maybeError(cause, logger); - if (maybeError.isPresent()) { - /* - * Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, Netty wraps too many - * invocations of user-code in try/catch blocks that swallow all throwables. This means that a rethrow here will not bubble up - * to where we want it to. So, we fork a thread and throw the exception from there where Netty can not get to it. We do not wrap - * the exception so as to not lose the original cause during exit. - */ - try { - // try to log the current stack trace - final String formatted = ExceptionsHelper.formatStackTrace(Thread.currentThread().getStackTrace()); - logger.error("fatal error on the network layer\n{}", formatted); - } finally { - new Thread( - () -> { - throw maybeError.get(); - }) - .start(); - } - } - } - } diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java index 3dcd59cf8e28..17a5c1fb97e8 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/HttpReadWriteHandler.java @@ -139,7 +139,7 @@ private void handleRequest(Object msg) { if (request.decoderResult().isFailure()) { Throwable cause = request.decoderResult().cause(); if (cause instanceof Error) { - ExceptionsHelper.dieOnError(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); transport.incomingRequestError(httpRequest, nioHttpChannel, new Exception(cause)); } else { transport.incomingRequestError(httpRequest, nioHttpChannel, (Exception) cause); diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyAdaptor.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyAdaptor.java index 41cb72aa3227..133206e1322d 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyAdaptor.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyAdaptor.java @@ -73,7 +73,7 @@ public void close() throws Exception { closeFuture.await(); if (closeFuture.isSuccess() == false) { Throwable cause = closeFuture.cause(); - ExceptionsHelper.dieOnError(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); throw (Exception) cause; } } @@ -84,7 +84,7 @@ public void addCloseListener(BiConsumer listener) { listener.accept(null, null); } else { final Throwable cause = f.cause(); - ExceptionsHelper.dieOnError(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); assert cause instanceof Exception; listener.accept(null, (Exception) cause); } diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyListener.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyListener.java index 2cdaa4708d15..637bbafff8ea 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyListener.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NettyListener.java @@ -223,7 +223,7 @@ public static NettyListener fromBiConsumer(BiConsumer biConsume biConsumer.accept(null, null); } else { if (cause instanceof Error) { - ExceptionsHelper.dieOnError(cause); + ExceptionsHelper.maybeDieOnAnotherThread(cause); biConsumer.accept(null, new Exception(cause)); } else { biConsumer.accept(null, (Exception) cause); diff --git a/qa/die-with-dignity/src/test/java/org/elasticsearch/qa/die_with_dignity/DieWithDignityIT.java b/qa/die-with-dignity/src/test/java/org/elasticsearch/qa/die_with_dignity/DieWithDignityIT.java index 992d3ce71f62..9250122025c0 100644 --- a/qa/die-with-dignity/src/test/java/org/elasticsearch/qa/die_with_dignity/DieWithDignityIT.java +++ b/qa/die-with-dignity/src/test/java/org/elasticsearch/qa/die_with_dignity/DieWithDignityIT.java @@ -90,14 +90,14 @@ public void testDieWithDignity() throws Exception { final Iterator it = lines.iterator(); - boolean fatalErrorOnTheNetworkLayer = false; + boolean fatalError = false; boolean fatalErrorInThreadExiting = false; - while (it.hasNext() && (fatalErrorOnTheNetworkLayer == false || fatalErrorInThreadExiting == false)) { + while (it.hasNext() && (fatalError == false || fatalErrorInThreadExiting == false)) { final String line = it.next(); - if (line.contains("fatal error on the network layer")) { - fatalErrorOnTheNetworkLayer = true; - } else if (line.matches(".*\\[ERROR\\]\\[o.e.b.ElasticsearchUncaughtExceptionHandler\\] \\[node-0\\]" + if (line.matches(".*\\[ERROR\\]\\[o\\.e\\.ExceptionsHelper\\s*\\] \\[node-0\\] fatal error")) { + fatalError = true; + } else if (line.matches(".*\\[ERROR\\]\\[o\\.e\\.b\\.ElasticsearchUncaughtExceptionHandler\\] \\[node-0\\]" + " fatal error in thread \\[Thread-\\d+\\], exiting$")) { fatalErrorInThreadExiting = true; assertTrue(it.hasNext()); @@ -105,7 +105,7 @@ public void testDieWithDignity() throws Exception { } } - assertTrue(fatalErrorOnTheNetworkLayer); + assertTrue(fatalError); assertTrue(fatalErrorInThreadExiting); } diff --git a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java index 09347f519fb2..9f7566662174 100644 --- a/server/src/main/java/org/elasticsearch/ExceptionsHelper.java +++ b/server/src/main/java/org/elasticsearch/ExceptionsHelper.java @@ -136,42 +136,6 @@ public static String formatStackTrace(final StackTraceElement[] stackTrace) { return Arrays.stream(stackTrace).skip(1).map(e -> "\tat " + e).collect(Collectors.joining("\n")); } - static final int MAX_ITERATIONS = 1024; - - /** - * Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable. - * - * @param cause the root throwable - * - * @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable - */ - public static Optional maybeError(final Throwable cause, final Logger logger) { - // early terminate if the cause is already an error - if (cause instanceof Error) { - return Optional.of((Error) cause); - } - - final Queue queue = new LinkedList<>(); - queue.add(cause); - int iterations = 0; - while (!queue.isEmpty()) { - iterations++; - if (iterations > MAX_ITERATIONS) { - logger.warn("giving up looking for fatal errors", cause); - break; - } - final Throwable current = queue.remove(); - if (current instanceof Error) { - return Optional.of((Error) current); - } - Collections.addAll(queue, current.getSuppressed()); - if (current.getCause() != null) { - queue.add(current.getCause()); - } - } - return Optional.empty(); - } - /** * Rethrows the first exception in the list and adds all remaining to the suppressed list. * If the given list is empty no exception is thrown @@ -243,13 +207,50 @@ public static boolean reThrowIfNotNull(@Nullable Throwable e) { return true; } + static final int MAX_ITERATIONS = 1024; + + /** + * Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable. + * + * @param cause the root throwable + * @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable + */ + public static Optional maybeError(final Throwable cause, final Logger logger) { + // early terminate if the cause is already an error + if (cause instanceof Error) { + return Optional.of((Error) cause); + } + + final Queue queue = new LinkedList<>(); + queue.add(cause); + int iterations = 0; + while (queue.isEmpty() == false) { + iterations++; + // this is a guard against deeply nested or circular chains of exceptions + if (iterations > MAX_ITERATIONS) { + logger.warn("giving up looking for fatal errors", cause); + break; + } + final Throwable current = queue.remove(); + if (current instanceof Error) { + return Optional.of((Error) current); + } + Collections.addAll(queue, current.getSuppressed()); + if (current.getCause() != null) { + queue.add(current.getCause()); + } + } + return Optional.empty(); + } + /** * If the specified cause is an unrecoverable error, this method will rethrow the cause on a separate thread so that it can not be - * caught and bubbles up to the uncaught exception handler. + * caught and bubbles up to the uncaught exception handler. Note that the cause tree is examined for any {@link Error}. See + * {@link #maybeError(Throwable, Logger)} for the semantics. * - * @param throwable the throwable to test + * @param throwable the throwable to possibly throw on another thread */ - public static void dieOnError(Throwable throwable) { + public static void maybeDieOnAnotherThread(final Throwable throwable) { ExceptionsHelper.maybeError(throwable, logger).ifPresent(error -> { /* * Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, sometimes the stack diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java index 3f99818f31af..30e41734906b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java @@ -194,11 +194,11 @@ public void run() { /* * Allowing the throwable to escape here will lead to be it being caught in FutureTask#run and set as the outcome of this * task; however, we never inspect the the outcomes of these scheduled tasks and so allowing the throwable to escape - * unhandled here could lead to us losing fatal errors. Instead, we rely on ExceptionsHelper#dieOnError to appropriately - * dispatch any error to the uncaught exception handler. We should never see an exception here as these do not escape from - * SchedulerEngine#notifyListeners. + * unhandled here could lead to us losing fatal errors. Instead, we rely on ExceptionsHelper#maybeThrowErrorOnAnotherThread + * to appropriately dispatch any error to the uncaught exception handler. We should never see an exception here as these do + * not escape from SchedulerEngine#notifyListeners. */ - ExceptionsHelper.dieOnError(t); + ExceptionsHelper.maybeDieOnAnotherThread(t); throw t; } scheduleNextRun(triggeredTime); From e9912081dd3507bf3ab1ceabd949ca692a5a92eb Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Wed, 22 Aug 2018 09:19:58 -0500 Subject: [PATCH 107/283] HLRC: Create server agnostic request and response (#32912) The HLRC has historically reused the same Request and Response classes that the server module uses. This commit deprecates the use of any server module Request and Response classes, and adds a small bit of validation logic that differs from server slightly, in that it does not assume a check for a null ValidationException class is not enough to determine if validation failed. --- .../client/RestHighLevelClient.java | 127 ++++++++++++++++-- .../org/elasticsearch/client/Validatable.java | 41 ++++++ .../client/ValidationException.java | 55 ++++++++ 3 files changed, 209 insertions(+), 14 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ValidationException.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 2b71b5be59d2..7376f74839ce 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -960,6 +960,11 @@ public final void fieldCapsAsync(FieldCapabilitiesRequest fieldCapabilitiesReque FieldCapabilitiesResponse::fromXContent, listener, emptySet()); } + /** + * @deprecated If creating a new HLRC ReST API call, consider creating new actions instead of reusing server actions. The Validation + * layer has been added to the ReST client, and requests should extend {@link Validatable} instead of {@link ActionRequest}. + */ + @Deprecated protected final Resp performRequestAndParseEntity(Req request, CheckedFunction requestConverter, RequestOptions options, @@ -969,15 +974,58 @@ protected final Resp performRequestAndParseEnt response -> parseEntity(response.getEntity(), entityParser), ignores); } + /** + * Defines a helper method for performing a request and then parsing the returned entity using the provided entityParser. + */ + protected final Resp performRequestAndParseEntity(Req request, + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction entityParser, + Set ignores) throws IOException { + return performRequest(request, requestConverter, options, + response -> parseEntity(response.getEntity(), entityParser), ignores); + } + + /** + * @deprecated If creating a new HLRC ReST API call, consider creating new actions instead of reusing server actions. The Validation + * layer has been added to the ReST client, and requests should extend {@link Validatable} instead of {@link ActionRequest}. + */ + @Deprecated protected final Resp performRequest(Req request, - CheckedFunction requestConverter, - RequestOptions options, - CheckedFunction responseConverter, - Set ignores) throws IOException { + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction responseConverter, + Set ignores) throws IOException { ActionRequestValidationException validationException = request.validate(); - if (validationException != null) { + if (validationException != null && validationException.validationErrors().isEmpty() == false) { throw validationException; } + return internalPerformRequest(request, requestConverter, options, responseConverter, ignores); + } + + /** + * Defines a helper method for performing a request. + */ + protected final Resp performRequest(Req request, + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction responseConverter, + Set ignores) throws IOException { + ValidationException validationException = request.validate(); + if (validationException != null && validationException.validationErrors().isEmpty() == false) { + throw validationException; + } + return internalPerformRequest(request, requestConverter, options, responseConverter, ignores); + } + + /** + * Provides common functionality for performing a request. + */ + private Resp internalPerformRequest(Req request, + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction responseConverter, + Set ignores) throws IOException { Request req = requestConverter.apply(request); req.setOptions(options); Response response; @@ -1005,25 +1053,75 @@ protected final Resp performRequest(Req reques } } + /** + * @deprecated If creating a new HLRC ReST API call, consider creating new actions instead of reusing server actions. The Validation + * layer has been added to the ReST client, and requests should extend {@link Validatable} instead of {@link ActionRequest}. + */ + @Deprecated protected final void performRequestAsyncAndParseEntity(Req request, - CheckedFunction requestConverter, - RequestOptions options, - CheckedFunction entityParser, - ActionListener listener, Set ignores) { + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction entityParser, + ActionListener listener, Set ignores) { performRequestAsync(request, requestConverter, options, response -> parseEntity(response.getEntity(), entityParser), listener, ignores); } + /** + * Defines a helper method for asynchronously performing a request. + */ + protected final void performRequestAsyncAndParseEntity(Req request, + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction entityParser, + ActionListener listener, Set ignores) { + performRequestAsync(request, requestConverter, options, + response -> parseEntity(response.getEntity(), entityParser), listener, ignores); + } + + + /** + * @deprecated If creating a new HLRC ReST API call, consider creating new actions instead of reusing server actions. The Validation + * layer has been added to the ReST client, and requests should extend {@link Validatable} instead of {@link ActionRequest}. + */ + @Deprecated protected final void performRequestAsync(Req request, - CheckedFunction requestConverter, - RequestOptions options, - CheckedFunction responseConverter, - ActionListener listener, Set ignores) { + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction responseConverter, + ActionListener listener, Set ignores) { ActionRequestValidationException validationException = request.validate(); - if (validationException != null) { + if (validationException != null && validationException.validationErrors().isEmpty() == false) { listener.onFailure(validationException); return; } + internalPerformRequestAsync(request, requestConverter, options, responseConverter, listener, ignores); + } + + /** + * Defines a helper method for asynchronously performing a request. + */ + protected final void performRequestAsync(Req request, + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction responseConverter, + ActionListener listener, Set ignores) { + ValidationException validationException = request.validate(); + if (validationException != null && validationException.validationErrors().isEmpty() == false) { + listener.onFailure(validationException); + return; + } + internalPerformRequestAsync(request, requestConverter, options, responseConverter, listener, ignores); + } + + /** + * Provides common functionality for asynchronously performing a request. + */ + private void internalPerformRequestAsync(Req request, + CheckedFunction requestConverter, + RequestOptions options, + CheckedFunction responseConverter, + ActionListener listener, Set ignores) { Request req; try { req = requestConverter.apply(request); @@ -1037,6 +1135,7 @@ protected final void performRequestAsync(Req r client.performRequestAsync(req, responseListener); } + final ResponseListener wrapResponseListener(CheckedFunction responseConverter, ActionListener actionListener, Set ignores) { return new ResponseListener() { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.java new file mode 100644 index 000000000000..2efff4d3663b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.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.client; + +/** + * Defines a validation layer for Requests. + */ +public interface Validatable { + ValidationException EMPTY_VALIDATION = new ValidationException() { + @Override + public void addValidationError(String error) { + throw new UnsupportedOperationException("Validation messages should not be added to the empty validation"); + } + }; + + /** + * Perform validation. This method does not have to be overridden in the event that no validation needs to be done. + * + * @return potentially null, in the event of older actions, an empty {@link ValidationException} in newer actions, or finally a + * {@link ValidationException} that contains a list of all failed validation. + */ + default ValidationException validate() { + return EMPTY_VALIDATION; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ValidationException.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ValidationException.java new file mode 100644 index 000000000000..6b5d738d6756 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ValidationException.java @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client; + +import java.util.ArrayList; +import java.util.List; + +/** + * Encapsulates an accumulation of validation errors + */ +public class ValidationException extends IllegalArgumentException { + private final List validationErrors = new ArrayList<>(); + + /** + * Add a new validation error to the accumulating validation errors + * @param error the error to add + */ + public void addValidationError(String error) { + validationErrors.add(error); + } + + /** + * Returns the validation errors accumulated + */ + public final List validationErrors() { + return validationErrors; + } + + @Override + public final String getMessage() { + StringBuilder sb = new StringBuilder(); + sb.append("Validation Failed: "); + int index = 0; + for (String error : validationErrors) { + sb.append(++index).append(": ").append(error).append(";"); + } + return sb.toString(); + } +} From 528e727999bc5bf851019e0848d1c2bbb8c17f34 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 22 Aug 2018 10:19:30 -0400 Subject: [PATCH 108/283] Fix method reference in comment in SchedulerEngine This commit fixes the name of a method reference in a comment in SchedulerEngine. --- .../elasticsearch/xpack/core/scheduler/SchedulerEngine.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java index 30e41734906b..66a2eb358986 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngine.java @@ -194,8 +194,8 @@ public void run() { /* * Allowing the throwable to escape here will lead to be it being caught in FutureTask#run and set as the outcome of this * task; however, we never inspect the the outcomes of these scheduled tasks and so allowing the throwable to escape - * unhandled here could lead to us losing fatal errors. Instead, we rely on ExceptionsHelper#maybeThrowErrorOnAnotherThread - * to appropriately dispatch any error to the uncaught exception handler. We should never see an exception here as these do + * unhandled here could lead to us losing fatal errors. Instead, we rely on ExceptionsHelper#maybeDieOnAnotherThread to + * appropriately dispatch any error to the uncaught exception handler. We should never see an exception here as these do * not escape from SchedulerEngine#notifyListeners. */ ExceptionsHelper.maybeDieOnAnotherThread(t); From abb4c183f1e9ed8835f4c4bfede6eebc9c85c3ef Mon Sep 17 00:00:00 2001 From: Dimitrios Liappis Date: Wed, 22 Aug 2018 18:18:30 +0300 Subject: [PATCH 109/283] Clarify ignore_above behavior with arrays of strings Currently docs don't explain how `ignore_above` behaves with arrays of strings. Clarify how `ignore_above` applies for arrays of strings and also note that all string(s) will still be visible in the `_source` field. Relates #33057 --- docs/reference/mapping/params/ignore-above.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/mapping/params/ignore-above.asciidoc b/docs/reference/mapping/params/ignore-above.asciidoc index 95704c6c8bbe..fe7c6881a064 100644 --- a/docs/reference/mapping/params/ignore-above.asciidoc +++ b/docs/reference/mapping/params/ignore-above.asciidoc @@ -2,6 +2,9 @@ === `ignore_above` Strings longer than the `ignore_above` setting will not be indexed or stored. +For arrays of strings, `ignore_above` will be applied for each array element separately and string elements longer than `ignore_above` will not be indexed or stored. + +NOTE: All strings/array elements will still be present in the `_source` field, if the latter is enabled which is the default in Elasticsearch. [source,js] -------------------------------------------------- From 393eec148217a721281b111ea8ab95c436ec5684 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 22 Aug 2018 17:23:54 +0200 Subject: [PATCH 110/283] Set maxScore for empty TopDocs to Nan rather than 0 (#32938) We used to set `maxScore` to `0` within `TopDocs` in situations where there is really no score as the size was set to `0` and scores were not even tracked. In such scenarios, `Float.Nan` is more appropriate, which gets converted to `max_score: null` on the REST layer. That's also more consistent with lucene which set `maxScore` to `Float.Nan` when merging empty `TopDocs` (see `TopDocs#merge`). --- .../org/elasticsearch/client/SearchIT.java | 12 ++--- .../bucket/children-aggregation.asciidoc | 2 +- docs/reference/getting-started.asciidoc | 2 +- .../mapping/params/normalizer.asciidoc | 2 +- .../migration/migrate_7_0/search.asciidoc | 5 ++ docs/reference/search/request-body.asciidoc | 2 +- .../suggesters/completion-suggest.asciidoc | 2 +- .../ParentChildInnerHitContextBuilder.java | 2 +- .../rest-api-spec/test/scroll/10_basic.yml | 48 +++++++++++++++++++ .../test/search/110_field_collapsing.yml | 17 +++++++ .../search/140_pre_filter_search_shards.yml | 1 - .../test/search/190_index_prefix_search.yml | 3 ++ .../test/search/210_rescore_explain.yml | 1 + .../elasticsearch/common/lucene/Lucene.java | 2 +- .../index/query/NestedQueryBuilder.java | 2 +- .../search/query/QueryPhase.java | 2 +- .../search/query/TopDocsCollectorContext.java | 2 +- .../aggregations/metrics/TopHitsIT.java | 4 +- .../search/query/QueryPhaseTests.java | 27 +++++++++-- .../rest/yaml/section/MatchAssertion.java | 8 +++- .../yaml/section/MatchAssertionTests.java | 42 ++++++++++++++++ 21 files changed, 163 insertions(+), 25 deletions(-) create mode 100644 test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java index 9c9c5425f000..739a590ba5f6 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java @@ -256,7 +256,7 @@ public void testSearchWithTermsAgg() throws IOException { assertNull(searchResponse.getSuggest()); assertEquals(Collections.emptyMap(), searchResponse.getProfileResults()); assertEquals(0, searchResponse.getHits().getHits().length); - assertEquals(0f, searchResponse.getHits().getMaxScore(), 0f); + assertEquals(Float.NaN, searchResponse.getHits().getMaxScore(), 0f); Terms termsAgg = searchResponse.getAggregations().get("agg1"); assertEquals("agg1", termsAgg.getName()); assertEquals(2, termsAgg.getBuckets().size()); @@ -293,7 +293,7 @@ public void testSearchWithRangeAgg() throws IOException { assertEquals(Collections.emptyMap(), searchResponse.getProfileResults()); assertEquals(5, searchResponse.getHits().totalHits); assertEquals(0, searchResponse.getHits().getHits().length); - assertEquals(0f, searchResponse.getHits().getMaxScore(), 0f); + assertEquals(Float.NaN, searchResponse.getHits().getMaxScore(), 0f); Range rangeAgg = searchResponse.getAggregations().get("agg1"); assertEquals("agg1", rangeAgg.getName()); assertEquals(2, rangeAgg.getBuckets().size()); @@ -323,7 +323,7 @@ public void testSearchWithTermsAndRangeAgg() throws IOException { assertNull(searchResponse.getSuggest()); assertEquals(Collections.emptyMap(), searchResponse.getProfileResults()); assertEquals(0, searchResponse.getHits().getHits().length); - assertEquals(0f, searchResponse.getHits().getMaxScore(), 0f); + assertEquals(Float.NaN, searchResponse.getHits().getMaxScore(), 0f); Terms termsAgg = searchResponse.getAggregations().get("agg1"); assertEquals("agg1", termsAgg.getName()); assertEquals(2, termsAgg.getBuckets().size()); @@ -375,7 +375,7 @@ public void testSearchWithMatrixStats() throws IOException { assertEquals(Collections.emptyMap(), searchResponse.getProfileResults()); assertEquals(5, searchResponse.getHits().totalHits); assertEquals(0, searchResponse.getHits().getHits().length); - assertEquals(0f, searchResponse.getHits().getMaxScore(), 0f); + assertEquals(Float.NaN, searchResponse.getHits().getMaxScore(), 0f); assertEquals(1, searchResponse.getAggregations().asList().size()); MatrixStats matrixStats = searchResponse.getAggregations().get("agg1"); assertEquals(5, matrixStats.getFieldCount("num")); @@ -474,7 +474,7 @@ public void testSearchWithParentJoin() throws IOException { assertEquals(Collections.emptyMap(), searchResponse.getProfileResults()); assertEquals(3, searchResponse.getHits().totalHits); assertEquals(0, searchResponse.getHits().getHits().length); - assertEquals(0f, searchResponse.getHits().getMaxScore(), 0f); + assertEquals(Float.NaN, searchResponse.getHits().getMaxScore(), 0f); assertEquals(1, searchResponse.getAggregations().asList().size()); Terms terms = searchResponse.getAggregations().get("top-tags"); assertEquals(0, terms.getDocCountError()); @@ -513,7 +513,7 @@ public void testSearchWithSuggest() throws IOException { assertNull(searchResponse.getAggregations()); assertEquals(Collections.emptyMap(), searchResponse.getProfileResults()); assertEquals(0, searchResponse.getHits().totalHits); - assertEquals(0f, searchResponse.getHits().getMaxScore(), 0f); + assertEquals(Float.NaN, searchResponse.getHits().getMaxScore(), 0f); assertEquals(0, searchResponse.getHits().getHits().length); assertEquals(1, searchResponse.getSuggest().size()); diff --git a/docs/reference/aggregations/bucket/children-aggregation.asciidoc b/docs/reference/aggregations/bucket/children-aggregation.asciidoc index 3805b2e564ca..e2b3c8ec5917 100644 --- a/docs/reference/aggregations/bucket/children-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/children-aggregation.asciidoc @@ -144,7 +144,7 @@ Possible response: }, "hits": { "total": 3, - "max_score": 0.0, + "max_score": null, "hits": [] }, "aggregations": { diff --git a/docs/reference/getting-started.asciidoc b/docs/reference/getting-started.asciidoc index 8229f74bdd05..c69597e74fd6 100755 --- a/docs/reference/getting-started.asciidoc +++ b/docs/reference/getting-started.asciidoc @@ -1141,7 +1141,7 @@ And the response (partially shown): }, "hits" : { "total" : 1000, - "max_score" : 0.0, + "max_score" : null, "hits" : [ ] }, "aggregations" : { diff --git a/docs/reference/mapping/params/normalizer.asciidoc b/docs/reference/mapping/params/normalizer.asciidoc index 3688a0e94541..73110cd11f5a 100644 --- a/docs/reference/mapping/params/normalizer.asciidoc +++ b/docs/reference/mapping/params/normalizer.asciidoc @@ -151,7 +151,7 @@ returns }, "hits": { "total": 3, - "max_score": 0.0, + "max_score": null, "hits": [] }, "aggregations": { diff --git a/docs/reference/migration/migrate_7_0/search.asciidoc b/docs/reference/migration/migrate_7_0/search.asciidoc index 094294d85304..76367115e130 100644 --- a/docs/reference/migration/migrate_7_0/search.asciidoc +++ b/docs/reference/migration/migrate_7_0/search.asciidoc @@ -100,3 +100,8 @@ and the context is only accepted if `path` points to a field with `geo_point` ty `max_concurrent_shard_requests` used to limit the total number of concurrent shard requests a single high level search request can execute. In 7.0 this changed to be the max number of concurrent shard requests per node. The default is now `5`. + +==== `max_score` set to `null` when scores are not tracked + +`max_score` used to be set to `0` whenever scores are not tracked. `null` is now used +instead which is a more appropriate value for a scenario where scores are not available. diff --git a/docs/reference/search/request-body.asciidoc b/docs/reference/search/request-body.asciidoc index e7c9b593af37..ad24d9c93c6b 100644 --- a/docs/reference/search/request-body.asciidoc +++ b/docs/reference/search/request-body.asciidoc @@ -161,7 +161,7 @@ be set to `true` in the response. }, "hits": { "total": 1, - "max_score": 0.0, + "max_score": null, "hits": [] } } diff --git a/docs/reference/search/suggesters/completion-suggest.asciidoc b/docs/reference/search/suggesters/completion-suggest.asciidoc index 9f9833bde9d5..c52f28bc7bea 100644 --- a/docs/reference/search/suggesters/completion-suggest.asciidoc +++ b/docs/reference/search/suggesters/completion-suggest.asciidoc @@ -258,7 +258,7 @@ Which should look like: }, "hits": { "total" : 0, - "max_score" : 0.0, + "max_score" : null, "hits" : [] }, "suggest": { diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java index 6593c7efb9fa..5e57a2774055 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java @@ -131,7 +131,7 @@ public TopDocs[] topDocs(SearchHit[] hits) throws IOException { for (LeafReaderContext ctx : context.searcher().getIndexReader().leaves()) { intersect(weight, innerHitQueryWeight, totalHitCountCollector, ctx); } - result[i] = new TopDocs(totalHitCountCollector.getTotalHits(), Lucene.EMPTY_SCORE_DOCS, 0); + result[i] = new TopDocs(totalHitCountCollector.getTotalHits(), Lucene.EMPTY_SCORE_DOCS, Float.NaN); } else { int topN = Math.min(from() + size(), context.searcher().getIndexReader().maxDoc()); TopDocsCollector topDocsCollector; diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/scroll/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/scroll/10_basic.yml index 5ecc357e0e16..6ab18146bba6 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/scroll/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/scroll/10_basic.yml @@ -233,3 +233,51 @@ query: match_all: {} size: 0 + +--- +"Scroll max_score is null": + - skip: + version: " - 6.99.99" + reason: max_score was set to 0 rather than null before 7.0 + + - do: + indices.create: + index: test_scroll + - do: + index: + index: test_scroll + type: test + id: 42 + body: { foo: 1 } + + - do: + index: + index: test_scroll + type: test + id: 43 + body: { foo: 2 } + + - do: + indices.refresh: {} + + - do: + search: + index: test_scroll + size: 1 + scroll: 1m + sort: foo + body: + query: + match_all: {} + + - set: {_scroll_id: scroll_id} + - length: {hits.hits: 1 } + - match: { hits.max_score: null } + + - do: + scroll: + scroll_id: $scroll_id + scroll: 1m + + - length: {hits.hits: 1 } + - match: { hits.max_score: null } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/110_field_collapsing.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/110_field_collapsing.yml index 521dc4c1cac8..dad05cce4eb4 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/110_field_collapsing.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/110_field_collapsing.yml @@ -244,6 +244,23 @@ setup: - match: { hits.total: 6 } - length: { hits.hits: 0 } +--- +"no hits and inner_hits max_score null": + + - skip: + version: " - 6.99.99" + reason: max_score was set to 0 rather than null before 7.0 + + - do: + search: + index: test + body: + size: 0 + collapse: { field: numeric_group, inner_hits: { name: sub_hits, size: 1} } + sort: [{ sort: desc }] + + - match: { hits.max_score: null } + --- "field collapsing and multiple inner_hits": diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/140_pre_filter_search_shards.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/140_pre_filter_search_shards.yml index dc6b130b2895..c63dee2e211f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/140_pre_filter_search_shards.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/140_pre_filter_search_shards.yml @@ -128,7 +128,6 @@ setup: - match: { hits.total: 2 } - match: { aggregations.some_agg.doc_count: 3 } - - do: search: pre_filter_shard_size: 1 diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/190_index_prefix_search.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/190_index_prefix_search.yml index dfe0b6825cdc..62770e2915d2 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/190_index_prefix_search.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/190_index_prefix_search.yml @@ -39,6 +39,7 @@ setup: df: text - match: {hits.total: 1} + - match: {hits.max_score: 1} - match: {hits.hits.0._score: 1} - do: @@ -52,6 +53,7 @@ setup: boost: 2 - match: {hits.total: 1} + - match: {hits.max_score: 2} - match: {hits.hits.0._score: 2} - do: @@ -61,6 +63,7 @@ setup: df: text - match: {hits.total: 1} + - match: {hits.max_score: 1} - match: {hits.hits.0._score: 1} --- diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/210_rescore_explain.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/210_rescore_explain.yml index 24920580c455..4d7ee91bef5f 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/210_rescore_explain.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/210_rescore_explain.yml @@ -29,6 +29,7 @@ query_weight: 5 rescore_query_weight: 10 + - match: {hits.max_score: 15} - match: { hits.hits.0._score: 15 } - match: { hits.hits.0._explanation.value: 15 } diff --git a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java index ebd0d5ba2efb..a24a6aea07fc 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java @@ -101,7 +101,7 @@ public class Lucene { public static final ScoreDoc[] EMPTY_SCORE_DOCS = new ScoreDoc[0]; - public static final TopDocs EMPTY_TOP_DOCS = new TopDocs(0, EMPTY_SCORE_DOCS, 0.0f); + public static final TopDocs EMPTY_TOP_DOCS = new TopDocs(0, EMPTY_SCORE_DOCS, Float.NaN); public static Version parseVersion(@Nullable String version, Version defaultVersion, Logger logger) { if (version == null) { diff --git a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index 889f41a037f8..991628578942 100644 --- a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -398,7 +398,7 @@ public TopDocs[] topDocs(SearchHit[] hits) throws IOException { if (size() == 0) { TotalHitCountCollector totalHitCountCollector = new TotalHitCountCollector(); intersect(weight, innerHitQueryWeight, totalHitCountCollector, ctx); - result[i] = new TopDocs(totalHitCountCollector.getTotalHits(), Lucene.EMPTY_SCORE_DOCS, 0); + result[i] = new TopDocs(totalHitCountCollector.getTotalHits(), Lucene.EMPTY_SCORE_DOCS, Float.NaN); } else { int topN = Math.min(from() + size(), context.searcher().getIndexReader().maxDoc()); TopDocsCollector topDocsCollector; diff --git a/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java b/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java index ca06005448c0..84c76e85f3dd 100644 --- a/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java +++ b/server/src/main/java/org/elasticsearch/search/query/QueryPhase.java @@ -95,7 +95,7 @@ public void execute(SearchContext searchContext) throws QueryPhaseExecutionExcep suggestPhase.execute(searchContext); // TODO: fix this once we can fetch docs for suggestions searchContext.queryResult().topDocs( - new TopDocs(0, Lucene.EMPTY_SCORE_DOCS, 0), + new TopDocs(0, Lucene.EMPTY_SCORE_DOCS, Float.NaN), new DocValueFormat[0]); return; } diff --git a/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java b/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java index dc110b279771..8d40cc802fff 100644 --- a/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java +++ b/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java @@ -120,7 +120,7 @@ Collector create(Collector in) { @Override void postProcess(QuerySearchResult result) { final int totalHitCount = hitCountSupplier.getAsInt(); - result.topDocs(new TopDocs(totalHitCount, Lucene.EMPTY_SCORE_DOCS, 0), null); + result.topDocs(new TopDocs(totalHitCount, Lucene.EMPTY_SCORE_DOCS, Float.NaN), null); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java index 952eb22848e1..a74734c622f8 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java @@ -321,7 +321,7 @@ public void testIssue11119() throws Exception { assertThat(response.getHits().getTotalHits(), equalTo(8L)); assertThat(response.getHits().getHits().length, equalTo(0)); - assertThat(response.getHits().getMaxScore(), equalTo(0f)); + assertThat(response.getHits().getMaxScore(), equalTo(Float.NaN)); Terms terms = response.getAggregations().get("terms"); assertThat(terms, notNullValue()); assertThat(terms.getName(), equalTo("terms")); @@ -356,7 +356,7 @@ public void testIssue11119() throws Exception { assertThat(response.getHits().getTotalHits(), equalTo(8L)); assertThat(response.getHits().getHits().length, equalTo(0)); - assertThat(response.getHits().getMaxScore(), equalTo(0f)); + assertThat(response.getHits().getMaxScore(), equalTo(Float.NaN)); terms = response.getAggregations().get("terms"); assertThat(terms, notNullValue()); assertThat(terms.getName(), equalTo("terms")); diff --git a/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java b/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java index 16365d829a83..872267417c37 100644 --- a/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java +++ b/server/src/test/java/org/elasticsearch/search/query/QueryPhaseTests.java @@ -67,6 +67,7 @@ import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.instanceOf; @@ -103,6 +104,7 @@ private void countTestCase(Query query, IndexReader reader, boolean shouldCollec final boolean rescore = QueryPhase.execute(context, searcher, checkCancelled -> {}); assertFalse(rescore); assertEquals(searcher.count(query), context.queryResult().topDocs().totalHits); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); } private void countTestCase(boolean withDeletions) throws Exception { @@ -172,11 +174,14 @@ public void testPostFilterDisablesCountOptimization() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertEquals(1, context.queryResult().topDocs().totalHits); + assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(0)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); contextSearcher = new IndexSearcher(reader); context.parsedPostFilter(new ParsedQuery(new MatchNoDocsQuery())); QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertEquals(0, context.queryResult().topDocs().totalHits); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); reader.close(); dir.close(); } @@ -205,13 +210,13 @@ public void testTerminateAfterWithFilter() throws Exception { context.parsedPostFilter(new ParsedQuery(new TermQuery(new Term("foo", Integer.toString(i))))); QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertEquals(1, context.queryResult().topDocs().totalHits); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(1F)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(1)); } reader.close(); dir.close(); } - public void testMinScoreDisablesCountOptimization() throws Exception { Directory dir = newDirectory(); final Sort sort = new Sort(new SortField("rank", SortField.Type.INT)); @@ -230,11 +235,13 @@ public void testMinScoreDisablesCountOptimization() throws Exception { context.setTask(new SearchTask(123L, "", "", "", null, Collections.emptyMap())); QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertEquals(1, context.queryResult().topDocs().totalHits); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); contextSearcher = new IndexSearcher(reader); context.minimumScore(100); QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertEquals(0, context.queryResult().topDocs().totalHits); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); reader.close(); dir.close(); } @@ -289,6 +296,7 @@ public void testInOrderScrollOptimization() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertThat(context.queryResult().topDocs().totalHits, equalTo((long) numDocs)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(1F)); assertNull(context.queryResult().terminatedEarly()); assertThat(context.terminateAfter(), equalTo(0)); assertThat(context.queryResult().getTotalHits(), equalTo((long) numDocs)); @@ -296,9 +304,11 @@ public void testInOrderScrollOptimization() throws Exception { contextSearcher = getAssertingEarlyTerminationSearcher(reader, size); QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertThat(context.queryResult().topDocs().totalHits, equalTo((long) numDocs)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(1F)); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.terminateAfter(), equalTo(size)); assertThat(context.queryResult().getTotalHits(), equalTo((long) numDocs)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(1F)); assertThat(context.queryResult().topDocs().scoreDocs[0].doc, greaterThanOrEqualTo(size)); reader.close(); dir.close(); @@ -334,12 +344,14 @@ public void testTerminateAfterEarlyTermination() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.queryResult().topDocs().totalHits, equalTo(1L)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(1F)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(1)); context.setSize(0); QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.queryResult().topDocs().totalHits, equalTo(1L)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(0)); } @@ -348,6 +360,7 @@ public void testTerminateAfterEarlyTermination() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.queryResult().topDocs().totalHits, equalTo(1L)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(1F)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(1)); } { @@ -360,6 +373,7 @@ public void testTerminateAfterEarlyTermination() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.queryResult().topDocs().totalHits, equalTo(1L)); + assertThat(context.queryResult().topDocs().getMaxScore(), greaterThan(0f)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(1)); context.setSize(0); @@ -367,6 +381,7 @@ public void testTerminateAfterEarlyTermination() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.queryResult().topDocs().totalHits, equalTo(1L)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(0)); } { @@ -376,6 +391,7 @@ public void testTerminateAfterEarlyTermination() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.queryResult().topDocs().totalHits, equalTo(1L)); + assertThat(context.queryResult().topDocs().getMaxScore(), greaterThan(0f)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(1)); assertThat(collector.getTotalHits(), equalTo(1)); context.queryCollectors().clear(); @@ -387,6 +403,7 @@ public void testTerminateAfterEarlyTermination() throws Exception { QueryPhase.execute(context, contextSearcher, checkCancelled -> {}); assertTrue(context.queryResult().terminatedEarly()); assertThat(context.queryResult().topDocs().totalHits, equalTo(1L)); + assertThat(context.queryResult().topDocs().getMaxScore(), equalTo(Float.NaN)); assertThat(context.queryResult().topDocs().scoreDocs.length, equalTo(0)); assertThat(collector.getTotalHits(), equalTo(1)); } @@ -539,19 +556,19 @@ public void testIndexSortScrollOptimization() throws Exception { dir.close(); } - static IndexSearcher getAssertingEarlyTerminationSearcher(IndexReader reader, int size) { + private static IndexSearcher getAssertingEarlyTerminationSearcher(IndexReader reader, int size) { return new IndexSearcher(reader) { protected void search(List leaves, Weight weight, Collector collector) throws IOException { - final Collector in = new AssertingEalyTerminationFilterCollector(collector, size); + final Collector in = new AssertingEarlyTerminationFilterCollector(collector, size); super.search(leaves, weight, in); } }; } - private static class AssertingEalyTerminationFilterCollector extends FilterCollector { + private static class AssertingEarlyTerminationFilterCollector extends FilterCollector { private final int size; - AssertingEalyTerminationFilterCollector(Collector in, int size) { + AssertingEarlyTerminationFilterCollector(Collector in, int size) { super(in); this.size = size; } diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java index 82d8dbeebe6a..6ecaae75a8ee 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java @@ -32,6 +32,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; /** @@ -70,8 +71,13 @@ protected void doAssert(Object actualValue, Object expectedValue) { } } - assertNotNull("field [" + getField() + "] is null", actualValue); logger.trace("assert that [{}] matches [{}] (field [{}])", actualValue, expectedValue, getField()); + if (expectedValue == null) { + assertNull("field [" + getField() + "] should be null but was [" + actualValue + "]", actualValue); + return; + } + assertNotNull("field [" + getField() + "] is null", actualValue); + if (actualValue.getClass().equals(safeClass(expectedValue)) == false) { if (actualValue instanceof Number && expectedValue instanceof Number) { //Double 1.0 is equal to Integer 1 diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java new file mode 100644 index 000000000000..2bd723474412 --- /dev/null +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/MatchAssertionTests.java @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.test.rest.yaml.section; + +import org.elasticsearch.common.xcontent.XContentLocation; +import org.elasticsearch.test.ESTestCase; + +public class MatchAssertionTests extends ESTestCase { + + public void testNull() { + XContentLocation xContentLocation = new XContentLocation(0, 0); + { + MatchAssertion matchAssertion = new MatchAssertion(xContentLocation, "field", null); + matchAssertion.doAssert(null, null); + expectThrows(AssertionError.class, () -> matchAssertion.doAssert("non-null", null)); + } + { + MatchAssertion matchAssertion = new MatchAssertion(xContentLocation, "field", "non-null"); + expectThrows(AssertionError.class, () -> matchAssertion.doAssert(null, "non-null")); + } + { + MatchAssertion matchAssertion = new MatchAssertion(xContentLocation, "field", "/exp/"); + expectThrows(AssertionError.class, () -> matchAssertion.doAssert(null, "/exp/")); + } + } +} From c3438bc8d87ddb1786646505d84880b2018623c5 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Wed, 22 Aug 2018 14:02:39 -0400 Subject: [PATCH 111/283] Switch some watcher tests to new style Requests (#33044) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. This changes all calls in the `x-pack/qa/smoke-test-monitoring-with-watcher`, `x-pack/qa/smoke-test-watcher`, and `x-pack/qa/smoke-test-watcher-with-security` projects to use the new versions. --- .../MonitoringWithWatcherRestIT.java | 69 +++++++++---------- ...cherWithSecurityClientYamlTestSuiteIT.java | 11 ++- .../SmokeTestWatcherWithSecurityIT.java | 56 +++++++-------- .../SmokeTestWatcherTestSuiteIT.java | 38 +++++----- 4 files changed, 81 insertions(+), 93 deletions(-) diff --git a/x-pack/qa/smoke-test-monitoring-with-watcher/src/test/java/org/elasticsearch/smoketest/MonitoringWithWatcherRestIT.java b/x-pack/qa/smoke-test-monitoring-with-watcher/src/test/java/org/elasticsearch/smoketest/MonitoringWithWatcherRestIT.java index d89d558f02fa..d3b9a974398d 100644 --- a/x-pack/qa/smoke-test-monitoring-with-watcher/src/test/java/org/elasticsearch/smoketest/MonitoringWithWatcherRestIT.java +++ b/x-pack/qa/smoke-test-monitoring-with-watcher/src/test/java/org/elasticsearch/smoketest/MonitoringWithWatcherRestIT.java @@ -5,12 +5,10 @@ */ package org.elasticsearch.smoketest; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.lucene.util.LuceneTestCase.AwaitsFix; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.test.rest.ESRestTestCase; @@ -23,7 +21,6 @@ import org.junit.After; import java.io.IOException; -import java.util.Collections; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.watcher.input.InputBuilders.simpleInput; @@ -36,25 +33,25 @@ public class MonitoringWithWatcherRestIT extends ESRestTestCase { @After public void cleanExporters() throws Exception { - String body = Strings.toString(jsonBuilder().startObject().startObject("transient") - .nullField("xpack.monitoring.exporters.*") - .endObject().endObject()); - assertOK(adminClient().performRequest("PUT", "_cluster/settings", Collections.emptyMap(), - new StringEntity(body, ContentType.APPLICATION_JSON))); - - assertOK(adminClient().performRequest("DELETE", ".watch*", Collections.emptyMap())); + Request request = new Request("PUT", "/_cluster/settings"); + request.setJsonEntity(Strings.toString(jsonBuilder().startObject() + .startObject("transient") + .nullField("xpack.monitoring.exporters.*") + .endObject().endObject())); + adminClient().performRequest(request); + adminClient().performRequest(new Request("DELETE", "/.watch*")); } public void testThatLocalExporterAddsWatches() throws Exception { String watchId = createMonitoringWatch(); - String body = BytesReference.bytes(jsonBuilder().startObject().startObject("transient") - .field("xpack.monitoring.exporters.my_local_exporter.type", "local") - .field("xpack.monitoring.exporters.my_local_exporter.cluster_alerts.management.enabled", true) - .endObject().endObject()).utf8ToString(); - - adminClient().performRequest("PUT", "_cluster/settings", Collections.emptyMap(), - new StringEntity(body, ContentType.APPLICATION_JSON)); + Request request = new Request("PUT", "/_cluster/settings"); + request.setJsonEntity(Strings.toString(jsonBuilder().startObject() + .startObject("transient") + .field("xpack.monitoring.exporters.my_local_exporter.type", "local") + .field("xpack.monitoring.exporters.my_local_exporter.cluster_alerts.management.enabled", true) + .endObject().endObject())); + adminClient().performRequest(request); assertTotalWatchCount(ClusterAlertsUtil.WATCH_IDS.length); @@ -65,14 +62,14 @@ public void testThatHttpExporterAddsWatches() throws Exception { String watchId = createMonitoringWatch(); String httpHost = getHttpHost(); - String body = BytesReference.bytes(jsonBuilder().startObject().startObject("transient") - .field("xpack.monitoring.exporters.my_http_exporter.type", "http") - .field("xpack.monitoring.exporters.my_http_exporter.host", httpHost) - .field("xpack.monitoring.exporters.my_http_exporter.cluster_alerts.management.enabled", true) - .endObject().endObject()).utf8ToString(); - - adminClient().performRequest("PUT", "_cluster/settings", Collections.emptyMap(), - new StringEntity(body, ContentType.APPLICATION_JSON)); + Request request = new Request("PUT", "/_cluster/settings"); + request.setJsonEntity(Strings.toString(jsonBuilder().startObject() + .startObject("transient") + .field("xpack.monitoring.exporters.my_http_exporter.type", "http") + .field("xpack.monitoring.exporters.my_http_exporter.host", httpHost) + .field("xpack.monitoring.exporters.my_http_exporter.cluster_alerts.management.enabled", true) + .endObject().endObject())); + adminClient().performRequest(request); assertTotalWatchCount(ClusterAlertsUtil.WATCH_IDS.length); @@ -80,15 +77,15 @@ public void testThatHttpExporterAddsWatches() throws Exception { } private void assertMonitoringWatchHasBeenOverWritten(String watchId) throws Exception { - ObjectPath path = ObjectPath.createFromResponse(client().performRequest("GET", "_xpack/watcher/watch/" + watchId)); + ObjectPath path = ObjectPath.createFromResponse(client().performRequest(new Request("GET", "/_xpack/watcher/watch/" + watchId))); String interval = path.evaluate("watch.trigger.schedule.interval"); assertThat(interval, is("1m")); } private void assertTotalWatchCount(int expectedWatches) throws Exception { assertBusy(() -> { - assertOK(client().performRequest("POST", ".watches/_refresh")); - ObjectPath path = ObjectPath.createFromResponse(client().performRequest("POST", ".watches/_count")); + assertOK(client().performRequest(new Request("POST", "/.watches/_refresh"))); + ObjectPath path = ObjectPath.createFromResponse(client().performRequest(new Request("POST", "/.watches/_count"))); int count = path.evaluate("count"); assertThat(count, is(expectedWatches)); }); @@ -97,28 +94,28 @@ private void assertTotalWatchCount(int expectedWatches) throws Exception { private String createMonitoringWatch() throws Exception { String clusterUUID = getClusterUUID(); String watchId = clusterUUID + "_kibana_version_mismatch"; - String sampleWatch = WatchSourceBuilders.watchBuilder() + Request request = new Request("PUT", "/_xpack/watcher/watch/" + watchId); + request.setJsonEntity(WatchSourceBuilders.watchBuilder() .trigger(TriggerBuilders.schedule(new IntervalSchedule(new IntervalSchedule.Interval(1000, MINUTES)))) .input(simpleInput()) .addAction("logme", ActionBuilders.loggingAction("foo")) - .buildAsBytes(XContentType.JSON).utf8ToString(); - client().performRequest("PUT", "_xpack/watcher/watch/" + watchId, Collections.emptyMap(), - new StringEntity(sampleWatch, ContentType.APPLICATION_JSON)); + .buildAsBytes(XContentType.JSON).utf8ToString()); + client().performRequest(request); return watchId; } private String getClusterUUID() throws Exception { - Response response = client().performRequest("GET", "_cluster/state/metadata", Collections.emptyMap()); + Response response = client().performRequest(new Request("GET", "/_cluster/state/metadata")); ObjectPath objectPath = ObjectPath.createFromResponse(response); String clusterUUID = objectPath.evaluate("metadata.cluster_uuid"); return clusterUUID; } public String getHttpHost() throws IOException { - ObjectPath path = ObjectPath.createFromResponse(client().performRequest("GET", "_cluster/state", Collections.emptyMap())); + ObjectPath path = ObjectPath.createFromResponse(client().performRequest(new Request("GET", "/_cluster/state"))); String masterNodeId = path.evaluate("master_node"); - ObjectPath nodesPath = ObjectPath.createFromResponse(client().performRequest("GET", "_nodes", Collections.emptyMap())); + ObjectPath nodesPath = ObjectPath.createFromResponse(client().performRequest(new Request("GET", "/_nodes"))); String httpHost = nodesPath.evaluate("nodes." + masterNodeId + ".http.publish_address"); return httpHost; } diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityClientYamlTestSuiteIT.java b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityClientYamlTestSuiteIT.java index a989bb476118..0c4afff509e9 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityClientYamlTestSuiteIT.java +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityClientYamlTestSuiteIT.java @@ -7,9 +7,7 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.elasticsearch.client.Response; +import org.elasticsearch.client.Request; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -49,9 +47,9 @@ public void startWatcher() throws Exception { emptyList(), emptyMap()); // create one document in this index, so we can test in the YAML tests, that the index cannot be accessed - Response resp = adminClient().performRequest("PUT", "/index_not_allowed_to_read/doc/1", Collections.emptyMap(), - new StringEntity("{\"foo\":\"bar\"}", ContentType.APPLICATION_JSON)); - assertThat(resp.getStatusLine().getStatusCode(), is(201)); + Request request = new Request("PUT", "/index_not_allowed_to_read/doc/1"); + request.setJsonEntity("{\"foo\":\"bar\"}"); + adminClient().performRequest(request); assertBusy(() -> { ClientYamlTestResponse response = @@ -129,4 +127,3 @@ protected Settings restAdminSettings() { .build(); } } - diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java index 1c8204aa1ec6..665b92bbc0e3 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java @@ -5,9 +5,8 @@ */ package org.elasticsearch.smoketest; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; @@ -21,7 +20,6 @@ import org.junit.Before; import java.io.IOException; -import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -41,27 +39,28 @@ public class SmokeTestWatcherWithSecurityIT extends ESRestTestCase { @Before public void startWatcher() throws Exception { - StringEntity entity = new StringEntity("{ \"value\" : \"15\" }", ContentType.APPLICATION_JSON); - assertOK(adminClient().performRequest("PUT", "my_test_index/doc/1", Collections.singletonMap("refresh", "true"), entity)); + Request createAllowedDoc = new Request("PUT", "/my_test_index/doc/1"); + createAllowedDoc.setJsonEntity("{ \"value\" : \"15\" }"); + createAllowedDoc.addParameter("refresh", "true"); + adminClient().performRequest(createAllowedDoc); // delete the watcher history to not clutter with entries from other test - adminClient().performRequest("DELETE", ".watcher-history-*", Collections.emptyMap()); + adminClient().performRequest(new Request("DELETE", ".watcher-history-*")); // create one document in this index, so we can test in the YAML tests, that the index cannot be accessed - Response resp = adminClient().performRequest("PUT", "/index_not_allowed_to_read/doc/1", Collections.emptyMap(), - new StringEntity("{\"foo\":\"bar\"}", ContentType.APPLICATION_JSON)); - assertThat(resp.getStatusLine().getStatusCode(), is(201)); + Request createNotAllowedDoc = new Request("PUT", "/index_not_allowed_to_read/doc/1"); + createNotAllowedDoc.setJsonEntity("{\"foo\":\"bar\"}"); + adminClient().performRequest(createNotAllowedDoc); assertBusy(() -> { try { - Response statsResponse = adminClient().performRequest("GET", "_xpack/watcher/stats"); + Response statsResponse = adminClient().performRequest(new Request("GET", "/_xpack/watcher/stats")); ObjectPath objectPath = ObjectPath.createFromResponse(statsResponse); String state = objectPath.evaluate("stats.0.watcher_state"); switch (state) { case "stopped": - Response startResponse = adminClient().performRequest("POST", "_xpack/watcher/_start"); - assertOK(startResponse); + Response startResponse = adminClient().performRequest(new Request("POST", "/_xpack/watcher/_start")); String body = EntityUtils.toString(startResponse.getEntity()); assertThat(body, containsString("\"acknowledged\":true")); break; @@ -82,18 +81,18 @@ public void startWatcher() throws Exception { assertBusy(() -> { for (String template : WatcherIndexTemplateRegistryField.TEMPLATE_NAMES) { - assertOK(adminClient().performRequest("HEAD", "_template/" + template)); + assertOK(adminClient().performRequest(new Request("HEAD", "_template/" + template))); } }); } @After public void stopWatcher() throws Exception { - assertOK(adminClient().performRequest("DELETE", "my_test_index")); + adminClient().performRequest(new Request("DELETE", "/my_test_index")); assertBusy(() -> { try { - Response statsResponse = adminClient().performRequest("GET", "_xpack/watcher/stats"); + Response statsResponse = adminClient().performRequest(new Request("GET", "/_xpack/watcher/stats")); ObjectPath objectPath = ObjectPath.createFromResponse(statsResponse); String state = objectPath.evaluate("stats.0.watcher_state"); @@ -106,8 +105,7 @@ public void stopWatcher() throws Exception { case "starting": throw new AssertionError("waiting until starting state reached started state to stop"); case "started": - Response stopResponse = adminClient().performRequest("POST", "_xpack/watcher/_stop", Collections.emptyMap()); - assertOK(stopResponse); + Response stopResponse = adminClient().performRequest(new Request("POST", "/_xpack/watcher/_stop")); String body = EntityUtils.toString(stopResponse.getEntity()); assertThat(body, containsString("\"acknowledged\":true")); break; @@ -210,7 +208,7 @@ public void testSearchTransformHasPermissions() throws Exception { boolean conditionMet = objectPath.evaluate("hits.hits.0._source.result.condition.met"); assertThat(conditionMet, is(true)); - ObjectPath getObjectPath = ObjectPath.createFromResponse(client().performRequest("GET", "my_test_index/doc/my-id")); + ObjectPath getObjectPath = ObjectPath.createFromResponse(client().performRequest(new Request("GET", "/my_test_index/doc/my-id"))); String value = getObjectPath.evaluate("_source.hits.hits.0._source.value"); assertThat(value, is("15")); } @@ -238,8 +236,7 @@ public void testSearchTransformInsufficientPermissions() throws Exception { getWatchHistoryEntry(watchId); - Response response = adminClient().performRequest("GET", "my_test_index/doc/some-id", - Collections.singletonMap("ignore", "404")); + Response response = adminClient().performRequest(new Request("HEAD", "/my_test_index/doc/some-id")); assertThat(response.getStatusLine().getStatusCode(), is(404)); } @@ -262,7 +259,7 @@ public void testIndexActionHasPermissions() throws Exception { boolean conditionMet = objectPath.evaluate("hits.hits.0._source.result.condition.met"); assertThat(conditionMet, is(true)); - ObjectPath getObjectPath = ObjectPath.createFromResponse(client().performRequest("GET", "my_test_index/doc/my-id")); + ObjectPath getObjectPath = ObjectPath.createFromResponse(client().performRequest(new Request("GET", "/my_test_index/doc/my-id"))); String spam = getObjectPath.evaluate("_source.spam"); assertThat(spam, is("eggs")); } @@ -286,16 +283,14 @@ public void testIndexActionInsufficientPrivileges() throws Exception { boolean conditionMet = objectPath.evaluate("hits.hits.0._source.result.condition.met"); assertThat(conditionMet, is(true)); - Response response = adminClient().performRequest("GET", "index_not_allowed_to_read/doc/my-id", - Collections.singletonMap("ignore", "404")); + Response response = adminClient().performRequest(new Request("HEAD", "/index_not_allowed_to_read/doc/my-id")); assertThat(response.getStatusLine().getStatusCode(), is(404)); } private void indexWatch(String watchId, XContentBuilder builder) throws Exception { - StringEntity entity = new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); - - Response response = client().performRequest("PUT", "_xpack/watcher/watch/" + watchId, Collections.emptyMap(), entity); - assertOK(response); + Request request = new Request("PUT", "/_xpack/watcher/watch/" + watchId); + request.setJsonEntity(Strings.toString(builder)); + Response response = client().performRequest(request); Map responseMap = entityAsMap(response); assertThat(responseMap, hasEntry("_id", watchId)); } @@ -307,7 +302,7 @@ private ObjectPath getWatchHistoryEntry(String watchId) throws Exception { private ObjectPath getWatchHistoryEntry(String watchId, String state) throws Exception { final AtomicReference objectPathReference = new AtomicReference<>(); assertBusy(() -> { - client().performRequest("POST", ".watcher-history-*/_refresh"); + client().performRequest(new Request("POST", "/.watcher-history-*/_refresh")); try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); @@ -323,8 +318,9 @@ private ObjectPath getWatchHistoryEntry(String watchId, String state) throws Exc .endObject().endArray(); builder.endObject(); - StringEntity entity = new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); - Response response = client().performRequest("POST", ".watcher-history-*/_search", Collections.emptyMap(), entity); + Request searchRequest = new Request("POST", "/.watcher-history-*/_search"); + searchRequest.setJsonEntity(Strings.toString(builder)); + Response response = client().performRequest(searchRequest); ObjectPath objectPath = ObjectPath.createFromResponse(response); int totalHits = objectPath.evaluate("hits.total"); assertThat(totalHits, is(greaterThanOrEqualTo(1))); diff --git a/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java b/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java index 86d97d01904f..f56f96efc788 100644 --- a/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java +++ b/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java @@ -5,8 +5,7 @@ */ package org.elasticsearch.smoketest; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; @@ -23,7 +22,6 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static java.util.Collections.emptyMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -39,15 +37,15 @@ public class SmokeTestWatcherTestSuiteIT extends ESRestTestCase { @Before public void startWatcher() throws Exception { // delete the watcher history to not clutter with entries from other test - assertOK(adminClient().performRequest("DELETE", ".watcher-history-*")); + assertOK(adminClient().performRequest(new Request("DELETE", "/.watcher-history-*"))); assertBusy(() -> { - Response response = adminClient().performRequest("GET", "_xpack/watcher/stats"); + Response response = adminClient().performRequest(new Request("GET", "/_xpack/watcher/stats")); String state = ObjectPath.createFromResponse(response).evaluate("stats.0.watcher_state"); switch (state) { case "stopped": - Response startResponse = adminClient().performRequest("POST", "/_xpack/watcher/_start"); + Response startResponse = adminClient().performRequest(new Request("POST", "/_xpack/watcher/_start")); boolean isAcknowledged = ObjectPath.createFromResponse(startResponse).evaluate("acknowledged"); assertThat(isAcknowledged, is(true)); break; @@ -65,7 +63,7 @@ public void startWatcher() throws Exception { assertBusy(() -> { for (String template : WatcherIndexTemplateRegistryField.TEMPLATE_NAMES) { - Response templateExistsResponse = adminClient().performRequest("HEAD", "_template/" + template, emptyMap()); + Response templateExistsResponse = adminClient().performRequest(new Request("HEAD", "/_template/" + template)); assertThat(templateExistsResponse.getStatusLine().getStatusCode(), is(200)); } }); @@ -74,7 +72,7 @@ public void startWatcher() throws Exception { @After public void stopWatcher() throws Exception { assertBusy(() -> { - Response response = adminClient().performRequest("GET", "_xpack/watcher/stats", emptyMap()); + Response response = adminClient().performRequest(new Request("GET", "/_xpack/watcher/stats")); String state = ObjectPath.createFromResponse(response).evaluate("stats.0.watcher_state"); switch (state) { @@ -86,7 +84,7 @@ public void stopWatcher() throws Exception { case "starting": throw new AssertionError("waiting until starting state reached started state to stop"); case "started": - Response stopResponse = adminClient().performRequest("POST", "/_xpack/watcher/_stop", emptyMap()); + Response stopResponse = adminClient().performRequest(new Request("POST", "/_xpack/watcher/_stop")); boolean isAcknowledged = ObjectPath.createFromResponse(stopResponse).evaluate("acknowledged"); assertThat(isAcknowledged, is(true)); break; @@ -112,12 +110,12 @@ public void testMonitorClusterHealth() throws Exception { String watchId = "cluster_health_watch"; // get master publish address - Response clusterStateResponse = adminClient().performRequest("GET", "_cluster/state"); + Response clusterStateResponse = adminClient().performRequest(new Request("GET", "/_cluster/state")); ObjectPath clusterState = ObjectPath.createFromResponse(clusterStateResponse); String masterNode = clusterState.evaluate("master_node"); assertThat(masterNode, is(notNullValue())); - Response statsResponse = adminClient().performRequest("GET", "_nodes"); + Response statsResponse = adminClient().performRequest(new Request("GET", "/_nodes")); ObjectPath stats = ObjectPath.createFromResponse(statsResponse); String address = stats.evaluate("nodes." + masterNode + ".http.publish_address"); assertThat(address, is(notNullValue())); @@ -163,16 +161,15 @@ public void testMonitorClusterHealth() throws Exception { } private void indexWatch(String watchId, XContentBuilder builder) throws Exception { - StringEntity entity = new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); - - Response response = client().performRequest("PUT", "_xpack/watcher/watch/" + watchId, emptyMap(), entity); - assertOK(response); + Request request = new Request("PUT", "/_xpack/watcher/watch/" + watchId); + request.setJsonEntity(Strings.toString(builder)); + Response response = client().performRequest(request); Map responseMap = entityAsMap(response); assertThat(responseMap, hasEntry("_id", watchId)); } private void deleteWatch(String watchId) throws IOException { - Response response = client().performRequest("DELETE", "_xpack/watcher/watch/" + watchId); + Response response = client().performRequest(new Request("DELETE", "/_xpack/watcher/watch/" + watchId)); assertOK(response); ObjectPath path = ObjectPath.createFromResponse(response); boolean found = path.evaluate("found"); @@ -182,7 +179,7 @@ private void deleteWatch(String watchId) throws IOException { private ObjectPath getWatchHistoryEntry(String watchId) throws Exception { final AtomicReference objectPathReference = new AtomicReference<>(); assertBusy(() -> { - client().performRequest("POST", ".watcher-history-*/_refresh"); + client().performRequest(new Request("POST", "/.watcher-history-*/_refresh")); try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); @@ -194,8 +191,9 @@ private ObjectPath getWatchHistoryEntry(String watchId) throws Exception { .endObject().endArray(); builder.endObject(); - StringEntity entity = new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); - Response response = client().performRequest("POST", ".watcher-history-*/_search", emptyMap(), entity); + Request searchRequest = new Request("POST", "/.watcher-history-*/_search"); + searchRequest.setJsonEntity(Strings.toString(builder)); + Response response = client().performRequest(searchRequest); ObjectPath objectPath = ObjectPath.createFromResponse(response); int totalHits = objectPath.evaluate("hits.total"); assertThat(totalHits, is(greaterThanOrEqualTo(1))); @@ -208,7 +206,7 @@ private ObjectPath getWatchHistoryEntry(String watchId) throws Exception { } private void assertWatchCount(int expectedWatches) throws IOException { - Response watcherStatsResponse = adminClient().performRequest("GET", "_xpack/watcher/stats"); + Response watcherStatsResponse = adminClient().performRequest(new Request("GET", "/_xpack/watcher/stats")); ObjectPath objectPath = ObjectPath.createFromResponse(watcherStatsResponse); int watchCount = objectPath.evaluate("stats.0.watch_count"); assertThat(watchCount, is(expectedWatches)); From 0cc99d270c6ad290efd600444cfa4dacda79c57d Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Wed, 22 Aug 2018 14:23:43 -0400 Subject: [PATCH 112/283] Switch ml basic tests to new style Requests (#32483) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. This changes all calls in the `x-pack/qa/ml-basic-multi-node` project to use the new versions. --- .../ml/integration/MlBasicMultiNodeIT.java | 292 +++++++++--------- 1 file changed, 143 insertions(+), 149 deletions(-) diff --git a/x-pack/plugin/ml/qa/basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java b/x-pack/plugin/ml/qa/basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java index e7381050260c..6e22e5b3f187 100644 --- a/x-pack/plugin/ml/qa/basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java +++ b/x-pack/plugin/ml/qa/basic-multi-node/src/test/java/org/elasticsearch/xpack/ml/integration/MlBasicMultiNodeIT.java @@ -6,12 +6,12 @@ package org.elasticsearch.xpack.ml.integration; import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; +import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xpack.ml.MachineLearning; @@ -22,18 +22,15 @@ import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; -import static org.elasticsearch.common.xcontent.XContentType.JSON; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; public class MlBasicMultiNodeIT extends ESRestTestCase { - @SuppressWarnings("unchecked") public void testMachineLearningInstalled() throws Exception { - Response response = client().performRequest("get", "/_xpack"); - assertEquals(200, response.getStatusLine().getStatusCode()); - Map features = (Map) responseEntityToMap(response).get("features"); - Map ml = (Map) features.get("ml"); + Response response = client().performRequest(new Request("GET", "/_xpack")); + Map features = (Map) entityAsMap(response).get("features"); + Map ml = (Map) features.get("ml"); assertNotNull(ml); assertTrue((Boolean) ml.get("available")); assertTrue((Boolean) ml.get("enabled")); @@ -55,18 +52,18 @@ public void testMiniFarequote() throws Exception { String jobId = "mini-farequote-job"; createFarequoteJob(jobId); - Response response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open"); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("opened", true), responseEntityToMap(response)); + Response openResponse = client().performRequest( + new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open")); + assertEquals(Collections.singletonMap("opened", true), entityAsMap(openResponse)); - String postData = + Request addData = new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_data"); + addData.setEntity(new NStringEntity( "{\"airline\":\"AAL\",\"responsetime\":\"132.2046\",\"sourcetype\":\"farequote\",\"time\":\"1403481600\"}\n" + - "{\"airline\":\"JZA\",\"responsetime\":\"990.4628\",\"sourcetype\":\"farequote\",\"time\":\"1403481700\"}"; - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_data", - Collections.emptyMap(), - new StringEntity(postData, randomFrom(ContentType.APPLICATION_JSON, ContentType.create("application/x-ndjson")))); - assertEquals(202, response.getStatusLine().getStatusCode()); - Map responseBody = responseEntityToMap(response); + "{\"airline\":\"JZA\",\"responsetime\":\"990.4628\",\"sourcetype\":\"farequote\",\"time\":\"1403481700\"}", + randomFrom(ContentType.APPLICATION_JSON, ContentType.create("application/x-ndjson")))); + Response addDataResponse = client().performRequest(addData); + assertEquals(202, addDataResponse.getStatusLine().getStatusCode()); + Map responseBody = entityAsMap(addDataResponse); assertEquals(2, responseBody.get("processed_record_count")); assertEquals(4, responseBody.get("processed_field_count")); assertEquals(177, responseBody.get("input_bytes")); @@ -78,20 +75,19 @@ public void testMiniFarequote() throws Exception { assertEquals(1403481600000L, responseBody.get("earliest_record_timestamp")); assertEquals(1403481700000L, responseBody.get("latest_record_timestamp")); - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_flush"); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertFlushResponse(response, true, 1403481600000L); + Response flushResponse = client().performRequest( + new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_flush")); + assertFlushResponse(flushResponse, true, 1403481600000L); - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_close", - Collections.singletonMap("timeout", "20s")); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("closed", true), responseEntityToMap(response)); + Request closeRequest = new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_close"); + closeRequest.addParameter("timeout", "20s"); + Response closeResponse = client().performRequest(closeRequest); + assertEquals(Collections.singletonMap("closed", true), entityAsMap(closeResponse)); - response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - assertEquals(200, response.getStatusLine().getStatusCode()); - @SuppressWarnings("unchecked") - Map dataCountsDoc = (Map) - ((Map)((List) responseEntityToMap(response).get("jobs")).get(0)).get("data_counts"); + Response statsResponse = client().performRequest( + new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + Map dataCountsDoc = (Map) + ((Map)((List) entityAsMap(statsResponse).get("jobs")).get(0)).get("data_counts"); assertEquals(2, dataCountsDoc.get("processed_record_count")); assertEquals(4, dataCountsDoc.get("processed_field_count")); assertEquals(177, dataCountsDoc.get("input_bytes")); @@ -103,12 +99,12 @@ public void testMiniFarequote() throws Exception { assertEquals(1403481600000L, dataCountsDoc.get("earliest_record_timestamp")); assertEquals(1403481700000L, dataCountsDoc.get("latest_record_timestamp")); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); - assertEquals(200, response.getStatusLine().getStatusCode()); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); } public void testMiniFarequoteWithDatafeeder() throws Exception { - String mappings = "{" + Request createAirlineDataRequest = new Request("PUT", "/airline-data"); + createAirlineDataRequest.setJsonEntity("{" + " \"mappings\": {" + " \"response\": {" + " \"properties\": {" @@ -118,40 +114,38 @@ public void testMiniFarequoteWithDatafeeder() throws Exception { + " }" + " }" + " }" - + "}"; - client().performRequest("put", "airline-data", Collections.emptyMap(), new StringEntity(mappings, ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data/response/1", Collections.emptyMap(), - new StringEntity("{\"time\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data/response/2", Collections.emptyMap(), - new StringEntity("{\"time\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}", - ContentType.APPLICATION_JSON)); + + "}"); + client().performRequest(createAirlineDataRequest); + Request airlineData1 = new Request("PUT", "/airline-data/response/1"); + airlineData1.setJsonEntity("{\"time\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}"); + client().performRequest(airlineData1); + Request airlineData2 = new Request("PUT", "/airline-data/response/2"); + airlineData2.setJsonEntity("{\"time\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}"); + client().performRequest(airlineData2); // Ensure all data is searchable - client().performRequest("post", "_refresh"); + client().performRequest(new Request("POST", "/_refresh")); String jobId = "mini-farequote-with-data-feeder-job"; createFarequoteJob(jobId); String datafeedId = "bar"; createDatafeed(datafeedId, jobId); - Response response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open"); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("opened", true), responseEntityToMap(response)); + Response openResponse = client().performRequest( + new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open")); + assertEquals(Collections.singletonMap("opened", true), entityAsMap(openResponse)); - response = client().performRequest("post", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start", - Collections.singletonMap("start", "0")); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("started", true), responseEntityToMap(response)); + Request startRequest = new Request("POST", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start"); + startRequest.addParameter("start", "0"); + Response startResponse = client().performRequest(startRequest); + assertEquals(Collections.singletonMap("started", true), entityAsMap(startResponse)); assertBusy(() -> { try { - Response statsResponse = - client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - assertEquals(200, statsResponse.getStatusLine().getStatusCode()); - @SuppressWarnings("unchecked") - Map dataCountsDoc = (Map) - ((Map)((List) responseEntityToMap(statsResponse).get("jobs")).get(0)).get("data_counts"); + Response statsResponse = client().performRequest( + new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + Map dataCountsDoc = (Map) + ((Map)((List) entityAsMap(statsResponse).get("jobs")).get(0)).get("data_counts"); assertEquals(2, dataCountsDoc.get("input_record_count")); assertEquals(2, dataCountsDoc.get("processed_record_count")); } catch (IOException e) { @@ -159,41 +153,38 @@ public void testMiniFarequoteWithDatafeeder() throws Exception { } }); - response = client().performRequest("post", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_stop"); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("stopped", true), responseEntityToMap(response)); + Response stopResponse = client().performRequest( + new Request("POST", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_stop")); + assertEquals(Collections.singletonMap("stopped", true), entityAsMap(stopResponse)); - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_close", - Collections.singletonMap("timeout", "20s")); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("closed", true), responseEntityToMap(response)); + Request closeRequest = new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_close"); + closeRequest.addParameter("timeout", "20s"); + assertEquals(Collections.singletonMap("closed", true), + entityAsMap(client().performRequest(closeRequest))); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId); - assertEquals(200, response.getStatusLine().getStatusCode()); - - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); - assertEquals(200, response.getStatusLine().getStatusCode()); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId)); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); } public void testMiniFarequoteReopen() throws Exception { String jobId = "mini-farequote-reopen"; createFarequoteJob(jobId); - Response response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open"); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("opened", true), responseEntityToMap(response)); + Response openResponse = client().performRequest( + new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open")); + assertEquals(Collections.singletonMap("opened", true), entityAsMap(openResponse)); - String postData = + Request addDataRequest = new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_data"); + addDataRequest.setEntity(new NStringEntity( "{\"airline\":\"AAL\",\"responsetime\":\"132.2046\",\"sourcetype\":\"farequote\",\"time\":\"1403481600\"}\n" + "{\"airline\":\"JZA\",\"responsetime\":\"990.4628\",\"sourcetype\":\"farequote\",\"time\":\"1403481700\"}\n" + "{\"airline\":\"JBU\",\"responsetime\":\"877.5927\",\"sourcetype\":\"farequote\",\"time\":\"1403481800\"}\n" + "{\"airline\":\"KLM\",\"responsetime\":\"1355.4812\",\"sourcetype\":\"farequote\",\"time\":\"1403481900\"}\n" + - "{\"airline\":\"NKS\",\"responsetime\":\"9991.3981\",\"sourcetype\":\"farequote\",\"time\":\"1403482000\"}"; - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_data", - Collections.emptyMap(), - new StringEntity(postData, randomFrom(ContentType.APPLICATION_JSON, ContentType.create("application/x-ndjson")))); - assertEquals(202, response.getStatusLine().getStatusCode()); - Map responseBody = responseEntityToMap(response); + "{\"airline\":\"NKS\",\"responsetime\":\"9991.3981\",\"sourcetype\":\"farequote\",\"time\":\"1403482000\"}", + randomFrom(ContentType.APPLICATION_JSON, ContentType.create("application/x-ndjson")))); + Response addDataResponse = client().performRequest(addDataRequest); + assertEquals(202, addDataResponse.getStatusLine().getStatusCode()); + Map responseBody = entityAsMap(addDataResponse); assertEquals(5, responseBody.get("processed_record_count")); assertEquals(10, responseBody.get("processed_field_count")); assertEquals(446, responseBody.get("input_bytes")); @@ -205,60 +196,56 @@ public void testMiniFarequoteReopen() throws Exception { assertEquals(1403481600000L, responseBody.get("earliest_record_timestamp")); assertEquals(1403482000000L, responseBody.get("latest_record_timestamp")); - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_flush"); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertFlushResponse(response, true, 1403481600000L); + Response flushResponse = client().performRequest( + new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_flush")); + assertFlushResponse(flushResponse, true, 1403481600000L); + + Request closeRequest = new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_close"); + closeRequest.addParameter("timeout", "20s"); + assertEquals(Collections.singletonMap("closed", true), + entityAsMap(client().performRequest(closeRequest))); - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_close", - Collections.singletonMap("timeout", "20s")); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("closed", true), responseEntityToMap(response)); + Request statsRequest = new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); + client().performRequest(statsRequest); - response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - assertEquals(200, response.getStatusLine().getStatusCode()); - - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open", - Collections.singletonMap("timeout", "20s")); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("opened", true), responseEntityToMap(response)); + Request openRequest = new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open"); + openRequest.addParameter("timeout", "20s"); + Response openResponse2 = client().performRequest(openRequest); + assertEquals(Collections.singletonMap("opened", true), entityAsMap(openResponse2)); // feed some more data points - postData = + Request addDataRequest2 = new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_data"); + addDataRequest2.setEntity(new NStringEntity( "{\"airline\":\"AAL\",\"responsetime\":\"136.2361\",\"sourcetype\":\"farequote\",\"time\":\"1407081600\"}\n" + "{\"airline\":\"VRD\",\"responsetime\":\"282.9847\",\"sourcetype\":\"farequote\",\"time\":\"1407081700\"}\n" + "{\"airline\":\"JAL\",\"responsetime\":\"493.0338\",\"sourcetype\":\"farequote\",\"time\":\"1407081800\"}\n" + "{\"airline\":\"UAL\",\"responsetime\":\"8.4275\",\"sourcetype\":\"farequote\",\"time\":\"1407081900\"}\n" + - "{\"airline\":\"FFT\",\"responsetime\":\"221.8693\",\"sourcetype\":\"farequote\",\"time\":\"1407082000\"}"; - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_data", - Collections.emptyMap(), - new StringEntity(postData, randomFrom(ContentType.APPLICATION_JSON, ContentType.create("application/x-ndjson")))); - assertEquals(202, response.getStatusLine().getStatusCode()); - responseBody = responseEntityToMap(response); - assertEquals(5, responseBody.get("processed_record_count")); - assertEquals(10, responseBody.get("processed_field_count")); - assertEquals(442, responseBody.get("input_bytes")); - assertEquals(15, responseBody.get("input_field_count")); - assertEquals(0, responseBody.get("invalid_date_count")); - assertEquals(0, responseBody.get("missing_field_count")); - assertEquals(0, responseBody.get("out_of_order_timestamp_count")); - assertEquals(1000, responseBody.get("bucket_count")); - + "{\"airline\":\"FFT\",\"responsetime\":\"221.8693\",\"sourcetype\":\"farequote\",\"time\":\"1407082000\"}", + randomFrom(ContentType.APPLICATION_JSON, ContentType.create("application/x-ndjson")))); + Response addDataResponse2 = client().performRequest(addDataRequest2); + assertEquals(202, addDataResponse2.getStatusLine().getStatusCode()); + Map responseBody2 = entityAsMap(addDataResponse2); + assertEquals(5, responseBody2.get("processed_record_count")); + assertEquals(10, responseBody2.get("processed_field_count")); + assertEquals(442, responseBody2.get("input_bytes")); + assertEquals(15, responseBody2.get("input_field_count")); + assertEquals(0, responseBody2.get("invalid_date_count")); + assertEquals(0, responseBody2.get("missing_field_count")); + assertEquals(0, responseBody2.get("out_of_order_timestamp_count")); + assertEquals(1000, responseBody2.get("bucket_count")); + // unintuitive: should return the earliest record timestamp of this feed??? - assertEquals(null, responseBody.get("earliest_record_timestamp")); - assertEquals(1407082000000L, responseBody.get("latest_record_timestamp")); - - response = client().performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_close", - Collections.singletonMap("timeout", "20s")); - assertEquals(200, response.getStatusLine().getStatusCode()); - assertEquals(Collections.singletonMap("closed", true), responseEntityToMap(response)); + assertEquals(null, responseBody2.get("earliest_record_timestamp")); + assertEquals(1407082000000L, responseBody2.get("latest_record_timestamp")); + + assertEquals(Collections.singletonMap("closed", true), + entityAsMap(client().performRequest(closeRequest))); // counts should be summed up - response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - assertEquals(200, response.getStatusLine().getStatusCode()); - - @SuppressWarnings("unchecked") - Map dataCountsDoc = (Map) - ((Map)((List) responseEntityToMap(response).get("jobs")).get(0)).get("data_counts"); + Response statsResponse = client().performRequest(statsRequest); + + Map dataCountsDoc = (Map) + ((Map)((List) entityAsMap(statsResponse).get("jobs")).get(0)).get("data_counts"); assertEquals(10, dataCountsDoc.get("processed_record_count")); assertEquals(20, dataCountsDoc.get("processed_field_count")); assertEquals(888, dataCountsDoc.get("input_bytes")); @@ -270,8 +257,7 @@ public void testMiniFarequoteReopen() throws Exception { assertEquals(1403481600000L, dataCountsDoc.get("earliest_record_timestamp")); assertEquals(1407082000000L, dataCountsDoc.get("latest_record_timestamp")); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); - assertEquals(200, response.getStatusLine().getStatusCode()); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); } private Response createDatafeed(String datafeedId, String jobId) throws Exception { @@ -282,45 +268,53 @@ private Response createDatafeed(String datafeedId, String jobId) throws Exceptio xContentBuilder.array("types", "response"); xContentBuilder.field("_source", true); xContentBuilder.endObject(); - return client().performRequest("put", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId, - Collections.emptyMap(), new StringEntity(Strings.toString(xContentBuilder), ContentType.APPLICATION_JSON)); + Request request = new Request("PUT", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId); + request.setJsonEntity(Strings.toString(xContentBuilder)); + return client().performRequest(request); } private Response createFarequoteJob(String jobId) throws Exception { XContentBuilder xContentBuilder = jsonBuilder(); xContentBuilder.startObject(); - xContentBuilder.field("job_id", jobId); - xContentBuilder.field("description", "Analysis of response time by airline"); - - xContentBuilder.startObject("analysis_config"); - xContentBuilder.field("bucket_span", "3600s"); - xContentBuilder.startArray("detectors"); - xContentBuilder.startObject(); - xContentBuilder.field("function", "metric"); - xContentBuilder.field("field_name", "responsetime"); - xContentBuilder.field("by_field_name", "airline"); - xContentBuilder.endObject(); - xContentBuilder.endArray(); - xContentBuilder.endObject(); + { + xContentBuilder.field("job_id", jobId); + xContentBuilder.field("description", "Analysis of response time by airline"); + + xContentBuilder.startObject("analysis_config"); + { + xContentBuilder.field("bucket_span", "3600s"); + xContentBuilder.startArray("detectors"); + { + xContentBuilder.startObject(); + { + xContentBuilder.field("function", "metric"); + xContentBuilder.field("field_name", "responsetime"); + xContentBuilder.field("by_field_name", "airline"); + } + xContentBuilder.endObject(); + } + xContentBuilder.endArray(); + } + xContentBuilder.endObject(); - xContentBuilder.startObject("data_description"); - xContentBuilder.field("format", "xcontent"); - xContentBuilder.field("time_field", "time"); - xContentBuilder.field("time_format", "epoch"); - xContentBuilder.endObject(); + xContentBuilder.startObject("data_description"); + { + xContentBuilder.field("format", "xcontent"); + xContentBuilder.field("time_field", "time"); + xContentBuilder.field("time_format", "epoch"); + } + xContentBuilder.endObject(); + } xContentBuilder.endObject(); - return client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + URLEncoder.encode(jobId, "UTF-8"), - Collections.emptyMap(), new StringEntity(Strings.toString(xContentBuilder), ContentType.APPLICATION_JSON)); - } - - private static Map responseEntityToMap(Response response) throws IOException { - return XContentHelper.convertToMap(JSON.xContent(), response.getEntity().getContent(), false); + Request request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + URLEncoder.encode(jobId, "UTF-8")); + request.setJsonEntity(Strings.toString(xContentBuilder)); + return client().performRequest(request); } private static void assertFlushResponse(Response response, boolean expectedFlushed, long expectedLastFinalizedBucketEnd) throws IOException { - Map asMap = responseEntityToMap(response); + Map asMap = entityAsMap(response); assertThat(asMap.size(), equalTo(2)); assertThat(asMap.get("flushed"), is(true)); assertThat(asMap.get("last_finalized_bucket_end"), equalTo(expectedLastFinalizedBucketEnd)); From edd477a15e07f021f40e496379df09c4e2e792c1 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Wed, 22 Aug 2018 21:53:42 +0300 Subject: [PATCH 113/283] Fix the default pom file name (#33063) Before this change the default was fixed at compile time and not picking up changes in the build script. --- .../groovy/org/elasticsearch/gradle/BuildPlugin.groovy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 7713d5c64dfc..fb979a77dace 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -531,12 +531,16 @@ class BuildPlugin implements Plugin { project.tasks.withType(GenerateMavenPom.class) { GenerateMavenPom generatePOMTask -> // The GenerateMavenPom task is aggressive about setting the destination, instead of fighting it, // just make a copy. - generatePOMTask.ext.pomFileName = "${project.archivesBaseName}-${project.version}.pom" + generatePOMTask.ext.pomFileName = null doLast { project.copy { from generatePOMTask.destination into "${project.buildDir}/distributions" - rename { generatePOMTask.ext.pomFileName } + rename { + generatePOMTask.ext.pomFileName == null ? + "${project.archivesBaseName}-${project.version}.pom" : + generatePOMTask.ext.pomFileName + } } } // build poms with assemble (if the assemble task exists) From de95dead2dae32ffcdf7cc22e1c37ea4896d1f5a Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Thu, 23 Aug 2018 00:21:38 +0300 Subject: [PATCH 114/283] SQL: skip uppercasing/lowercasing function tests for AZ locales as well (#32910) * Added the rest of the Locales that have different behavior for uppercasing/lowercasing scenarios to the skip list --- .../string/StringFunctionProcessorTests.java | 51 +++++++++++++------ .../xpack/qa/sql/jdbc/SqlSpecTestCase.java | 11 ++-- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionProcessorTests.java index a4d9d4cb57ab..d3336ec89a84 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionProcessorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringFunctionProcessorTests.java @@ -75,17 +75,27 @@ public void testLCase() { stringCharInputValidation(proc); } - public void testLCaseWithTRLocale() { + public void testLCaseWithAZTRLocale() { + Locale initialLocale = Locale.getDefault(); Locale.setDefault(Locale.forLanguageTag("tr")); - StringProcessor proc = new StringProcessor(StringOperation.LCASE); - // ES-SQL is not locale sensitive (so far). The obvious test for this is the Turkish language, uppercase letter I conversion - // in non-Turkish locale the lowercasing would create i and an additional dot, while in Turkish Locale it would only create "i" - // unicode 0069 = i - assertEquals("\u0069\u0307", proc.process("\u0130")); - // unicode 0049 = I (regular capital letter i) - // in Turkish locale this would be lowercased to a "i" without dot (unicode 0131) - assertEquals("\u0069", proc.process("\u0049")); + try { + StringProcessor proc = new StringProcessor(StringOperation.LCASE); + // ES-SQL is not locale sensitive (so far). The obvious test for this is the Turkish language, uppercase letter I conversion + // in non-Turkish locale the lowercasing would create i and an additional dot, while in Turkish Locale it would only create "i" + // unicode 0069 = i + assertEquals("\u0069\u0307", proc.process("\u0130")); + // unicode 0049 = I (regular capital letter i) + // in Turkish locale this would be lowercased to a "i" without dot (unicode 0131) + assertEquals("\u0069", proc.process("\u0049")); + + Locale.setDefault(Locale.forLanguageTag("az")); + assertEquals("\u0069\u0307", proc.process("\u0130")); + assertEquals("\u0069", proc.process("\u0049")); + } finally { + // restore the original Locale + Locale.setDefault(initialLocale); + } } public void testUCase() { @@ -102,13 +112,22 @@ public void testUCase() { stringCharInputValidation(proc); } - public void testUCaseWithTRLocale() { + public void testUCaseWithAZTRLocale() { + Locale initialLocale = Locale.getDefault(); Locale.setDefault(Locale.forLanguageTag("tr")); - StringProcessor proc = new StringProcessor(StringOperation.UCASE); - - // ES-SQL is not Locale sensitive (so far). - // in Turkish locale, small letter "i" is uppercased to "I" with a dot above (unicode 130), otherwise in "i" (unicode 49) - assertEquals("\u0049", proc.process("\u0069")); + + try { + StringProcessor proc = new StringProcessor(StringOperation.UCASE); + // ES-SQL is not Locale sensitive (so far). + // in Turkish locale, small letter "i" is uppercased to "I" with a dot above (unicode 130), otherwise in "i" (unicode 49) + assertEquals("\u0049", proc.process("\u0069")); + + Locale.setDefault(Locale.forLanguageTag("az")); + assertEquals("\u0049", proc.process("\u0069")); + } finally { + // restore the original Locale + Locale.setDefault(initialLocale); + } } public void testLength() { @@ -179,7 +198,7 @@ public void testCharLength() { assertEquals(7, proc.process("foo bar")); assertEquals(0, proc.process("")); assertEquals(1, proc.process('f')); - assertEquals(1, proc.process('€')); + assertEquals(1, proc.process('\u20ac')); // euro symbol stringCharInputValidation(proc); } diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java index d61b4b9a946b..4d90c9cce502 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java @@ -13,6 +13,7 @@ import java.sql.Connection; import java.sql.ResultSet; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -37,8 +38,7 @@ public static List readScriptSpec() throws Exception { tests.addAll(readScriptSpec("/agg.sql-spec", parser)); tests.addAll(readScriptSpec("/arithmetic.sql-spec", parser)); tests.addAll(readScriptSpec("/string-functions.sql-spec", parser)); - // AwaitsFix: https://github.com/elastic/elasticsearch/issues/32589 - // tests.addAll(readScriptSpec("/case-functions.sql-spec", parser)); + tests.addAll(readScriptSpec("/case-functions.sql-spec", parser)); return tests; } @@ -60,8 +60,11 @@ public SqlSpecTestCase(String fileName, String groupName, String testName, Integ @Override protected final void doTest() throws Throwable { - boolean goodLocale = !(Locale.getDefault().equals(new Locale.Builder().setLanguageTag("tr").build()) - || Locale.getDefault().equals(new Locale.Builder().setLanguageTag("tr-TR").build())); + // we skip the tests in case of these locales because ES-SQL is Locale-insensitive for now + // while H2 does take the Locale into consideration + String[] h2IncompatibleLocales = new String[] {"tr", "az", "tr-TR", "tr-CY", "az-Latn", "az-Cyrl", "az-Latn-AZ", "az-Cyrl-AZ"}; + boolean goodLocale = !Arrays.stream(h2IncompatibleLocales) + .anyMatch((l) -> Locale.getDefault().equals(new Locale.Builder().setLanguageTag(l).build())); if (fileName.startsWith("case-functions")) { Assume.assumeTrue(goodLocale); } From 46247ff1f9788ca46958833fbb15e7afcc8f6f4e Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 23 Aug 2018 07:43:36 +0200 Subject: [PATCH 115/283] INGEST: Cleanup Redundant Put Method (#33034) --- .../elasticsearch/ingest/IngestService.java | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index 4cc4fb69a54f..ae3416ef3b06 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -207,7 +207,21 @@ static List innerGetPipelines(IngestMetadata ingestMetada */ public void putPipeline(Map ingestInfos, PutPipelineRequest request, ActionListener listener) throws Exception { - put(clusterService, ingestInfos, request, listener); + // validates the pipeline and processor configuration before submitting a cluster update task: + validatePipeline(ingestInfos, request); + clusterService.submitStateUpdateTask("put-pipeline-" + request.getId(), + new AckedClusterStateUpdateTask(request, listener) { + + @Override + protected AcknowledgedResponse newResponse(boolean acknowledged) { + return new AcknowledgedResponse(acknowledged); + } + + @Override + public ClusterState execute(ClusterState currentState) { + return innerPut(request, currentState); + } + }); } /** @@ -280,28 +294,6 @@ static ClusterState innerPut(PutPipelineRequest request, ClusterState currentSta return newState.build(); } - /** - * Stores the specified pipeline definition in the request. - */ - public void put(ClusterService clusterService, Map ingestInfos, PutPipelineRequest request, - ActionListener listener) throws Exception { - // validates the pipeline and processor configuration before submitting a cluster update task: - validatePipeline(ingestInfos, request); - clusterService.submitStateUpdateTask("put-pipeline-" + request.getId(), - new AckedClusterStateUpdateTask(request, listener) { - - @Override - protected AcknowledgedResponse newResponse(boolean acknowledged) { - return new AcknowledgedResponse(acknowledged); - } - - @Override - public ClusterState execute(ClusterState currentState) { - return innerPut(request, currentState); - } - }); - } - void validatePipeline(Map ingestInfos, PutPipelineRequest request) throws Exception { if (ingestInfos.isEmpty()) { throw new IllegalStateException("Ingest info is empty"); From ffe895e16e38917f88789443ed975ad1fc35056c Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Thu, 23 Aug 2018 09:52:48 +0200 Subject: [PATCH 116/283] Change query field expansion (#33020) This commit changes the query field expansion for query parsers to not rely on an hardcoded list of field types. Instead we rely on the type of exception that is thrown by MappedFieldType#termQuery to include/exclude an expanded field. Supersedes #31655 Closes #31798 --- .../index/mapper/MappedFieldType.java | 9 ++- .../index/search/QueryParserHelper.java | 72 ++++--------------- .../search/query/QueryStringIT.java | 4 +- .../search/query/all-query-index.json | 8 +-- 4 files changed, 29 insertions(+), 64 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 5f3f4a4de49d..4a3fa852e7f7 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java @@ -35,6 +35,7 @@ import org.apache.lucene.search.TermInSetQuery; import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.joda.DateMathParser; @@ -314,7 +315,13 @@ public boolean isAggregatable() { /** Generates a query that will only match documents that contain the given value. * The default implementation returns a {@link TermQuery} over the value bytes, * boosted by {@link #boost()}. - * @throws IllegalArgumentException if {@code value} cannot be converted to the expected data type */ + * @throws IllegalArgumentException if {@code value} cannot be converted to the expected data type or if the field is not searchable + * due to the way it is configured (eg. not indexed) + * @throws ElasticsearchParseException if {@code value} cannot be converted to the expected data type + * @throws UnsupportedOperationException if the field is not searchable regardless of options + * @throws QueryShardException if the field is not searchable regardless of options + */ + // TODO: Standardize exception types public abstract Query termQuery(Object value, @Nullable QueryShardContext context); /** Build a constant-scoring query that matches all values. The default implementation uses a diff --git a/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java b/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java index df96ff87ec25..d3bac583eac6 100644 --- a/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java +++ b/server/src/main/java/org/elasticsearch/index/search/QueryParserHelper.java @@ -19,47 +19,21 @@ package org.elasticsearch.index.search; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.regex.Regex; -import org.elasticsearch.index.mapper.DateFieldMapper; -import org.elasticsearch.index.mapper.DocumentMapper; -import org.elasticsearch.index.mapper.FieldMapper; -import org.elasticsearch.index.mapper.IpFieldMapper; -import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; -import org.elasticsearch.index.mapper.Mapper; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.mapper.MetadataFieldMapper; -import org.elasticsearch.index.mapper.NumberFieldMapper; -import org.elasticsearch.index.mapper.TextFieldMapper; import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.query.QueryShardException; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; /** * Helpers to extract and expand field names and boosts */ public final class QueryParserHelper { - // Mapping types the "all-ish" query can be executed against - // TODO: Fix the API so that we don't need a hardcoded list of types - private static final Set ALLOWED_QUERY_MAPPER_TYPES; - - static { - ALLOWED_QUERY_MAPPER_TYPES = new HashSet<>(); - ALLOWED_QUERY_MAPPER_TYPES.add(DateFieldMapper.CONTENT_TYPE); - ALLOWED_QUERY_MAPPER_TYPES.add(IpFieldMapper.CONTENT_TYPE); - ALLOWED_QUERY_MAPPER_TYPES.add(KeywordFieldMapper.CONTENT_TYPE); - for (NumberFieldMapper.NumberType nt : NumberFieldMapper.NumberType.values()) { - ALLOWED_QUERY_MAPPER_TYPES.add(nt.typeName()); - } - ALLOWED_QUERY_MAPPER_TYPES.add("scaled_float"); - ALLOWED_QUERY_MAPPER_TYPES.add(TextFieldMapper.CONTENT_TYPE); - } - private QueryParserHelper() {} /** @@ -85,22 +59,6 @@ public static Map parseFieldsAndWeights(List fields) { return fieldsAndWeights; } - /** - * Get a {@link FieldMapper} associated with a field name or null. - * @param mapperService The mapper service where to find the mapping. - * @param field The field name to search. - */ - public static Mapper getFieldMapper(MapperService mapperService, String field) { - DocumentMapper mapper = mapperService.documentMapper(); - if (mapper != null) { - Mapper fieldMapper = mapper.mappers().getMapper(field); - if (fieldMapper != null) { - return fieldMapper; - } - } - return null; - } - public static Map resolveMappingFields(QueryShardContext context, Map fieldsAndWeights) { return resolveMappingFields(context, fieldsAndWeights, null); @@ -138,8 +96,7 @@ public static Map resolveMappingFields(QueryShardContext context, * @param fieldOrPattern The field name or the pattern to resolve * @param weight The weight for the field * @param acceptAllTypes Whether all field type should be added when a pattern is expanded. - * If false, only {@link #ALLOWED_QUERY_MAPPER_TYPES} are accepted and other field types - * are discarded from the query. + * If false, only searchable field types are added. * @param acceptMetadataField Whether metadata fields should be added when a pattern is expanded. */ public static Map resolveMappingField(QueryShardContext context, String fieldOrPattern, float weight, @@ -154,8 +111,7 @@ public static Map resolveMappingField(QueryShardContext context, * @param fieldOrPattern The field name or the pattern to resolve * @param weight The weight for the field * @param acceptAllTypes Whether all field type should be added when a pattern is expanded. - * If false, only {@link #ALLOWED_QUERY_MAPPER_TYPES} are accepted and other field types - * are discarded from the query. + * If false, only searchable field types are added. * @param acceptMetadataField Whether metadata fields should be added when a pattern is expanded. * @param fieldSuffix The suffix name to add to the expanded field names if a mapping exists for that name. * The original name of the field is kept if adding the suffix to the field name does not point to a valid field @@ -177,18 +133,20 @@ public static Map resolveMappingField(QueryShardContext context, continue; } - // Ignore fields that are not in the allowed mapper types. Some - // types do not support term queries, and thus we cannot generate - // a special query for them. - String mappingType = fieldType.typeName(); - if (acceptAllTypes == false && ALLOWED_QUERY_MAPPER_TYPES.contains(mappingType) == false) { + if (acceptMetadataField == false && fieldType.name().startsWith("_")) { + // Ignore metadata fields continue; } - // Ignore metadata fields. - Mapper mapper = getFieldMapper(context.getMapperService(), fieldName); - if (acceptMetadataField == false && mapper instanceof MetadataFieldMapper) { - continue; + if (acceptAllTypes == false) { + try { + fieldType.termQuery("", context); + } catch (QueryShardException |UnsupportedOperationException e) { + // field type is never searchable with term queries (eg. geo point): ignore + continue; + } catch (IllegalArgumentException |ElasticsearchParseException e) { + // other exceptions are parsing errors or not indexed fields: keep + } } fields.put(fieldName, weight); } diff --git a/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java b/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java index 5caab8c9dfec..a90e98a38eef 100644 --- a/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/QueryStringIT.java @@ -430,8 +430,8 @@ public void testFieldAliasOnDisallowedFieldType() throws Exception { indexRequests.add(client().prepareIndex("test", "_doc", "1").setSource("f3", "text", "f2", "one")); indexRandom(true, false, indexRequests); - // The wildcard field matches aliases for both a text and boolean field. - // By default, the boolean field should be ignored when building the query. + // The wildcard field matches aliases for both a text and geo_point field. + // By default, the geo_point field should be ignored when building the query. SearchResponse response = client().prepareSearch("test") .setQuery(queryStringQuery("text").field("f*_alias")) .execute().actionGet(); diff --git a/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json b/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json index abdc11928229..9ab8995813e3 100644 --- a/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json +++ b/server/src/test/resources/org/elasticsearch/search/query/all-query-index.json @@ -46,10 +46,6 @@ "format": "yyyy/MM/dd||epoch_millis" }, "f_bool": {"type": "boolean"}, - "f_bool_alias": { - "type": "alias", - "path": "f_bool" - }, "f_byte": {"type": "byte"}, "f_short": {"type": "short"}, "f_int": {"type": "integer"}, @@ -60,6 +56,10 @@ "f_binary": {"type": "binary"}, "f_suggest": {"type": "completion"}, "f_geop": {"type": "geo_point"}, + "f_geop_alias": { + "type": "alias", + "path": "f_geop" + }, "f_geos": {"type": "geo_shape"} } } From d7219c05a27ebdee7f0a56ed52d5f625bfb11f5a Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 23 Aug 2018 10:04:00 +0200 Subject: [PATCH 117/283] Search: Support of wildcard on docvalue_fields (#32980) * Search: Support of wildcard on docvalue_fields For consistency with stored_fields, docvalue_fields should support the use of wildcards. Documentation of doc values fields is updated accordingly. See also: #26390 Closes #26299 --- .../search/request/docvalue-fields.asciidoc | 21 +++++ .../elasticsearch/search/SearchService.java | 19 ++-- .../search/fields/SearchFieldsIT.java | 90 +++++++++++++++++++ 3 files changed, 125 insertions(+), 5 deletions(-) diff --git a/docs/reference/search/request/docvalue-fields.asciidoc b/docs/reference/search/request/docvalue-fields.asciidoc index fa5baf1db226..bcfcb20d1d53 100644 --- a/docs/reference/search/request/docvalue-fields.asciidoc +++ b/docs/reference/search/request/docvalue-fields.asciidoc @@ -30,6 +30,27 @@ GET /_search Doc value fields can work on fields that are not stored. +`*` can be used as a wild card, for example: + +[source,js] +-------------------------------------------------- +GET /_search +{ + "query" : { + "match_all": {} + }, + "docvalue_fields" : [ + { + "field": "*field", <1> + "format": "use_field_mapping" <2> + } + ] +} +-------------------------------------------------- +// CONSOLE +<1> Match all fields ending with `field` +<2> Format to be applied to all matching fields. + Note that if the fields parameter specifies fields without docvalues it will try to load the value from the fielddata cache causing the terms for that field to be loaded to memory (cached), which will result in more memory consumption. diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index 4bf5e03b8a7c..a7db2c55fe14 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -98,6 +98,8 @@ import org.elasticsearch.transport.TransportRequest; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -789,14 +791,21 @@ private void parseSource(DefaultSearchContext context, SearchSourceBuilder sourc context.fetchSourceContext(source.fetchSource()); } if (source.docValueFields() != null) { + List docValueFields = new ArrayList<>(); + for (DocValueFieldsContext.FieldAndFormat format : source.docValueFields()) { + Collection fieldNames = context.mapperService().simpleMatchToFullName(format.field); + for (String fieldName: fieldNames) { + docValueFields.add(new DocValueFieldsContext.FieldAndFormat(fieldName, format.format)); + } + } int maxAllowedDocvalueFields = context.mapperService().getIndexSettings().getMaxDocvalueFields(); - if (source.docValueFields().size() > maxAllowedDocvalueFields) { + if (docValueFields.size() > maxAllowedDocvalueFields) { throw new IllegalArgumentException( - "Trying to retrieve too many docvalue_fields. Must be less than or equal to: [" + maxAllowedDocvalueFields - + "] but was [" + source.docValueFields().size() + "]. This limit can be set by changing the [" - + IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.getKey() + "] index level setting."); + "Trying to retrieve too many docvalue_fields. Must be less than or equal to: [" + maxAllowedDocvalueFields + + "] but was [" + docValueFields.size() + "]. This limit can be set by changing the [" + + IndexSettings.MAX_DOCVALUE_FIELDS_SEARCH_SETTING.getKey() + "] index level setting."); } - context.docValueFieldsContext(new DocValueFieldsContext(source.docValueFields())); + context.docValueFieldsContext(new DocValueFieldsContext(docValueFields)); } if (source.highlighter() != null) { HighlightBuilder highlightBuilder = source.highlighter(); diff --git a/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java b/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java index aea0243a399d..45b6340ba6f4 100644 --- a/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java +++ b/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java @@ -810,6 +810,32 @@ public void testDocValueFields() throws Exception { equalTo(new BytesRef(new byte[] {42, 100}))); assertThat(searchResponse.getHits().getAt(0).getFields().get("ip_field").getValue(), equalTo("::1")); + builder = client().prepareSearch().setQuery(matchAllQuery()) + .addDocValueField("*field"); + searchResponse = builder.execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + fields = new HashSet<>(searchResponse.getHits().getAt(0).getFields().keySet()); + assertThat(fields, equalTo(newHashSet("byte_field", "short_field", "integer_field", "long_field", + "float_field", "double_field", "date_field", "boolean_field", "text_field", "keyword_field", + "binary_field", "ip_field"))); + + assertThat(searchResponse.getHits().getAt(0).getFields().get("byte_field").getValue().toString(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("short_field").getValue().toString(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("integer_field").getValue(), equalTo((Object) 3L)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("long_field").getValue(), equalTo((Object) 4L)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("float_field").getValue(), equalTo((Object) 5.0)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("double_field").getValue(), equalTo((Object) 6.0d)); + dateField = searchResponse.getHits().getAt(0).getFields().get("date_field").getValue(); + assertThat(dateField.toInstant().toEpochMilli(), equalTo(date.toInstant().toEpochMilli())); + assertThat(searchResponse.getHits().getAt(0).getFields().get("boolean_field").getValue(), equalTo((Object) true)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("text_field").getValue(), equalTo("foo")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("keyword_field").getValue(), equalTo("foo")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("binary_field").getValue(), + equalTo(new BytesRef(new byte[] {42, 100}))); + assertThat(searchResponse.getHits().getAt(0).getFields().get("ip_field").getValue(), equalTo("::1")); + builder = client().prepareSearch().setQuery(matchAllQuery()) .addDocValueField("text_field", "use_field_mapping") .addDocValueField("keyword_field", "use_field_mapping") @@ -977,6 +1003,70 @@ public void testDocValueFieldsWithFieldAlias() throws Exception { assertThat(fetchedDate, equalTo(date)); } + public void testWildcardDocValueFieldsWithFieldAlias() throws Exception { + XContentBuilder mapping = XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("_source") + .field("enabled", false) + .endObject() + .startObject("properties") + .startObject("text_field") + .field("type", "text") + .field("fielddata", true) + .endObject() + .startObject("date_field") + .field("type", "date") + .field("format", "yyyy-MM-dd") + .endObject() + .startObject("text_field_alias") + .field("type", "alias") + .field("path", "text_field") + .endObject() + .startObject("date_field_alias") + .field("type", "alias") + .field("path", "date_field") + .endObject() + .endObject() + .endObject() + .endObject(); + assertAcked(prepareCreate("test").addMapping("type", mapping)); + ensureGreen("test"); + + ZonedDateTime date = ZonedDateTime.of(1990, 12, 29, 0, 0, 0, 0, ZoneOffset.UTC); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ROOT); + + index("test", "type", "1", "text_field", "foo", "date_field", formatter.format(date)); + refresh("test"); + + SearchRequestBuilder builder = client().prepareSearch().setQuery(matchAllQuery()) + .addDocValueField("*alias", "use_field_mapping") + .addDocValueField("date_field"); + SearchResponse searchResponse = builder.execute().actionGet(); + + assertNoFailures(searchResponse); + assertHitCount(searchResponse, 1); + SearchHit hit = searchResponse.getHits().getAt(0); + + Map fields = hit.getFields(); + assertThat(fields.keySet(), equalTo(newHashSet("text_field_alias", "date_field_alias", "date_field"))); + + DocumentField textFieldAlias = fields.get("text_field_alias"); + assertThat(textFieldAlias.getName(), equalTo("text_field_alias")); + assertThat(textFieldAlias.getValue(), equalTo("foo")); + + DocumentField dateFieldAlias = fields.get("date_field_alias"); + assertThat(dateFieldAlias.getName(), equalTo("date_field_alias")); + assertThat(dateFieldAlias.getValue(), + equalTo("1990-12-29")); + + DocumentField dateField = fields.get("date_field"); + assertThat(dateField.getName(), equalTo("date_field")); + + ZonedDateTime fetchedDate = dateField.getValue(); + assertThat(fetchedDate, equalTo(date)); + } + public void testStoredFieldsWithFieldAlias() throws Exception { XContentBuilder mapping = XContentFactory.jsonBuilder() From f84ed14294db30ec2fb068438f5a9082ef0302f1 Mon Sep 17 00:00:00 2001 From: lipsill <39668292+lipsill@users.noreply.github.com> Date: Thu, 23 Aug 2018 10:07:59 +0200 Subject: [PATCH 118/283] Watcher: Improve error messages for CronEvalTool (#32800) CronEvalTool prints an error only for cron expressions that result in no upcoming time events. If a cron expression results in less than the specified count (default 10) time events, now all the coming times are printed without displaying error message. Closes #32735 --- .../trigger/schedule/tool/CronEvalTool.java | 10 +++++-- .../schedule/tool/CronEvalToolTests.java | 30 ++++++++++++++++++- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java index 33b1217895dc..d22d402aa157 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalTool.java @@ -61,14 +61,18 @@ void execute(Terminal terminal, String expression, int count) throws Exception { Cron cron = new Cron(expression); long time = date.getMillis(); + for (int i = 0; i < count; i++) { long prevTime = time; time = cron.getNextValidTimeAfter(time); if (time < 0) { - throw new UserException(ExitCodes.OK, (i + 1) + ".\t Could not compute future times since [" - + formatter.print(prevTime) + "] " + "(perhaps the cron expression only points to times in the past?)"); + if (i == 0) { + throw new UserException(ExitCodes.OK, "Could not compute future times since [" + + formatter.print(prevTime) + "] " + "(perhaps the cron expression only points to times in the past?)"); + } + break; } - terminal.println((i+1) + ".\t" + formatter.print(time)); + terminal.println((i + 1) + ".\t" + formatter.print(time)); } } } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalToolTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalToolTests.java index 223884249481..f1e864d547c8 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalToolTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/tool/CronEvalToolTests.java @@ -8,6 +8,13 @@ import org.elasticsearch.cli.Command; import org.elasticsearch.cli.CommandTestCase; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + public class CronEvalToolTests extends CommandTestCase { @Override protected Command newCommand() { @@ -18,6 +25,27 @@ public void testParse() throws Exception { String countOption = randomBoolean() ? "-c" : "--count"; int count = randomIntBetween(1, 100); String output = execute(countOption, Integer.toString(count), "0 0 0 1-6 * ?"); - assertTrue(output, output.contains("Here are the next " + count + " times this cron expression will trigger")); + assertThat(output, containsString("Here are the next " + count + " times this cron expression will trigger")); + } + + public void testGetNextValidTimes() throws Exception { + final int year = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT).get(Calendar.YEAR) + 1; + { + String output = execute("0 3 23 8 9 ? " + year); + assertThat(output, containsString("Here are the next 10 times this cron expression will trigger:")); + assertThat(output, not(containsString("ERROR"))); + assertThat(output, not(containsString("2.\t"))); + } + { + String output = execute("0 3 23 */4 9 ? " + year); + assertThat(output, containsString("Here are the next 10 times this cron expression will trigger:")); + assertThat(output, not(containsString("ERROR"))); + } + { + Exception expectThrows = expectThrows(Exception.class, () -> execute("0 3 23 */4 9 ? 2017")); + String message = expectThrows.getMessage(); + assertThat(message, containsString("Could not compute future times since")); + assertThat(message, containsString("(perhaps the cron expression only points to times in the past?)")); + } } } From 50441f97ae745814db96c262e99d0f465aca5b2c Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Thu, 23 Aug 2018 09:35:06 +0100 Subject: [PATCH 119/283] HLRC: Add ML Get Buckets API (#33056) Relates #29827 --- .../client/MLRequestConverters.java | 17 +- .../client/MachineLearningClient.java | 58 +++- .../client/MLRequestConvertersTests.java | 21 ++ .../client/MachineLearningGetResultsIT.java | 217 ++++++++++++++ .../MlClientDocumentationIT.java | 111 +++++++- .../high-level/ml/get-buckets.asciidoc | 125 ++++++++ .../high-level/supported-apis.asciidoc | 2 + .../protocol/xpack/ml/GetBucketsRequest.java | 268 ++++++++++++++++++ .../protocol/xpack/ml/GetBucketsResponse.java | 78 +++++ .../protocol/xpack/ml/PutJobResponse.java | 3 - .../xpack/ml/job/util/PageParams.java | 99 +++++++ .../xpack/ml/GetBucketsRequestTests.java | 78 +++++ .../xpack/ml/GetBucketsResponseTests.java | 53 ++++ .../xpack/ml/job/results/BucketTests.java | 2 +- .../xpack/ml/util/PageParamsTests.java | 43 +++ 15 files changed, 1159 insertions(+), 16 deletions(-) create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningGetResultsIT.java create mode 100644 docs/java-rest/high-level/ml/get-buckets.asciidoc create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequest.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponse.java create mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/PageParams.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequestTests.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponseTests.java create mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/util/PageParamsTests.java 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 1c2b6f79eaf6..024ba9e2fca8 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.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.GetJobRequest; +import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; @@ -69,7 +70,7 @@ static Request getJob(GetJobRequest getJobRequest) { return request; } - static Request openJob(OpenJobRequest openJobRequest) throws IOException { + static Request openJob(OpenJobRequest openJobRequest) { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") .addPathPartAsIs("ml") @@ -109,4 +110,18 @@ static Request deleteJob(DeleteJobRequest deleteJobRequest) { return request; } + + static Request getBuckets(GetBucketsRequest getBucketsRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(getBucketsRequest.getJobId()) + .addPathPartAsIs("results") + .addPathPartAsIs("buckets") + .build(); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + request.setEntity(createEntity(getBucketsRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } } 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 90acabfbdd8a..c4dcc1eaffc5 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 @@ -23,6 +23,8 @@ import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; +import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; +import org.elasticsearch.protocol.xpack.ml.GetBucketsResponse; import org.elasticsearch.protocol.xpack.ml.GetJobRequest; import org.elasticsearch.protocol.xpack.ml.GetJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; @@ -54,7 +56,7 @@ public final class MachineLearningClient { * For additional info * see ML PUT job documentation * - * @param request the PutJobRequest containing the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} settings + * @param request The PutJobRequest containing the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} settings * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return PutJobResponse with enclosed {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} object * @throws IOException when there is a serialization issue sending the request or receiving the response @@ -73,7 +75,7 @@ public PutJobResponse putJob(PutJobRequest request, RequestOptions options) thro * For additional info * see ML PUT job documentation * - * @param request the request containing the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} settings + * @param request The request containing the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} 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 */ @@ -93,7 +95,7 @@ public void putJobAsync(PutJobRequest request, RequestOptions options, ActionLis * For additional info * see *

- * @param request {@link GetJobRequest} request containing a list of jobId(s) and additional options + * @param request {@link GetJobRequest} Request containing a list of jobId(s) and additional options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return {@link GetJobResponse} response object containing * the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} objects and the number of jobs found @@ -114,7 +116,7 @@ public GetJobResponse getJob(GetJobRequest request, RequestOptions options) thro * For additional info * see *

- * @param request {@link GetJobRequest} request containing a list of jobId(s) and additional options + * @param request {@link GetJobRequest} Request containing a list of jobId(s) and additional 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 with {@link GetJobResponse} upon request completion */ @@ -133,7 +135,7 @@ public void getJobAsync(GetJobRequest request, RequestOptions options, ActionLis * For additional info * see ML Delete Job documentation *

- * @param request the request to delete the job + * @param request The request to delete the job * @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 @@ -152,7 +154,7 @@ public DeleteJobResponse deleteJob(DeleteJobRequest request, RequestOptions opti * For additional info * see ML Delete Job documentation *

- * @param request the request to delete the job + * @param request The request to delete the job * @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 */ @@ -176,7 +178,7 @@ public void deleteJobAsync(DeleteJobRequest request, RequestOptions options, Act * For additional info * see *

- * @param request request containing job_id and additional optional options + * @param request Request containing job_id and additional optional options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return response containing if the job was successfully opened or not. * @throws IOException when there is a serialization issue sending the request or receiving the response @@ -199,7 +201,7 @@ public OpenJobResponse openJob(OpenJobRequest request, RequestOptions options) t * For additional info * see *

- * @param request request containing job_id and additional optional options + * @param request Request containing job_id and additional optional 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 */ @@ -217,7 +219,7 @@ public void openJobAsync(OpenJobRequest request, RequestOptions options, ActionL * * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results. * - * @param request request containing job_ids and additional options. See {@link CloseJobRequest} + * @param request Request containing job_ids and additional options. See {@link CloseJobRequest} * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return response containing if the job was successfully closed or not. * @throws IOException when there is a serialization issue sending the request or receiving the response @@ -235,7 +237,7 @@ public CloseJobResponse closeJob(CloseJobRequest request, RequestOptions options * * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results. * - * @param request request containing job_ids and additional options. See {@link CloseJobRequest} + * @param request Request containing job_ids and additional options. See {@link CloseJobRequest} * @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 */ @@ -247,4 +249,40 @@ public void closeJobAsync(CloseJobRequest request, RequestOptions options, Actio listener, Collections.emptySet()); } + + /** + * Gets the buckets for a Machine Learning Job. + *

+ * For additional info + * see ML GET buckets documentation + * + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + */ + public GetBucketsResponse getBuckets(GetBucketsRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::getBuckets, + options, + GetBucketsResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Gets the buckets for a Machine Learning Job, notifies listener once the requested buckets are retrieved. + *

+ * For additional info + * see ML GET buckets 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 getBucketsAsync(GetBucketsRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::getBuckets, + options, + GetBucketsResponse::fromXContent, + listener, + Collections.emptySet()); + } } 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 0d95c3596585..9065cda9cd6f 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 @@ -22,17 +22,20 @@ import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; +import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; import org.elasticsearch.protocol.xpack.ml.GetJobRequest; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; import org.elasticsearch.protocol.xpack.ml.PutJobRequest; import org.elasticsearch.protocol.xpack.ml.job.config.AnalysisConfig; import org.elasticsearch.protocol.xpack.ml.job.config.Detector; import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import org.elasticsearch.test.ESTestCase; import java.io.ByteArrayOutputStream; @@ -49,6 +52,7 @@ public void testPutJob() throws IOException { Request request = MLRequestConverters.putJob(putJobRequest); + assertEquals(HttpPut.METHOD_NAME, request.getMethod()); assertThat(request.getEndpoint(), equalTo("/_xpack/ml/anomaly_detectors/foo")); try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) { Job parsedJob = Job.PARSER.apply(parser, null).build(); @@ -118,6 +122,23 @@ public void testDeleteJob() { assertEquals(Boolean.toString(true), request.getParameters().get("force")); } + public void testGetBuckets() throws IOException { + String jobId = randomAlphaOfLength(10); + GetBucketsRequest getBucketsRequest = new GetBucketsRequest(jobId); + getBucketsRequest.setPageParams(new PageParams(100, 300)); + getBucketsRequest.setAnomalyScore(75.0); + getBucketsRequest.setSort("anomaly_score"); + getBucketsRequest.setDescending(true); + + Request request = MLRequestConverters.getBuckets(getBucketsRequest); + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/results/buckets", request.getEndpoint()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) { + GetBucketsRequest parsedRequest = GetBucketsRequest.PARSER.apply(parser, null); + assertThat(parsedRequest, equalTo(getBucketsRequest)); + } + } + private static Job createValidJob(String jobId) { AnalysisConfig.Builder analysisConfig = AnalysisConfig.builder(Collections.singletonList( Detector.builder().setFunction("count").build())); 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 new file mode 100644 index 000000000000..a4f83c347ad1 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningGetResultsIT.java @@ -0,0 +1,217 @@ +/* + * 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; + +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; +import org.elasticsearch.protocol.xpack.ml.GetBucketsResponse; +import org.elasticsearch.protocol.xpack.ml.PutJobRequest; +import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; +import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +public class MachineLearningGetResultsIT extends ESRestHighLevelClientTestCase { + + private static final String RESULTS_INDEX = ".ml-anomalies-shared"; + private static final String DOC = "doc"; + + private static final String JOB_ID = "get-results-it-job"; + + // 2018-08-01T00:00:00Z + private static final long START_TIME_EPOCH_MS = 1533081600000L; + + private BucketStats bucketStats = new BucketStats(); + + @Before + public void createJobAndIndexResults() throws IOException { + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + Job job = MachineLearningIT.buildJob(JOB_ID); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + + long time = START_TIME_EPOCH_MS; + long endTime = time + 3600000L * 24 * 10; // 10 days of hourly buckets + while (time < endTime) { + addBucketIndexRequest(time, false, bulkRequest); + addRecordIndexRequests(time, false, bulkRequest); + time += 3600000L; + } + + // Also index an interim bucket + addBucketIndexRequest(time, true, bulkRequest); + addRecordIndexRequests(time, true, bulkRequest); + + highLevelClient().bulk(bulkRequest, RequestOptions.DEFAULT); + } + + private void addBucketIndexRequest(long timestamp, boolean isInterim, BulkRequest bulkRequest) { + IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC); + double bucketScore = randomDoubleBetween(0.0, 100.0, true); + bucketStats.report(bucketScore); + indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"result_type\":\"bucket\", \"timestamp\": " + timestamp + "," + + "\"bucket_span\": 3600,\"is_interim\": " + isInterim + ", \"anomaly_score\": " + bucketScore + + ", \"bucket_influencers\":[{\"job_id\": \"" + JOB_ID + "\", \"result_type\":\"bucket_influencer\", " + + "\"influencer_field_name\": \"bucket_time\", \"timestamp\": " + timestamp + ", \"bucket_span\": 3600, " + + "\"is_interim\": " + isInterim + "}]}", XContentType.JSON); + bulkRequest.add(indexRequest); + } + + private void addRecordIndexRequests(long timestamp, boolean isInterim, BulkRequest bulkRequest) { + if (randomBoolean()) { + return; + } + int recordCount = randomIntBetween(1, 3); + for (int i = 0; i < recordCount; ++i) { + IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC); + double recordScore = randomDoubleBetween(0.0, 100.0, true); + double p = randomDoubleBetween(0.0, 0.05, false); + indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"result_type\":\"record\", \"timestamp\": " + timestamp + "," + + "\"bucket_span\": 3600,\"is_interim\": " + isInterim + ", \"record_score\": " + recordScore + ", \"probability\": " + + p + "}", XContentType.JSON); + bulkRequest.add(indexRequest); + } + } + + @After + public void deleteJob() throws IOException { + new MlRestTestStateCleaner(logger, client()).clearMlMetadata(); + } + + public void testGetBuckets() throws IOException { + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + assertThat(response.count(), equalTo(241L)); + assertThat(response.buckets().size(), equalTo(100)); + assertThat(response.buckets().get(0).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS)); + } + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + request.setTimestamp("1533081600000"); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + assertThat(response.count(), equalTo(1L)); + assertThat(response.buckets().size(), equalTo(1)); + assertThat(response.buckets().get(0).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS)); + } + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + request.setAnomalyScore(75.0); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + assertThat(response.count(), equalTo(bucketStats.criticalCount)); + assertThat(response.buckets().size(), equalTo((int) Math.min(100, bucketStats.criticalCount))); + assertThat(response.buckets().stream().anyMatch(b -> b.getAnomalyScore() < 75.0), is(false)); + } + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + request.setExcludeInterim(true); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + assertThat(response.count(), equalTo(240L)); + } + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + request.setStart("1533081600000"); + request.setEnd("1533092400000"); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + assertThat(response.count(), equalTo(3L)); + assertThat(response.buckets().get(0).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS)); + assertThat(response.buckets().get(1).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS + 3600000L)); + assertThat(response.buckets().get(2).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS + 2 * + 3600000L)); + } + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + request.setPageParams(new PageParams(3, 3)); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + assertThat(response.buckets().size(), equalTo(3)); + assertThat(response.buckets().get(0).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS + 3 * 3600000L)); + assertThat(response.buckets().get(1).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS + 4 * 3600000L)); + assertThat(response.buckets().get(2).getTimestamp().getTime(), equalTo(START_TIME_EPOCH_MS + 5 * 3600000L)); + } + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + request.setSort("anomaly_score"); + request.setDescending(true); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + double previousScore = 100.0; + for (Bucket bucket : response.buckets()) { + assertThat(bucket.getAnomalyScore(), lessThanOrEqualTo(previousScore)); + previousScore = bucket.getAnomalyScore(); + } + } + { + GetBucketsRequest request = new GetBucketsRequest(JOB_ID); + // Make sure we get all buckets + request.setPageParams(new PageParams(0, 10000)); + request.setExpand(true); + + GetBucketsResponse response = execute(request, machineLearningClient::getBuckets, machineLearningClient::getBucketsAsync); + + assertThat(response.buckets().stream().anyMatch(b -> b.getRecords().size() > 0), is(true)); + } + } + + private static class BucketStats { + // score < 50.0 + private long minorCount; + + // score < 75.0 + private long majorCount; + + // score > 75.0 + private long criticalCount; + + private void report(double anomalyScore) { + if (anomalyScore < 50.0) { + minorCount++; + } else if (anomalyScore < 75.0) { + majorCount++; + } else { + criticalCount++; + } + } + } +} 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 73531bae5532..683f91dae2eb 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 @@ -20,16 +20,21 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.ESRestHighLevelClientTestCase; import org.elasticsearch.client.MachineLearningIT; import org.elasticsearch.client.MlRestTestStateCleaner; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; +import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; +import org.elasticsearch.protocol.xpack.ml.GetBucketsResponse; import org.elasticsearch.protocol.xpack.ml.GetJobRequest; import org.elasticsearch.protocol.xpack.ml.GetJobResponse; import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; @@ -40,6 +45,8 @@ import org.elasticsearch.protocol.xpack.ml.job.config.DataDescription; import org.elasticsearch.protocol.xpack.ml.job.config.Detector; import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; +import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import org.junit.After; import java.io.IOException; @@ -293,7 +300,7 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } - + public void testCloseJob() throws Exception { RestHighLevelClient client = highLevelClient(); @@ -334,6 +341,7 @@ public void onFailure(Exception e) { }; //end::x-pack-ml-close-job-listener CloseJobRequest closeJobRequest = new CloseJobRequest("closing-my-second-machine-learning-job"); + // Replace the empty listener by a blocking listener in test final CountDownLatch latch = new CountDownLatch(1); listener = new LatchedActionListener<>(listener, latch); @@ -345,4 +353,105 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testGetBuckets() throws IOException, InterruptedException { + RestHighLevelClient client = highLevelClient(); + + String jobId = "test-get-buckets"; + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + // Let us index a bucket + IndexRequest indexRequest = new IndexRequest(".ml-anomalies-shared", "doc"); + indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + indexRequest.source("{\"job_id\":\"test-get-buckets\", \"result_type\":\"bucket\", \"timestamp\": 1533081600000," + + "\"bucket_span\": 600,\"is_interim\": false, \"anomaly_score\": 80.0}", XContentType.JSON); + client.index(indexRequest, RequestOptions.DEFAULT); + + { + // tag::x-pack-ml-get-buckets-request + GetBucketsRequest request = new GetBucketsRequest(jobId); // <1> + // end::x-pack-ml-get-buckets-request + + // tag::x-pack-ml-get-buckets-timestamp + request.setTimestamp("2018-08-17T00:00:00Z"); // <1> + // end::x-pack-ml-get-buckets-timestamp + + // Set timestamp to null as it is incompatible with other args + request.setTimestamp(null); + + // tag::x-pack-ml-get-buckets-anomaly-score + request.setAnomalyScore(75.0); // <1> + // end::x-pack-ml-get-buckets-anomaly-score + + // tag::x-pack-ml-get-buckets-desc + request.setDescending(true); // <1> + // end::x-pack-ml-get-buckets-desc + + // tag::x-pack-ml-get-buckets-end + request.setEnd("2018-08-21T00:00:00Z"); // <1> + // end::x-pack-ml-get-buckets-end + + // tag::x-pack-ml-get-buckets-exclude-interim + request.setExcludeInterim(true); // <1> + // end::x-pack-ml-get-buckets-exclude-interim + + // tag::x-pack-ml-get-buckets-expand + request.setExpand(true); // <1> + // end::x-pack-ml-get-buckets-expand + + // tag::x-pack-ml-get-buckets-page + request.setPageParams(new PageParams(100, 200)); // <1> + // end::x-pack-ml-get-buckets-page + + // Set page params back to null so the response contains the bucket we indexed + request.setPageParams(null); + + // tag::x-pack-ml-get-buckets-sort + request.setSort("anomaly_score"); // <1> + // end::x-pack-ml-get-buckets-sort + + // tag::x-pack-ml-get-buckets-start + request.setStart("2018-08-01T00:00:00Z"); // <1> + // end::x-pack-ml-get-buckets-start + + // tag::x-pack-ml-get-buckets-execute + GetBucketsResponse response = client.machineLearning().getBuckets(request, RequestOptions.DEFAULT); + // end::x-pack-ml-get-buckets-execute + + // tag::x-pack-ml-get-buckets-response + long count = response.count(); // <1> + List buckets = response.buckets(); // <2> + // end::x-pack-ml-get-buckets-response + assertEquals(1, buckets.size()); + } + { + GetBucketsRequest request = new GetBucketsRequest(jobId); + + // tag::x-pack-ml-get-buckets-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(GetBucketsResponse getBucketsResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::x-pack-ml-get-buckets-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-buckets-execute-async + client.machineLearning().getBucketsAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::x-pack-ml-get-buckets-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } } diff --git a/docs/java-rest/high-level/ml/get-buckets.asciidoc b/docs/java-rest/high-level/ml/get-buckets.asciidoc new file mode 100644 index 000000000000..81a21d3d18ac --- /dev/null +++ b/docs/java-rest/high-level/ml/get-buckets.asciidoc @@ -0,0 +1,125 @@ +[[java-rest-high-x-pack-ml-get-buckets]] +=== Get Buckets API + +The Get Buckets API retrieves one or more bucket results. +It accepts a `GetBucketsRequest` object and responds +with a `GetBucketsResponse` object. + +[[java-rest-high-x-pack-ml-get-buckets-request]] +==== Get Buckets Request + +A `GetBucketsRequest` 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-buckets-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-buckets-timestamp] +-------------------------------------------------- +<1> The timestamp of the bucket to get. Otherwise it will return all buckets. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-anomaly-score] +-------------------------------------------------- +<1> Buckets with anomaly scores greater or equal than this value will be returned. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-desc] +-------------------------------------------------- +<1> If `true`, the buckets are sorted in descending order. Defaults to `false`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-end] +-------------------------------------------------- +<1> Buckets with timestamps earlier than this time will be returned. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-exclude-interim] +-------------------------------------------------- +<1> If `true`, interim results will be excluded. Defaults to `false`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-expand] +-------------------------------------------------- +<1> If `true`, buckets will include their anomaly records. Defaults to `false`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-page] +-------------------------------------------------- +<1> The page parameters `from` and `size`. `from` specifies the number of buckets to skip. +`size` specifies the maximum number of buckets to get. Defaults to `0` and `100` respectively. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-sort] +-------------------------------------------------- +<1> The field to sort buckets on. Defaults to `timestamp`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-end] +-------------------------------------------------- +<1> Buckets with timestamps on or after this time will be returned. + +[[java-rest-high-x-pack-ml-get-buckets-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-buckets-execute] +-------------------------------------------------- + + +[[java-rest-high-x-pack-ml-get-buckets-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-buckets-execute-async] +-------------------------------------------------- +<1> The `GetBucketsRequest` 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 `GetBucketsResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-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-buckets-response]] +==== Get Buckets Response + +The returned `GetBucketsResponse` contains the requested buckets: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-response] +-------------------------------------------------- +<1> The count of buckets that were matched +<2> The buckets 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 c7b46b399622..e04e391f3e0b 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -209,12 +209,14 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> include::ml/put-job.asciidoc[] include::ml/get-job.asciidoc[] include::ml/delete-job.asciidoc[] include::ml/open-job.asciidoc[] include::ml/close-job.asciidoc[] +include::ml/get-buckets.asciidoc[] == Migration APIs diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequest.java new file mode 100644 index 000000000000..4957f9b6ff6e --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequest.java @@ -0,0 +1,268 @@ +/* + * 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.protocol.xpack.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.protocol.xpack.ml.job.config.Job; +import org.elasticsearch.protocol.xpack.ml.job.results.Result; +import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; + +import java.io.IOException; +import java.util.Objects; + +/** + * A request to retrieve buckets of a given job + */ +public class GetBucketsRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField EXPAND = new ParseField("expand"); + public static final ParseField EXCLUDE_INTERIM = new ParseField("exclude_interim"); + public static final ParseField START = new ParseField("start"); + public static final ParseField END = new ParseField("end"); + public static final ParseField ANOMALY_SCORE = new ParseField("anomaly_score"); + public static final ParseField TIMESTAMP = new ParseField("timestamp"); + public static final ParseField SORT = new ParseField("sort"); + public static final ParseField DESCENDING = new ParseField("desc"); + + public static final ObjectParser PARSER = new ObjectParser<>("get_buckets_request", GetBucketsRequest::new); + + static { + PARSER.declareString((request, jobId) -> request.jobId = jobId, Job.ID); + PARSER.declareString(GetBucketsRequest::setTimestamp, Result.TIMESTAMP); + PARSER.declareBoolean(GetBucketsRequest::setExpand, EXPAND); + PARSER.declareBoolean(GetBucketsRequest::setExcludeInterim, EXCLUDE_INTERIM); + PARSER.declareStringOrNull(GetBucketsRequest::setStart, START); + PARSER.declareStringOrNull(GetBucketsRequest::setEnd, END); + PARSER.declareObject(GetBucketsRequest::setPageParams, PageParams.PARSER, PageParams.PAGE); + PARSER.declareDouble(GetBucketsRequest::setAnomalyScore, ANOMALY_SCORE); + PARSER.declareString(GetBucketsRequest::setSort, SORT); + PARSER.declareBoolean(GetBucketsRequest::setDescending, DESCENDING); + } + + private String jobId; + private String timestamp; + private Boolean expand; + private Boolean excludeInterim; + private String start; + private String end; + private PageParams pageParams; + private Double anomalyScore; + private String sort; + private Boolean descending; + + private GetBucketsRequest() {} + + /** + * Constructs a request to retrieve buckets of a given job + * @param jobId id of the job to retrieve buckets of + */ + public GetBucketsRequest(String jobId) { + this.jobId = Objects.requireNonNull(jobId); + } + + public String getJobId() { + return jobId; + } + + /** + * Sets the timestamp of a specific bucket to be retrieved. + * @param timestamp the timestamp of a specific bucket to be retrieved + */ + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getTimestamp() { + return timestamp; + } + + public boolean isExpand() { + return expand; + } + + /** + * Sets the value of "expand". + * When {@code true}, buckets will be expanded to include their records. + * @param expand value of "expand" to be set + */ + public void setExpand(boolean expand) { + this.expand = expand; + } + + public boolean isExcludeInterim() { + return excludeInterim; + } + + /** + * Sets the value of "exclude_interim". + * When {@code true}, interim buckets will be filtered out. + * @param excludeInterim value of "exclude_interim" to be set + */ + public void setExcludeInterim(boolean excludeInterim) { + this.excludeInterim = excludeInterim; + } + + public String getStart() { + return start; + } + + /** + * Sets the value of "start" which is a timestamp. + * Only buckets whose timestamp is on or after the "start" value will be returned. + * @param start value of "start" to be set + */ + public void setStart(String start) { + this.start = start; + } + + public String getEnd() { + return end; + } + + /** + * Sets the value of "end" which is a timestamp. + * Only buckets whose timestamp is before the "end" value will be returned. + * @param end value of "end" to be set + */ + public void setEnd(String end) { + this.end = end; + } + + public PageParams getPageParams() { + return pageParams; + } + + /** + * Sets the paging parameters + * @param pageParams the paging parameters + */ + public void setPageParams(PageParams pageParams) { + this.pageParams = pageParams; + } + + public Double getAnomalyScore() { + return anomalyScore; + } + + /** + * Sets the value of "anomaly_score". + * Only buckets with "anomaly_score" equal or greater will be returned. + * @param anomalyScore value of "anomaly_score". + */ + public void setAnomalyScore(double anomalyScore) { + this.anomalyScore = anomalyScore; + } + + public String getSort() { + return sort; + } + + /** + * Sets the value of "sort". + * Specifies the bucket field to sort on. + * @param sort value of "sort". + */ + public void setSort(String sort) { + this.sort = sort; + } + + public boolean isDescending() { + return descending; + } + + /** + * Sets the value of "desc". + * Specifies the sorting order. + * @param descending value of "desc" + */ + public void setDescending(boolean descending) { + this.descending = descending; + } + + @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 (timestamp != null) { + builder.field(Result.TIMESTAMP.getPreferredName(), timestamp); + } + if (expand != null) { + builder.field(EXPAND.getPreferredName(), expand); + } + if (excludeInterim != null) { + builder.field(EXCLUDE_INTERIM.getPreferredName(), excludeInterim); + } + if (start != null) { + builder.field(START.getPreferredName(), start); + } + if (end != null) { + builder.field(END.getPreferredName(), end); + } + if (pageParams != null) { + builder.field(PageParams.PAGE.getPreferredName(), pageParams); + } + if (anomalyScore != null) { + builder.field(ANOMALY_SCORE.getPreferredName(), anomalyScore); + } + if (sort != null) { + builder.field(SORT.getPreferredName(), sort); + } + if (descending != null) { + builder.field(DESCENDING.getPreferredName(), descending); + } + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(jobId, timestamp, expand, excludeInterim, anomalyScore, pageParams, start, end, sort, descending); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GetBucketsRequest other = (GetBucketsRequest) obj; + return Objects.equals(jobId, other.jobId) && + Objects.equals(timestamp, other.timestamp) && + Objects.equals(expand, other.expand) && + Objects.equals(excludeInterim, other.excludeInterim) && + Objects.equals(anomalyScore, other.anomalyScore) && + Objects.equals(pageParams, other.pageParams) && + Objects.equals(start, other.start) && + Objects.equals(end, other.end) && + Objects.equals(sort, other.sort) && + Objects.equals(descending, other.descending); + } +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponse.java new file mode 100644 index 000000000000..4350661f68b3 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponse.java @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.ml; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +/** + * A response containing the requested buckets + */ +public class GetBucketsResponse extends AbstractResultResponse { + + public static final ParseField BUCKETS = new ParseField("buckets"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_buckets_response", + true, a -> new GetBucketsResponse((List) a[0], (long) a[1])); + + static { + PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), Bucket.PARSER, BUCKETS); + PARSER.declareLong(ConstructingObjectParser.constructorArg(), COUNT); + } + + public static GetBucketsResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + GetBucketsResponse(List buckets, long count) { + super(BUCKETS, buckets, count); + } + + /** + * The retrieved buckets + * @return the retrieved buckets + */ + public List buckets() { + 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; + } + GetBucketsResponse other = (GetBucketsResponse) obj; + return count == other.count && Objects.equals(results, other.results); + } +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java index b37bd35d6b17..1bd9e87f6544 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java @@ -39,9 +39,6 @@ public PutJobResponse(Job job) { this.job = job; } - public PutJobResponse() { - } - public Job getResponse() { return job; } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/PageParams.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/PageParams.java new file mode 100644 index 000000000000..2e20e84d7b81 --- /dev/null +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/PageParams.java @@ -0,0 +1,99 @@ +/* + * 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.protocol.xpack.ml.job.util; + +import org.elasticsearch.common.Nullable; +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; + +/** + * Paging parameters for GET requests + */ +public class PageParams implements ToXContentObject { + + public static final ParseField PAGE = new ParseField("page"); + public static final ParseField FROM = new ParseField("from"); + public static final ParseField SIZE = new ParseField("size"); + + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(PAGE.getPreferredName(), + a -> new PageParams((Integer) a[0], (Integer) a[1])); + + static { + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), FROM); + PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), SIZE); + } + + private final Integer from; + private final Integer size; + + /** + * Constructs paging parameters + * @param from skips the specified number of items. When {@code null} the default value will be used. + * @param size specifies the maximum number of items to obtain. When {@code null} the default value will be used. + */ + public PageParams(@Nullable Integer from, @Nullable Integer size) { + this.from = from; + this.size = size; + } + + public int getFrom() { + return from; + } + + public int getSize() { + return size; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (from != null) { + builder.field(FROM.getPreferredName(), from); + } + if (size != null) { + builder.field(SIZE.getPreferredName(), size); + } + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(from, size); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PageParams other = (PageParams) obj; + return Objects.equals(from, other.from) && + Objects.equals(size, other.size); + } + +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequestTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequestTests.java new file mode 100644 index 000000000000..6364ad339b12 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequestTests.java @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.protocol.xpack.ml; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class GetBucketsRequestTests extends AbstractXContentTestCase { + + @Override + protected GetBucketsRequest createTestInstance() { + GetBucketsRequest request = new GetBucketsRequest(randomAlphaOfLengthBetween(1, 20)); + + if (randomBoolean()) { + request.setTimestamp(String.valueOf(randomLong())); + } else { + if (randomBoolean()) { + request.setStart(String.valueOf(randomLong())); + } + if (randomBoolean()) { + request.setEnd(String.valueOf(randomLong())); + } + if (randomBoolean()) { + request.setExcludeInterim(randomBoolean()); + } + if (randomBoolean()) { + request.setAnomalyScore(randomDouble()); + } + if (randomBoolean()) { + int from = randomInt(10000); + int size = randomInt(10000); + request.setPageParams(new PageParams(from, size)); + } + if (randomBoolean()) { + request.setSort("anomaly_score"); + } + if (randomBoolean()) { + request.setDescending(randomBoolean()); + } + } + if (randomBoolean()) { + request.setExpand(randomBoolean()); + } + if (randomBoolean()) { + request.setExcludeInterim(randomBoolean()); + } + return request; + } + + @Override + protected GetBucketsRequest doParseInstance(XContentParser parser) throws IOException { + return GetBucketsRequest.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponseTests.java new file mode 100644 index 000000000000..889c3e93bc70 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponseTests.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.protocol.xpack.ml; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; +import org.elasticsearch.protocol.xpack.ml.job.results.BucketTests; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GetBucketsResponseTests extends AbstractXContentTestCase { + + @Override + protected GetBucketsResponse createTestInstance() { + String jobId = randomAlphaOfLength(20); + int listSize = randomInt(10); + List buckets = new ArrayList<>(listSize); + for (int j = 0; j < listSize; j++) { + Bucket bucket = BucketTests.createTestInstance(jobId); + buckets.add(bucket); + } + return new GetBucketsResponse(buckets, listSize); + } + + @Override + protected GetBucketsResponse doParseInstance(XContentParser parser) throws IOException { + return GetBucketsResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketTests.java index 28b1893afe18..0eb988d8eb82 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketTests.java @@ -35,7 +35,7 @@ public Bucket createTestInstance() { return createTestInstance("foo"); } - public Bucket createTestInstance(String jobId) { + public static Bucket createTestInstance(String jobId) { Bucket bucket = new Bucket(jobId, new Date(randomNonNegativeLong()), randomNonNegativeLong()); if (randomBoolean()) { bucket.setAnomalyScore(randomDouble()); diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/util/PageParamsTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/util/PageParamsTests.java new file mode 100644 index 000000000000..6bd51e93c6f3 --- /dev/null +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/util/PageParamsTests.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.protocol.xpack.ml.util; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; +import org.elasticsearch.test.AbstractXContentTestCase; + +public class PageParamsTests extends AbstractXContentTestCase { + + @Override + protected PageParams doParseInstance(XContentParser parser) { + return PageParams.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + @Override + protected PageParams createTestInstance() { + Integer from = randomBoolean() ? randomInt() : null; + Integer size = randomBoolean() ? randomInt() : null; + return new PageParams(from, size); + } +} From 07cce953059e6e0001366405d555187c8498f7e3 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 23 Aug 2018 12:08:06 +0300 Subject: [PATCH 120/283] [DOCS] Remove reload password from docs cf. #32889 Reload call `_nodes/reload_secure_settings` is not requiring an empty password anymore (#32889). Reflect this in docs. --- docs/reference/setup/secure-settings.asciidoc | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/reference/setup/secure-settings.asciidoc b/docs/reference/setup/secure-settings.asciidoc index 2177440457ac..6abf5dea14d0 100644 --- a/docs/reference/setup/secure-settings.asciidoc +++ b/docs/reference/setup/secure-settings.asciidoc @@ -91,9 +91,6 @@ using the `bin/elasticsearch-keystore add` command, call: [source,js] ---- POST _nodes/reload_secure_settings -{ - "secure_settings_password": "" -} ---- // CONSOLE This API will decrypt and re-read the entire keystore, on every cluster node, From 61f5c188e0a577555db382dd0fe9a4222da9df1a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 23 Aug 2018 12:24:32 +0200 Subject: [PATCH 121/283] HLRC: Fix Compile Error From Missing Throws (#33083) * 50441f97ae745814db96c262e99d0f465aca5b2c#diff-53a95fe7ded21313483f1b2f15977395L72 removed the throws breaking compilation here --- .../main/java/org/elasticsearch/client/MLRequestConverters.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 024ba9e2fca8..6c1cc2057010 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 @@ -70,7 +70,7 @@ static Request getJob(GetJobRequest getJobRequest) { return request; } - static Request openJob(OpenJobRequest openJobRequest) { + static Request openJob(OpenJobRequest openJobRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") .addPathPartAsIs("ml") From 917e5a8c949824e653d92b443768e3909c375992 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 23 Aug 2018 13:19:21 +0200 Subject: [PATCH 122/283] TESTS: Fix Random Fail in MockTcpTransportTests (#33061) * `foobar.txGet()` appears to return before `serviceB.stop()` returns, causing `ServiceB.close()` to run concurrently with the `stop` call and running into a race codition * Closes #32863 --- .../transport/AbstractSimpleTransportTestCase.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java index 21dbc561c6b0..c485f9d45bda 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java @@ -768,6 +768,7 @@ public void onAfter() { public void testNotifyOnShutdown() throws Exception { final CountDownLatch latch2 = new CountDownLatch(1); + final CountDownLatch latch3 = new CountDownLatch(1); try { serviceA.registerRequestHandler("internal:foobar", StringMessageRequest::new, ThreadPool.Names.GENERIC, (request, channel, task) -> { @@ -777,6 +778,8 @@ public void testNotifyOnShutdown() throws Exception { serviceB.stop(); } catch (Exception e) { fail(e.getMessage()); + } finally { + latch3.countDown(); } }); TransportFuture foobar = serviceB.submitRequest(nodeA, "internal:foobar", @@ -788,6 +791,7 @@ public void testNotifyOnShutdown() throws Exception { } catch (TransportException ex) { } + latch3.await(); } finally { serviceB.close(); // make sure we are fully closed here otherwise we might run into assertions down the road serviceA.disconnectFromNode(nodeB); From f3cfd4504f4828db2f2b694fd0bba5654121cbfc Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Thu, 23 Aug 2018 13:33:39 +0200 Subject: [PATCH 123/283] Use `addIfAbsent` instead of checking if an element is contained Relates to #32988 --- .../org/elasticsearch/transport/ConnectionManager.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java index 0c6be15fd92b..da8faaf3c33f 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java @@ -38,7 +38,6 @@ import java.io.Closeable; import java.io.IOException; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -85,9 +84,7 @@ public ConnectionManager(Settings settings, Transport transport, ThreadPool thre } public void addListener(TransportConnectionListener listener) { - if (connectionListener.listeners.contains(listener) == false) { - this.connectionListener.listeners.add(listener); - } + this.connectionListener.listeners.addIfAbsent(listener); } public void removeListener(TransportConnectionListener listener) { @@ -297,7 +294,7 @@ public void onFailure(Exception e) { private static final class DelegatingNodeConnectionListener implements TransportConnectionListener { - private final List listeners = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList listeners = new CopyOnWriteArrayList<>(); @Override public void onNodeDisconnected(DiscoveryNode key) { From f860e589a629bd89f24f022a50c8a2fcb7a77673 Mon Sep 17 00:00:00 2001 From: markharwood Date: Thu, 23 Aug 2018 15:00:30 +0100 Subject: [PATCH 124/283] Test fix - GraphExploreResponseTests should not randomise array elements Closes #33086 --- .../protocol/xpack/graph/GraphExploreResponseTests.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index 74b434581788..0f8f055049be 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -87,6 +87,11 @@ protected GraphExploreResponse doParseInstance(XContentParser parser) throws IO protected boolean supportsUnknownFields() { return true; } + + @Override + protected String[] getShuffleFieldsExceptions() { + return new String[]{"vertices"}; + } protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { return field -> field.startsWith("responses"); From 644c0de5ec0e14ca6ab3abc8d4a18b96f95a023b Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Thu, 23 Aug 2018 09:48:53 -0500 Subject: [PATCH 125/283] Move non duplicated actions back into xpack core (#32952) Most actions' request and response were moved from xpack core into protocol. We have decided to instead duplicate the actions in the HLRC instead of trying to reuse them. This commit moves the non duplicated actions back into xpack core and severs the tie between xpack core and protocol so no other actions can be moved and not duplicated. --- x-pack/plugin/core/build.gradle | 7 +- .../protocol/xpack/XPackInfoRequest.java | 86 ++++ .../protocol/xpack/XPackInfoResponse.java | 487 ++++++++++++++++++ .../protocol/xpack/XPackUsageRequest.java | 18 + .../protocol/xpack/XPackUsageResponse.java | 43 ++ .../protocol/xpack/common/ProtocolUtils.java | 58 +++ .../protocol/xpack/graph/Connection.java | 216 ++++++++ .../xpack/graph/GraphExploreRequest.java | 388 ++++++++++++++ .../xpack/graph/GraphExploreResponse.java | 248 +++++++++ .../protocol/xpack/graph/Hop.java | 160 ++++++ .../protocol/xpack/graph/Vertex.java | 255 +++++++++ .../protocol/xpack/graph/VertexRequest.java | 235 +++++++++ .../protocol/xpack/graph/package-info.java | 11 + .../xpack/license/DeleteLicenseRequest.java | 18 + .../xpack/license/GetLicenseRequest.java | 28 + .../xpack/license/GetLicenseResponse.java | 25 + .../protocol/xpack/license/LicenseStatus.java | 54 ++ .../xpack/license/LicensesStatus.java | 55 ++ .../xpack/license/PutLicenseRequest.java | 40 ++ .../xpack/license/PutLicenseResponse.java | 195 +++++++ .../protocol/xpack/license/package-info.java | 11 + .../migration/IndexUpgradeInfoRequest.java | 85 +++ .../migration/IndexUpgradeInfoResponse.java | 120 +++++ .../migration/UpgradeActionRequired.java | 42 ++ .../xpack/migration/package-info.java | 11 + .../protocol/xpack/package-info.java | 10 + .../protocol/xpack/security/User.java | 246 +++++++++ .../protocol/xpack/security/package-info.java | 11 + .../xpack/watcher/DeleteWatchRequest.java | 76 +++ .../xpack/watcher/DeleteWatchResponse.java | 110 ++++ .../xpack/watcher/PutWatchRequest.java | 145 ++++++ .../xpack/watcher/PutWatchResponse.java | 111 ++++ .../protocol/xpack/watcher/package-info.java | 11 + .../xpack/XPackInfoResponseTests.java | 146 ++++++ .../xpack/common/ProtocolUtilsTests.java | 58 +++ .../graph/GraphExploreResponseTests.java | 118 +++++ .../xpack/license/LicenseStatusTests.java | 17 + .../license/PutLicenseResponseTests.java | 112 ++++ .../IndexUpgradeInfoRequestTests.java | 36 ++ .../IndexUpgradeInfoResponseTests.java | 54 ++ .../protocol/xpack/security/UserTests.java | 25 + .../watcher/DeleteWatchResponseTests.java | 32 ++ .../xpack/watcher/PutWatchResponseTests.java | 32 ++ 43 files changed, 4241 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/common/ProtocolUtils.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Hop.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Vertex.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/VertexRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/DeleteLicenseRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicenseStatus.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicensesStatus.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/package-info.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/UpgradeActionRequired.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/package-info.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/package-info.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/package-info.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/XPackInfoResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/common/ProtocolUtilsTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/LicenseStatusTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponseTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponseTests.java diff --git a/x-pack/plugin/core/build.gradle b/x-pack/plugin/core/build.gradle index ef428bdd73df..a58500b880f9 100644 --- a/x-pack/plugin/core/build.gradle +++ b/x-pack/plugin/core/build.gradle @@ -8,7 +8,6 @@ import java.nio.file.StandardCopyOption apply plugin: 'elasticsearch.esplugin' apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' -apply plugin: 'com.github.johnrengelman.shadow' archivesBaseName = 'x-pack-core' @@ -27,7 +26,6 @@ dependencyLicenses { dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" - bundle project(':x-pack:protocol') compile "org.apache.httpcomponents:httpclient:${versions.httpclient}" compile "org.apache.httpcomponents:httpcore:${versions.httpcore}" compile "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}" @@ -112,8 +110,7 @@ test { // TODO: don't publish test artifacts just to run messy tests, fix the tests! // https://github.com/elastic/x-plugins/issues/724 configurations { - testArtifacts.extendsFrom(testRuntime, shadow) - testArtifacts.exclude(group: project(':x-pack:protocol').group, module: project(':x-pack:protocol').name) + testArtifacts.extendsFrom testRuntime } task testJar(type: Jar) { appendix 'test' @@ -122,7 +119,7 @@ task testJar(type: Jar) { artifacts { // normal es plugins do not publish the jar but we need to since users need it for Transport Clients and extensions - archives shadowJar + archives jar testArtifacts testJar } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoRequest.java new file mode 100644 index 000000000000..41f066daf93d --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoRequest.java @@ -0,0 +1,86 @@ +/* + * 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.protocol.xpack; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.Locale; + +/** + * Fetch information about X-Pack from the cluster. + */ +public class XPackInfoRequest extends ActionRequest { + + public enum Category { + BUILD, LICENSE, FEATURES; + + public static EnumSet toSet(String... categories) { + EnumSet set = EnumSet.noneOf(Category.class); + for (String category : categories) { + switch (category) { + case "_all": + return EnumSet.allOf(Category.class); + case "_none": + return EnumSet.noneOf(Category.class); + default: + set.add(Category.valueOf(category.toUpperCase(Locale.ROOT))); + } + } + return set; + } + } + + private boolean verbose; + private EnumSet categories = EnumSet.noneOf(Category.class); + + public XPackInfoRequest() {} + + public void setVerbose(boolean verbose) { + this.verbose = verbose; + } + + public boolean isVerbose() { + return verbose; + } + + public void setCategories(EnumSet categories) { + this.categories = categories; + } + + public EnumSet getCategories() { + return categories; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + this.verbose = in.readBoolean(); + EnumSet categories = EnumSet.noneOf(Category.class); + int size = in.readVInt(); + for (int i = 0; i < size; i++) { + categories.add(Category.valueOf(in.readString())); + } + this.categories = categories; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBoolean(verbose); + out.writeVInt(categories.size()); + for (Category category : categories) { + out.writeString(category.name()); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java new file mode 100644 index 000000000000..2a7eddcf3539 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java @@ -0,0 +1,487 @@ +/* + * 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.protocol.xpack; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser.ValueType; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.license.LicenseStatus; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class XPackInfoResponse extends ActionResponse implements ToXContentObject { + /** + * Value of the license's expiration time if it should never expire. + */ + public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS = Long.MAX_VALUE - TimeUnit.HOURS.toMillis(24 * 365); + // TODO move this constant to License.java once we move License.java to the protocol jar + + @Nullable private BuildInfo buildInfo; + @Nullable private LicenseInfo licenseInfo; + @Nullable private FeatureSetsInfo featureSetsInfo; + + public XPackInfoResponse() {} + + public XPackInfoResponse(@Nullable BuildInfo buildInfo, @Nullable LicenseInfo licenseInfo, @Nullable FeatureSetsInfo featureSetsInfo) { + this.buildInfo = buildInfo; + this.licenseInfo = licenseInfo; + this.featureSetsInfo = featureSetsInfo; + } + + /** + * @return The build info (incl. build hash and timestamp) + */ + public BuildInfo getBuildInfo() { + return buildInfo; + } + + /** + * @return The current license info (incl. UID, type/mode. status and expiry date). May return {@code null} when no + * license is currently installed. + */ + public LicenseInfo getLicenseInfo() { + return licenseInfo; + } + + /** + * @return The current status of the feature sets in X-Pack. Feature sets describe the features available/enabled in X-Pack. + */ + public FeatureSetsInfo getFeatureSetsInfo() { + return featureSetsInfo; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalWriteable(buildInfo); + out.writeOptionalWriteable(licenseInfo); + out.writeOptionalWriteable(featureSetsInfo); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + this.buildInfo = in.readOptionalWriteable(BuildInfo::new); + this.licenseInfo = in.readOptionalWriteable(LicenseInfo::new); + this.featureSetsInfo = in.readOptionalWriteable(FeatureSetsInfo::new); + } + + @Override + public boolean equals(Object other) { + if (other == null || other.getClass() != getClass()) return false; + if (this == other) return true; + XPackInfoResponse rhs = (XPackInfoResponse) other; + return Objects.equals(buildInfo, rhs.buildInfo) + && Objects.equals(licenseInfo, rhs.licenseInfo) + && Objects.equals(featureSetsInfo, rhs.featureSetsInfo); + } + + @Override + public int hashCode() { + return Objects.hash(buildInfo, licenseInfo, featureSetsInfo); + } + + @Override + public String toString() { + return Strings.toString(this, true, false); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "xpack_info_response", true, (a, v) -> { + BuildInfo buildInfo = (BuildInfo) a[0]; + LicenseInfo licenseInfo = (LicenseInfo) a[1]; + @SuppressWarnings("unchecked") // This is how constructing object parser works + List featureSets = (List) a[2]; + FeatureSetsInfo featureSetsInfo = featureSets == null ? null : new FeatureSetsInfo(new HashSet<>(featureSets)); + return new XPackInfoResponse(buildInfo, licenseInfo, featureSetsInfo); + }); + static { + PARSER.declareObject(optionalConstructorArg(), BuildInfo.PARSER, new ParseField("build")); + /* + * licenseInfo is sort of "double optional" because it is + * optional but it can also be send as `null`. + */ + PARSER.declareField(optionalConstructorArg(), (p, v) -> { + if (p.currentToken() == XContentParser.Token.VALUE_NULL) { + return null; + } + return LicenseInfo.PARSER.parse(p, v); + }, + new ParseField("license"), ValueType.OBJECT_OR_NULL); + PARSER.declareNamedObjects(optionalConstructorArg(), + (p, c, name) -> FeatureSetsInfo.FeatureSet.PARSER.parse(p, name), + new ParseField("features")); + } + public static XPackInfoResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + if (buildInfo != null) { + builder.field("build", buildInfo, params); + } + + EnumSet categories = XPackInfoRequest.Category + .toSet(Strings.splitStringByCommaToArray(params.param("categories", "_all"))); + if (licenseInfo != null) { + builder.field("license", licenseInfo, params); + } else if (categories.contains(XPackInfoRequest.Category.LICENSE)) { + // if the user requested the license info, and there is no license, we should send + // back an explicit null value (indicating there is no license). This is different + // than not adding the license info at all + builder.nullField("license"); + } + + if (featureSetsInfo != null) { + builder.field("features", featureSetsInfo, params); + } + + if (params.paramAsBoolean("human", true)) { + builder.field("tagline", "You know, for X"); + } + + return builder.endObject(); + } + + public static class LicenseInfo implements ToXContentObject, Writeable { + private final String uid; + private final String type; + private final String mode; + private final LicenseStatus status; + private final long expiryDate; + + public LicenseInfo(String uid, String type, String mode, LicenseStatus status, long expiryDate) { + this.uid = uid; + this.type = type; + this.mode = mode; + this.status = status; + this.expiryDate = expiryDate; + } + + public LicenseInfo(StreamInput in) throws IOException { + this(in.readString(), in.readString(), in.readString(), LicenseStatus.readFrom(in), in.readLong()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(uid); + out.writeString(type); + out.writeString(mode); + status.writeTo(out); + out.writeLong(expiryDate); + } + + public String getUid() { + return uid; + } + + public String getType() { + return type; + } + + public String getMode() { + return mode; + } + + public long getExpiryDate() { + return expiryDate; + } + + public LicenseStatus getStatus() { + return status; + } + + @Override + public boolean equals(Object other) { + if (other == null || other.getClass() != getClass()) return false; + if (this == other) return true; + LicenseInfo rhs = (LicenseInfo) other; + return Objects.equals(uid, rhs.uid) + && Objects.equals(type, rhs.type) + && Objects.equals(mode, rhs.mode) + && Objects.equals(status, rhs.status) + && expiryDate == rhs.expiryDate; + } + + @Override + public int hashCode() { + return Objects.hash(uid, type, mode, status, expiryDate); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "license_info", true, (a, v) -> { + String uid = (String) a[0]; + String type = (String) a[1]; + String mode = (String) a[2]; + LicenseStatus status = LicenseStatus.fromString((String) a[3]); + Long expiryDate = (Long) a[4]; + long primitiveExpiryDate = expiryDate == null ? BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS : expiryDate; + return new LicenseInfo(uid, type, mode, status, primitiveExpiryDate); + }); + static { + PARSER.declareString(constructorArg(), new ParseField("uid")); + PARSER.declareString(constructorArg(), new ParseField("type")); + PARSER.declareString(constructorArg(), new ParseField("mode")); + PARSER.declareString(constructorArg(), new ParseField("status")); + PARSER.declareLong(optionalConstructorArg(), new ParseField("expiry_date_in_millis")); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .field("uid", uid) + .field("type", type) + .field("mode", mode) + .field("status", status.label()); + if (expiryDate != BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) { + builder.timeField("expiry_date_in_millis", "expiry_date", expiryDate); + } + return builder.endObject(); + } + } + + public static class BuildInfo implements ToXContentObject, Writeable { + private final String hash; + private final String timestamp; + + public BuildInfo(String hash, String timestamp) { + this.hash = hash; + this.timestamp = timestamp; + } + + public BuildInfo(StreamInput input) throws IOException { + this(input.readString(), input.readString()); + } + + @Override + public void writeTo(StreamOutput output) throws IOException { + output.writeString(hash); + output.writeString(timestamp); + } + + public String getHash() { + return hash; + } + + public String getTimestamp() { + return timestamp; + } + + @Override + public boolean equals(Object other) { + if (other == null || other.getClass() != getClass()) return false; + if (this == other) return true; + BuildInfo rhs = (BuildInfo) other; + return Objects.equals(hash, rhs.hash) + && Objects.equals(timestamp, rhs.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(hash, timestamp); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "build_info", true, (a, v) -> new BuildInfo((String) a[0], (String) a[1])); + static { + PARSER.declareString(constructorArg(), new ParseField("hash")); + PARSER.declareString(constructorArg(), new ParseField("date")); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field("hash", hash) + .field("date", timestamp) + .endObject(); + } + } + + public static class FeatureSetsInfo implements ToXContentObject, Writeable { + private final Map featureSets; + + public FeatureSetsInfo(Set featureSets) { + Map map = new HashMap<>(featureSets.size()); + for (FeatureSet featureSet : featureSets) { + map.put(featureSet.name, featureSet); + } + this.featureSets = Collections.unmodifiableMap(map); + } + + public FeatureSetsInfo(StreamInput in) throws IOException { + int size = in.readVInt(); + Map featureSets = new HashMap<>(size); + for (int i = 0; i < size; i++) { + FeatureSet featureSet = new FeatureSet(in); + featureSets.put(featureSet.name, featureSet); + } + this.featureSets = Collections.unmodifiableMap(featureSets); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(featureSets.size()); + for (FeatureSet featureSet : featureSets.values()) { + featureSet.writeTo(out); + } + } + + public Map getFeatureSets() { + return featureSets; + } + + @Override + public boolean equals(Object other) { + if (other == null || other.getClass() != getClass()) return false; + if (this == other) return true; + FeatureSetsInfo rhs = (FeatureSetsInfo) other; + return Objects.equals(featureSets, rhs.featureSets); + } + + @Override + public int hashCode() { + return Objects.hash(featureSets); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + List names = new ArrayList<>(this.featureSets.keySet()).stream().sorted().collect(Collectors.toList()); + for (String name : names) { + builder.field(name, featureSets.get(name), params); + } + return builder.endObject(); + } + + public static class FeatureSet implements ToXContentObject, Writeable { + private final String name; + @Nullable private final String description; + private final boolean available; + private final boolean enabled; + @Nullable private final Map nativeCodeInfo; + + public FeatureSet(String name, @Nullable String description, boolean available, boolean enabled, + @Nullable Map nativeCodeInfo) { + this.name = name; + this.description = description; + this.available = available; + this.enabled = enabled; + this.nativeCodeInfo = nativeCodeInfo; + } + + public FeatureSet(StreamInput in) throws IOException { + this(in.readString(), in.readOptionalString(), in.readBoolean(), in.readBoolean(), + in.getVersion().onOrAfter(Version.V_5_4_0) ? in.readMap() : null); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeOptionalString(description); + out.writeBoolean(available); + out.writeBoolean(enabled); + if (out.getVersion().onOrAfter(Version.V_5_4_0)) { + out.writeMap(nativeCodeInfo); + } + } + + public String name() { + return name; + } + + @Nullable + public String description() { + return description; + } + + public boolean available() { + return available; + } + + public boolean enabled() { + return enabled; + } + + @Nullable + public Map nativeCodeInfo() { + return nativeCodeInfo; + } + + @Override + public boolean equals(Object other) { + if (other == null || other.getClass() != getClass()) return false; + if (this == other) return true; + FeatureSet rhs = (FeatureSet) other; + return Objects.equals(name, rhs.name) + && Objects.equals(description, rhs.description) + && available == rhs.available + && enabled == rhs.enabled + && Objects.equals(nativeCodeInfo, rhs.nativeCodeInfo); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, available, enabled, nativeCodeInfo); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "feature_set", true, (a, name) -> { + String description = (String) a[0]; + boolean available = (Boolean) a[1]; + boolean enabled = (Boolean) a[2]; + @SuppressWarnings("unchecked") // Matches up with declaration below + Map nativeCodeInfo = (Map) a[3]; + return new FeatureSet(name, description, available, enabled, nativeCodeInfo); + }); + static { + PARSER.declareString(optionalConstructorArg(), new ParseField("description")); + PARSER.declareBoolean(constructorArg(), new ParseField("available")); + PARSER.declareBoolean(constructorArg(), new ParseField("enabled")); + PARSER.declareObject(optionalConstructorArg(), (p, name) -> p.map(), new ParseField("native_code_info")); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (description != null) { + builder.field("description", description); + } + builder.field("available", available); + builder.field("enabled", enabled); + if (nativeCodeInfo != null) { + builder.field("native_code_info", nativeCodeInfo); + } + return builder.endObject(); + } + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageRequest.java new file mode 100644 index 000000000000..83621a9ac3d4 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageRequest.java @@ -0,0 +1,18 @@ +/* + * 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.protocol.xpack; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.MasterNodeRequest; + +public class XPackUsageRequest extends MasterNodeRequest { + + @Override + public ActionRequestValidationException validate() { + return null; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageResponse.java new file mode 100644 index 000000000000..ccf681837fdc --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackUsageResponse.java @@ -0,0 +1,43 @@ +/* + * 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.protocol.xpack; + +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Response object from calling the xpack usage api. + * + * Usage information for each feature is accessible through {@link #getUsages()}. + */ +public class XPackUsageResponse { + + private final Map> usages; + + private XPackUsageResponse(Map> usages) throws IOException { + this.usages = usages; + } + + @SuppressWarnings("unchecked") + private static Map castMap(Object value) { + return (Map)value; + } + + /** Return a map from feature name to usage information for that feature. */ + public Map> getUsages() { + return usages; + } + + public static XPackUsageResponse fromXContent(XContentParser parser) throws IOException { + Map rawMap = parser.map(); + Map> usages = rawMap.entrySet().stream().collect( + Collectors.toMap(Map.Entry::getKey, e -> castMap(e.getValue()))); + return new XPackUsageResponse(usages); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/common/ProtocolUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/common/ProtocolUtils.java new file mode 100644 index 000000000000..393409551212 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/common/ProtocolUtils.java @@ -0,0 +1,58 @@ +/* + * 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.protocol.xpack.common; + +import java.util.Arrays; +import java.util.Map; + +/** + * Common utilities used for XPack protocol classes + */ +public final class ProtocolUtils { + + /** + * Implements equals for a map of string arrays + * + * The map of string arrays is used in some XPack protocol classes but does't work with equal. + */ + public static boolean equals(Map a, Map b) { + if (a == null) { + return b == null; + } + if (b == null) { + return false; + } + if (a.size() != b.size()) { + return false; + } + for (Map.Entry entry : a.entrySet()) { + String[] val = entry.getValue(); + String key = entry.getKey(); + if (val == null) { + if (b.get(key) != null || b.containsKey(key) == false) { + return false; + } + } else { + if (Arrays.equals(val, b.get(key)) == false) { + return false; + } + } + } + return true; + } + + /** + * Implements hashCode for map of string arrays + * + * The map of string arrays does't work with hashCode. + */ + public static int hashCode(Map a) { + int hash = 0; + for (Map.Entry entry : a.entrySet()) + hash += Arrays.hashCode(entry.getValue()); + return hash; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java new file mode 100644 index 000000000000..994c7e2c2d5a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Connection.java @@ -0,0 +1,216 @@ +/* + * 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.protocol.xpack.graph; + +import com.carrotsearch.hppc.ObjectIntHashMap; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContent.Params; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.graph.Vertex.VertexId; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * A Connection links exactly two {@link Vertex} objects. The basis of a + * connection is one or more documents have been found that contain + * this pair of terms and the strength of the connection is recorded + * as a weight. + */ +public class Connection { + private Vertex from; + private Vertex to; + private double weight; + private long docCount; + + public Connection(Vertex from, Vertex to, double weight, long docCount) { + this.from = from; + this.to = to; + this.weight = weight; + this.docCount = docCount; + } + + public Connection(StreamInput in, Map vertices) throws IOException { + from = vertices.get(new VertexId(in.readString(), in.readString())); + to = vertices.get(new VertexId(in.readString(), in.readString())); + weight = in.readDouble(); + docCount = in.readVLong(); + } + + Connection() { + } + + void writeTo(StreamOutput out) throws IOException { + out.writeString(from.getField()); + out.writeString(from.getTerm()); + out.writeString(to.getField()); + out.writeString(to.getTerm()); + out.writeDouble(weight); + out.writeVLong(docCount); + } + + public ConnectionId getId() { + return new ConnectionId(from.getId(), to.getId()); + } + + public Vertex getFrom() { + return from; + } + + public Vertex getTo() { + return to; + } + + /** + * @return a measure of the relative connectedness between a pair of {@link Vertex} objects + */ + public double getWeight() { + return weight; + } + + /** + * @return the number of documents in the sampled set that contained this + * pair of {@link Vertex} objects. + */ + public long getDocCount() { + return docCount; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Connection other = (Connection) obj; + return docCount == other.docCount && + weight == other.weight && + Objects.equals(to, other.to) && + Objects.equals(from, other.from); + } + + @Override + public int hashCode() { + return Objects.hash(docCount, weight, from, to); + } + + + private static final ParseField SOURCE = new ParseField("source"); + private static final ParseField TARGET = new ParseField("target"); + private static final ParseField WEIGHT = new ParseField("weight"); + private static final ParseField DOC_COUNT = new ParseField("doc_count"); + + + void toXContent(XContentBuilder builder, Params params, ObjectIntHashMap vertexNumbers) throws IOException { + builder.field(SOURCE.getPreferredName(), vertexNumbers.get(from)); + builder.field(TARGET.getPreferredName(), vertexNumbers.get(to)); + builder.field(WEIGHT.getPreferredName(), weight); + builder.field(DOC_COUNT.getPreferredName(), docCount); + } + + //When deserializing from XContent we need to wait for all vertices to be loaded before + // Connection objects can be created that reference them. This class provides the interim + // state for connections. + static class UnresolvedConnection { + int fromIndex; + int toIndex; + double weight; + long docCount; + UnresolvedConnection(int fromIndex, int toIndex, double weight, long docCount) { + super(); + this.fromIndex = fromIndex; + this.toIndex = toIndex; + this.weight = weight; + this.docCount = docCount; + } + public Connection resolve(List vertices) { + return new Connection(vertices.get(fromIndex), vertices.get(toIndex), weight, docCount); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "ConnectionParser", true, + args -> { + int source = (Integer) args[0]; + int target = (Integer) args[1]; + double weight = (Double) args[2]; + long docCount = (Long) args[3]; + return new UnresolvedConnection(source, target, weight, docCount); + }); + + static { + PARSER.declareInt(constructorArg(), SOURCE); + PARSER.declareInt(constructorArg(), TARGET); + PARSER.declareDouble(constructorArg(), WEIGHT); + PARSER.declareLong(constructorArg(), DOC_COUNT); + } + static UnresolvedConnection fromXContent(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + } + + + /** + * An identifier (implements hashcode and equals) that represents a + * unique key for a {@link Connection} + */ + public static class ConnectionId { + private final VertexId source; + private final VertexId target; + + public ConnectionId(VertexId source, VertexId target) { + this.source = source; + this.target = target; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + ConnectionId vertexId = (ConnectionId) o; + + if (source != null ? !source.equals(vertexId.source) : vertexId.source != null) + return false; + if (target != null ? !target.equals(vertexId.target) : vertexId.target != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = source != null ? source.hashCode() : 0; + result = 31 * result + (target != null ? target.hashCode() : 0); + return result; + } + + public VertexId getSource() { + return source; + } + + public VertexId getTarget() { + return target; + } + + @Override + public String toString() { + return getSource() + "->" + getTarget(); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreRequest.java new file mode 100644 index 000000000000..196982c0a35f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreRequest.java @@ -0,0 +1,388 @@ +/* + * 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.protocol.xpack.graph; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.ValidateActions; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.Strings; +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.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.aggregations.bucket.sampler.SamplerAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.significant.SignificantTerms; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregator; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * Holds the criteria required to guide the exploration of connected terms which + * can be returned as a graph. + */ +public class GraphExploreRequest extends ActionRequest implements IndicesRequest.Replaceable, ToXContentObject { + + public static final String NO_HOPS_ERROR_MESSAGE = "Graph explore request must have at least one hop"; + public static final String NO_VERTICES_ERROR_MESSAGE = "Graph explore hop must have at least one VertexRequest"; + private String[] indices = Strings.EMPTY_ARRAY; + private IndicesOptions indicesOptions = IndicesOptions.fromOptions(false, false, true, false); + private String[] types = Strings.EMPTY_ARRAY; + private String routing; + private TimeValue timeout; + + private int sampleSize = SamplerAggregationBuilder.DEFAULT_SHARD_SAMPLE_SIZE; + private String sampleDiversityField; + private int maxDocsPerDiversityValue; + private boolean useSignificance = true; + private boolean returnDetailedInfo; + + private List hops = new ArrayList<>(); + + public GraphExploreRequest() { + } + + /** + * Constructs a new graph request to run against the provided indices. No + * indices means it will run against all indices. + */ + public GraphExploreRequest(String... indices) { + this.indices = indices; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (hops.size() == 0) { + validationException = ValidateActions.addValidationError(NO_HOPS_ERROR_MESSAGE, validationException); + } + for (Hop hop : hops) { + validationException = hop.validate(validationException); + } + return validationException; + } + + @Override + public String[] indices() { + return this.indices; + } + + @Override + public GraphExploreRequest indices(String... indices) { + this.indices = indices; + return this; + } + + @Override + public IndicesOptions indicesOptions() { + return indicesOptions; + } + + public GraphExploreRequest indicesOptions(IndicesOptions indicesOptions) { + if (indicesOptions == null) { + throw new IllegalArgumentException("IndicesOptions must not be null"); + } + this.indicesOptions = indicesOptions; + return this; + } + + public String[] types() { + return this.types; + } + + public GraphExploreRequest types(String... types) { + this.types = types; + return this; + } + + public String routing() { + return this.routing; + } + + public GraphExploreRequest routing(String routing) { + this.routing = routing; + return this; + } + + public GraphExploreRequest routing(String... routings) { + this.routing = Strings.arrayToCommaDelimitedString(routings); + return this; + } + + public TimeValue timeout() { + return timeout; + } + + /** + * Graph exploration can be set to timeout after the given period. Search + * operations involved in each hop are limited to the remaining time + * available but can still overrun due to the nature of their "best efforts" + * timeout support. When a timeout occurs partial results are returned. + * + * @param timeout + * a {@link TimeValue} object which determines the maximum length + * of time to spend exploring + */ + public GraphExploreRequest timeout(TimeValue timeout) { + if (timeout == null) { + throw new IllegalArgumentException("timeout must not be null"); + } + this.timeout = timeout; + return this; + } + + public GraphExploreRequest timeout(String timeout) { + timeout(TimeValue.parseTimeValue(timeout, null, getClass().getSimpleName() + ".timeout")); + return this; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + + indices = in.readStringArray(); + indicesOptions = IndicesOptions.readIndicesOptions(in); + types = in.readStringArray(); + routing = in.readOptionalString(); + timeout = in.readOptionalTimeValue(); + sampleSize = in.readInt(); + sampleDiversityField = in.readOptionalString(); + maxDocsPerDiversityValue = in.readInt(); + + useSignificance = in.readBoolean(); + returnDetailedInfo = in.readBoolean(); + + int numHops = in.readInt(); + Hop parentHop = null; + for (int i = 0; i < numHops; i++) { + Hop hop = new Hop(parentHop); + hop.readFrom(in); + hops.add(hop); + parentHop = hop; + } + + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringArray(indices); + indicesOptions.writeIndicesOptions(out); + out.writeStringArray(types); + out.writeOptionalString(routing); + out.writeOptionalTimeValue(timeout); + + out.writeInt(sampleSize); + out.writeOptionalString(sampleDiversityField); + out.writeInt(maxDocsPerDiversityValue); + + out.writeBoolean(useSignificance); + out.writeBoolean(returnDetailedInfo); + out.writeInt(hops.size()); + for (Iterator iterator = hops.iterator(); iterator.hasNext();) { + Hop hop = iterator.next(); + hop.writeTo(out); + } + } + + @Override + public String toString() { + return "graph explore [" + Arrays.toString(indices) + "][" + Arrays.toString(types) + "]"; + } + + /** + * The number of top-matching documents that are considered during each hop + * (default is {@link SamplerAggregationBuilder#DEFAULT_SHARD_SAMPLE_SIZE} + * Very small values (less than 50) may not provide sufficient + * weight-of-evidence to identify significant connections between terms. + *

+ * Very large values (many thousands) are not recommended with loosely + * defined queries (fuzzy queries or those with many OR clauses). This is + * because any useful signals in the best documents are diluted with + * irrelevant noise from low-quality matches. Performance is also typically + * better with smaller samples as there are less look-ups required for + * background frequencies of terms found in the documents + *

+ * + * @param maxNumberOfDocsPerHop + * shard-level sample size in documents + */ + public void sampleSize(int maxNumberOfDocsPerHop) { + sampleSize = maxNumberOfDocsPerHop; + } + + public int sampleSize() { + return sampleSize; + } + + /** + * Optional choice of single-value field on which to diversify sampled + * search results + */ + public void sampleDiversityField(String name) { + sampleDiversityField = name; + } + + public String sampleDiversityField() { + return sampleDiversityField; + } + + /** + * Optional number of permitted docs with same value in sampled search + * results. Must also declare which field using sampleDiversityField + */ + public void maxDocsPerDiversityValue(int maxDocs) { + this.maxDocsPerDiversityValue = maxDocs; + } + + public int maxDocsPerDiversityValue() { + return maxDocsPerDiversityValue; + } + + /** + * Controls the choice of algorithm used to select interesting terms. The + * default value is true which means terms are selected based on + * significance (see the {@link SignificantTerms} aggregation) rather than + * popularity (using the {@link TermsAggregator}). + * + * @param value + * true if the significant_terms algorithm should be used. + */ + public void useSignificance(boolean value) { + this.useSignificance = value; + } + + public boolean useSignificance() { + return useSignificance; + } + + /** + * Return detailed information about vertex frequencies as part of JSON + * results - defaults to false + * + * @param value + * true if detailed information is required in JSON responses + */ + public void returnDetailedInfo(boolean value) { + this.returnDetailedInfo = value; + } + + public boolean returnDetailedInfo() { + return returnDetailedInfo; + } + + /** + * Add a stage in the graph exploration. Each hop represents a stage of + * querying elasticsearch to identify terms which can then be connnected to + * other terms in a subsequent hop. + * + * @param guidingQuery + * optional choice of query which influences which documents are + * considered in this stage + * @return a {@link Hop} object that holds settings for a stage in the graph + * exploration + */ + public Hop createNextHop(QueryBuilder guidingQuery) { + Hop parent = null; + if (hops.size() > 0) { + parent = hops.get(hops.size() - 1); + } + Hop newHop = new Hop(parent); + newHop.guidingQuery = guidingQuery; + hops.add(newHop); + return newHop; + } + + public int getHopNumbers() { + return hops.size(); + } + + public Hop getHop(int hopNumber) { + return hops.get(hopNumber); + } + + public static class TermBoost { + String term; + float boost; + + public TermBoost(String term, float boost) { + super(); + this.term = term; + if (boost <= 0) { + throw new IllegalArgumentException("Boosts must be a positive non-zero number"); + } + this.boost = boost; + } + + TermBoost() { + } + + public String getTerm() { + return term; + } + + public float getBoost() { + return boost; + } + + void readFrom(StreamInput in) throws IOException { + this.term = in.readString(); + this.boost = in.readFloat(); + } + + void writeTo(StreamOutput out) throws IOException { + out.writeString(term); + out.writeFloat(boost); + } + + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + builder.startObject("controls"); + { + if (sampleSize != SamplerAggregationBuilder.DEFAULT_SHARD_SAMPLE_SIZE) { + builder.field("sample_size", sampleSize); + } + if (sampleDiversityField != null) { + builder.startObject("sample_diversity"); + builder.field("field", sampleDiversityField); + builder.field("max_docs_per_value", maxDocsPerDiversityValue); + builder.endObject(); + } + builder.field("use_significance", useSignificance); + if (returnDetailedInfo) { + builder.field("return_detailed_stats", returnDetailedInfo); + } + } + builder.endObject(); + + for (Hop hop : hops) { + if (hop.parentHop != null) { + builder.startObject("connections"); + } + hop.toXContent(builder, params); + } + for (Hop hop : hops) { + if (hop.parentHop != null) { + builder.endObject(); + } + } + builder.endObject(); + + return builder; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponse.java new file mode 100644 index 000000000000..12eb20617ff0 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponse.java @@ -0,0 +1,248 @@ +/* + * 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.protocol.xpack.graph; + +import com.carrotsearch.hppc.ObjectIntHashMap; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ShardOperationFailedException; +import org.elasticsearch.action.search.ShardSearchFailure; +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.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.graph.Connection.ConnectionId; +import org.elasticsearch.protocol.xpack.graph.Connection.UnresolvedConnection; +import org.elasticsearch.protocol.xpack.graph.Vertex.VertexId; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.action.search.ShardSearchFailure.readShardSearchFailure; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Graph explore response holds a graph of {@link Vertex} and {@link Connection} objects + * (nodes and edges in common graph parlance). + * + * @see GraphExploreRequest + */ +public class GraphExploreResponse extends ActionResponse implements ToXContentObject { + + private long tookInMillis; + private boolean timedOut = false; + private ShardOperationFailedException[] shardFailures = ShardSearchFailure.EMPTY_ARRAY; + private Map vertices; + private Map connections; + private boolean returnDetailedInfo; + static final String RETURN_DETAILED_INFO_PARAM = "returnDetailedInfo"; + + public GraphExploreResponse() { + } + + public GraphExploreResponse(long tookInMillis, boolean timedOut, ShardOperationFailedException[] shardFailures, + Map vertices, Map connections, boolean returnDetailedInfo) { + this.tookInMillis = tookInMillis; + this.timedOut = timedOut; + this.shardFailures = shardFailures; + this.vertices = vertices; + this.connections = connections; + this.returnDetailedInfo = returnDetailedInfo; + } + + + public TimeValue getTook() { + return new TimeValue(tookInMillis); + } + + public long getTookInMillis() { + return tookInMillis; + } + + /** + * @return true if the time stated in {@link GraphExploreRequest#timeout(TimeValue)} was exceeded + * (not all hops may have been completed in this case) + */ + public boolean isTimedOut() { + return this.timedOut; + } + public ShardOperationFailedException[] getShardFailures() { + return shardFailures; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + tookInMillis = in.readVLong(); + timedOut = in.readBoolean(); + + int size = in.readVInt(); + if (size == 0) { + shardFailures = ShardSearchFailure.EMPTY_ARRAY; + } else { + shardFailures = new ShardSearchFailure[size]; + for (int i = 0; i < shardFailures.length; i++) { + shardFailures[i] = readShardSearchFailure(in); + } + } + // read vertices + size = in.readVInt(); + vertices = new HashMap<>(); + for (int i = 0; i < size; i++) { + Vertex n = Vertex.readFrom(in); + vertices.put(n.getId(), n); + } + + size = in.readVInt(); + + connections = new HashMap<>(); + for (int i = 0; i < size; i++) { + Connection e = new Connection(in, vertices); + connections.put(e.getId(), e); + } + + returnDetailedInfo = in.readBoolean(); + + } + + public Collection getConnections() { + return connections.values(); + } + + public Collection getVertices() { + return vertices.values(); + } + + public Vertex getVertex(VertexId id) { + return vertices.get(id); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeVLong(tookInMillis); + out.writeBoolean(timedOut); + + out.writeVInt(shardFailures.length); + for (ShardOperationFailedException shardSearchFailure : shardFailures) { + shardSearchFailure.writeTo(out); + } + + out.writeVInt(vertices.size()); + for (Vertex vertex : vertices.values()) { + vertex.writeTo(out); + } + + out.writeVInt(connections.size()); + for (Connection connection : connections.values()) { + connection.writeTo(out); + } + + out.writeBoolean(returnDetailedInfo); + + } + + private static final ParseField TOOK = new ParseField("took"); + private static final ParseField TIMED_OUT = new ParseField("timed_out"); + private static final ParseField VERTICES = new ParseField("vertices"); + private static final ParseField CONNECTIONS = new ParseField("connections"); + private static final ParseField FAILURES = new ParseField("failures"); + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(TOOK.getPreferredName(), tookInMillis); + builder.field(TIMED_OUT.getPreferredName(), timedOut); + + builder.startArray(FAILURES.getPreferredName()); + if (shardFailures != null) { + for (ShardOperationFailedException shardFailure : shardFailures) { + builder.startObject(); + shardFailure.toXContent(builder, params); + builder.endObject(); + } + } + builder.endArray(); + + ObjectIntHashMap vertexNumbers = new ObjectIntHashMap<>(vertices.size()); + + Map extraParams = new HashMap<>(); + extraParams.put(RETURN_DETAILED_INFO_PARAM, Boolean.toString(returnDetailedInfo)); + Params extendedParams = new DelegatingMapParams(extraParams, params); + + builder.startArray(VERTICES.getPreferredName()); + for (Vertex vertex : vertices.values()) { + builder.startObject(); + vertexNumbers.put(vertex, vertexNumbers.size()); + vertex.toXContent(builder, extendedParams); + builder.endObject(); + } + builder.endArray(); + + builder.startArray(CONNECTIONS.getPreferredName()); + for (Connection connection : connections.values()) { + builder.startObject(); + connection.toXContent(builder, extendedParams, vertexNumbers); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + return builder; + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "GraphExploreResponsenParser", true, + args -> { + GraphExploreResponse result = new GraphExploreResponse(); + result.vertices = new HashMap<>(); + result.connections = new HashMap<>(); + + result.tookInMillis = (Long) args[0]; + result.timedOut = (Boolean) args[1]; + + @SuppressWarnings("unchecked") + List vertices = (List) args[2]; + @SuppressWarnings("unchecked") + List unresolvedConnections = (List) args[3]; + @SuppressWarnings("unchecked") + List failures = (List) args[4]; + for (Vertex vertex : vertices) { + // reverse-engineer if detailed stats were requested - + // mainly here for testing framework's equality tests + result.returnDetailedInfo = result.returnDetailedInfo || vertex.getFg() > 0; + result.vertices.put(vertex.getId(), vertex); + } + for (UnresolvedConnection unresolvedConnection : unresolvedConnections) { + Connection resolvedConnection = unresolvedConnection.resolve(vertices); + result.connections.put(resolvedConnection.getId(), resolvedConnection); + } + if (failures.size() > 0) { + result.shardFailures = failures.toArray(new ShardSearchFailure[failures.size()]); + } + return result; + }); + + static { + PARSER.declareLong(constructorArg(), TOOK); + PARSER.declareBoolean(constructorArg(), TIMED_OUT); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> Vertex.fromXContent(p), VERTICES); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> UnresolvedConnection.fromXContent(p), CONNECTIONS); + PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ShardSearchFailure.fromXContent(p), FAILURES); + } + + public static GraphExploreResponse fromXContext(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Hop.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Hop.java new file mode 100644 index 000000000000..e61403e8b37a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Hop.java @@ -0,0 +1,160 @@ +/* + * 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.protocol.xpack.graph; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ValidateActions; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A Hop represents one of potentially many stages in a graph exploration. + * Each Hop identifies one or more fields in which it will attempt to find + * terms that are significantly connected to the previous Hop. Each field is identified + * using a {@link VertexRequest} + * + *

An example series of Hops on webserver logs would be: + *

    + *
  1. an initial Hop to find + * the top ten IPAddresses trying to access urls containing the word "admin"
  2. + *
  3. a secondary Hop to see which other URLs those IPAddresses were trying to access
  4. + *
+ * + *

+ * Optionally, each hop can contain a "guiding query" that further limits the set of documents considered. + * In our weblog example above we might choose to constrain the second hop to only look at log records that + * had a reponse code of 404. + *

+ *

+ * If absent, the list of {@link VertexRequest}s is inherited from the prior Hop's list to avoid repeating + * the fields that will be examined at each stage. + *

+ * + */ +public class Hop implements ToXContentFragment{ + final Hop parentHop; + List vertices = null; + QueryBuilder guidingQuery = null; + + public Hop(Hop parent) { + this.parentHop = parent; + } + + public ActionRequestValidationException validate(ActionRequestValidationException validationException) { + + if (getEffectiveVertexRequests().size() == 0) { + validationException = ValidateActions.addValidationError(GraphExploreRequest.NO_VERTICES_ERROR_MESSAGE, validationException); + } + return validationException; + + } + + public Hop getParentHop() { + return parentHop; + } + + void writeTo(StreamOutput out) throws IOException { + out.writeOptionalNamedWriteable(guidingQuery); + if (vertices == null) { + out.writeVInt(0); + } else { + out.writeVInt(vertices.size()); + for (VertexRequest vr : vertices) { + vr.writeTo(out); + } + } + } + + void readFrom(StreamInput in) throws IOException { + guidingQuery = in.readOptionalNamedWriteable(QueryBuilder.class); + int size = in.readVInt(); + if (size > 0) { + vertices = new ArrayList<>(); + for (int i = 0; i < size; i++) { + VertexRequest vr = new VertexRequest(); + vr.readFrom(in); + vertices.add(vr); + } + } + } + + public QueryBuilder guidingQuery() { + if (guidingQuery != null) { + return guidingQuery; + } + return QueryBuilders.matchAllQuery(); + } + + /** + * Add a field in which this {@link Hop} will look for terms that are highly linked to + * previous hops and optionally the guiding query. + * + * @param fieldName a field in the chosen index + */ + public VertexRequest addVertexRequest(String fieldName) { + if (vertices == null) { + vertices = new ArrayList<>(); + } + VertexRequest vr = new VertexRequest(); + vr.fieldName(fieldName); + vertices.add(vr); + return vr; + } + + /** + * An optional parameter that focuses the exploration on documents that + * match the given query. + * + * @param queryBuilder any query + */ + public void guidingQuery(QueryBuilder queryBuilder) { + guidingQuery = queryBuilder; + } + + protected List getEffectiveVertexRequests() { + if (vertices != null) { + return vertices; + } + if (parentHop == null) { + return Collections.emptyList(); + } + // otherwise inherit settings from parent + return parentHop.getEffectiveVertexRequests(); + } + + public int getNumberVertexRequests() { + return getEffectiveVertexRequests().size(); + } + + public VertexRequest getVertexRequest(int requestNumber) { + return getEffectiveVertexRequests().get(requestNumber); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (guidingQuery != null) { + builder.field("query"); + guidingQuery.toXContent(builder, params); + } + if(vertices != null && vertices.size()>0) { + builder.startArray("vertices"); + for (VertexRequest vertexRequest : vertices) { + vertexRequest.toXContent(builder, params); + } + builder.endArray(); + } + return builder; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Vertex.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Vertex.java new file mode 100644 index 000000000000..f17812a6396a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/Vertex.java @@ -0,0 +1,255 @@ +/* + * 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.protocol.xpack.graph; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * A vertex in a graph response represents a single term (a field and value pair) + * which appears in one or more documents found as part of the graph exploration. + * + * A vertex term could be a bank account number, an email address, a hashtag or any + * other term that appears in documents and is interesting to represent in a network. + */ +public class Vertex implements ToXContentFragment { + + private final String field; + private final String term; + private double weight; + private final int depth; + private final long bg; + private long fg; + private static final ParseField FIELD = new ParseField("field"); + private static final ParseField TERM = new ParseField("term"); + private static final ParseField WEIGHT = new ParseField("weight"); + private static final ParseField DEPTH = new ParseField("depth"); + private static final ParseField FG = new ParseField("fg"); + private static final ParseField BG = new ParseField("bg"); + + + public Vertex(String field, String term, double weight, int depth, long bg, long fg) { + super(); + this.field = field; + this.term = term; + this.weight = weight; + this.depth = depth; + this.bg = bg; + this.fg = fg; + } + + static Vertex readFrom(StreamInput in) throws IOException { + return new Vertex(in.readString(), in.readString(), in.readDouble(), in.readVInt(), in.readVLong(), in.readVLong()); + } + + void writeTo(StreamOutput out) throws IOException { + out.writeString(field); + out.writeString(term); + out.writeDouble(weight); + out.writeVInt(depth); + out.writeVLong(bg); + out.writeVLong(fg); + } + + @Override + public int hashCode() { + return Objects.hash(field, term, weight, depth, bg, fg); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Vertex other = (Vertex) obj; + return depth == other.depth && + weight == other.weight && + bg == other.bg && + fg == other.fg && + Objects.equals(field, other.field) && + Objects.equals(term, other.term); + + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + boolean returnDetailedInfo = params.paramAsBoolean(GraphExploreResponse.RETURN_DETAILED_INFO_PARAM, false); + builder.field(FIELD.getPreferredName(), field); + builder.field(TERM.getPreferredName(), term); + builder.field(WEIGHT.getPreferredName(), weight); + builder.field(DEPTH.getPreferredName(), depth); + if (returnDetailedInfo) { + builder.field(FG.getPreferredName(), fg); + builder.field(BG.getPreferredName(), bg); + } + return builder; + } + + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "VertexParser", true, + args -> { + String field = (String) args[0]; + String term = (String) args[1]; + double weight = (Double) args[2]; + int depth = (Integer) args[3]; + Long optionalBg = (Long) args[4]; + Long optionalFg = (Long) args[5]; + long bg = optionalBg == null ? 0 : optionalBg; + long fg = optionalFg == null ? 0 : optionalFg; + return new Vertex(field, term, weight, depth, bg, fg); + }); + + static { + PARSER.declareString(constructorArg(), FIELD); + PARSER.declareString(constructorArg(), TERM); + PARSER.declareDouble(constructorArg(), WEIGHT); + PARSER.declareInt(constructorArg(), DEPTH); + PARSER.declareLong(optionalConstructorArg(), BG); + PARSER.declareLong(optionalConstructorArg(), FG); + } + + static Vertex fromXContent(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + + + /** + * @return a {@link VertexId} object that uniquely identifies this Vertex + */ + public VertexId getId() { + return createId(field, term); + } + + /** + * A convenience method for creating a {@link VertexId} + * @param field the field + * @param term the term + * @return a {@link VertexId} that can be used for looking up vertices + */ + public static VertexId createId(String field, String term) { + return new VertexId(field,term); + } + + @Override + public String toString() { + return getId().toString(); + } + + public String getField() { + return field; + } + + public String getTerm() { + return term; + } + + /** + * The weight of a vertex is an accumulation of all of the {@link Connection}s + * that are linked to this {@link Vertex} as part of a graph exploration. + * It is used internally to identify the most interesting vertices to be returned. + * @return a measure of the {@link Vertex}'s relative importance. + */ + public double getWeight() { + return weight; + } + + public void setWeight(final double weight) { + this.weight = weight; + } + + /** + * If the {@link GraphExploreRequest#useSignificance(boolean)} is true (the default) + * this statistic is available. + * @return the number of documents in the index that contain this term (see bg_count in + * + * the significant_terms aggregation) + */ + public long getBg() { + return bg; + } + + /** + * If the {@link GraphExploreRequest#useSignificance(boolean)} is true (the default) + * this statistic is available. + * Together with {@link #getBg()} these numbers are used to derive the significance of a term. + * @return the number of documents in the sample of best matching documents that contain this term (see fg_count in + * + * the significant_terms aggregation) + */ + public long getFg() { + return fg; + } + + public void setFg(final long fg) { + this.fg = fg; + } + + /** + * @return the sequence number in the series of hops where this Vertex term was first encountered + */ + public int getHopDepth() { + return depth; + } + + /** + * An identifier (implements hashcode and equals) that represents a + * unique key for a {@link Vertex} + */ + public static class VertexId { + private final String field; + private final String term; + + public VertexId(String field, String term) { + this.field = field; + this.term = term; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + VertexId vertexId = (VertexId) o; + + if (field != null ? !field.equals(vertexId.field) : vertexId.field != null) + return false; + if (term != null ? !term.equals(vertexId.term) : vertexId.term != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = field != null ? field.hashCode() : 0; + result = 31 * result + (term != null ? term.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return field + ":" + term; + } + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/VertexRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/VertexRequest.java new file mode 100644 index 000000000000..63d2c616547d --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/VertexRequest.java @@ -0,0 +1,235 @@ +/* + * 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.protocol.xpack.graph; + +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 org.elasticsearch.protocol.xpack.graph.GraphExploreRequest.TermBoost; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A request to identify terms from a choice of field as part of a {@link Hop}. + * Optionally, a set of terms can be provided that are used as an exclusion or + * inclusion list to filter which terms are considered. + * + */ +public class VertexRequest implements ToXContentObject { + private String fieldName; + private int size = DEFAULT_SIZE; + public static final int DEFAULT_SIZE = 5; + private Map includes; + private Set excludes; + public static final int DEFAULT_MIN_DOC_COUNT = 3; + private int minDocCount = DEFAULT_MIN_DOC_COUNT; + public static final int DEFAULT_SHARD_MIN_DOC_COUNT = 2; + private int shardMinDocCount = DEFAULT_SHARD_MIN_DOC_COUNT; + + + public VertexRequest() { + + } + + void readFrom(StreamInput in) throws IOException { + fieldName = in.readString(); + size = in.readVInt(); + minDocCount = in.readVInt(); + shardMinDocCount = in.readVInt(); + + int numIncludes = in.readVInt(); + if (numIncludes > 0) { + includes = new HashMap<>(); + for (int i = 0; i < numIncludes; i++) { + TermBoost tb = new TermBoost(); + tb.readFrom(in); + includes.put(tb.term, tb); + } + } + + int numExcludes = in.readVInt(); + if (numExcludes > 0) { + excludes = new HashSet<>(); + for (int i = 0; i < numExcludes; i++) { + excludes.add(in.readString()); + } + } + + } + + void writeTo(StreamOutput out) throws IOException { + out.writeString(fieldName); + out.writeVInt(size); + out.writeVInt(minDocCount); + out.writeVInt(shardMinDocCount); + + if (includes != null) { + out.writeVInt(includes.size()); + for (TermBoost tb : includes.values()) { + tb.writeTo(out); + } + } else { + out.writeVInt(0); + } + + if (excludes != null) { + out.writeVInt(excludes.size()); + for (String term : excludes) { + out.writeString(term); + } + } else { + out.writeVInt(0); + } + } + + public String fieldName() { + return fieldName; + } + + public VertexRequest fieldName(String fieldName) { + this.fieldName = fieldName; + return this; + } + + public int size() { + return size; + } + + /** + * @param size The maximum number of terms that should be returned from this field as part of this {@link Hop} + */ + public VertexRequest size(int size) { + this.size = size; + return this; + } + + public boolean hasIncludeClauses() { + return includes != null && includes.size() > 0; + } + + public boolean hasExcludeClauses() { + return excludes != null && excludes.size() > 0; + } + + /** + * Adds a term that should be excluded from results + * @param term A term to be excluded + */ + public void addExclude(String term) { + if (includes != null) { + throw new IllegalArgumentException("Cannot have both include and exclude clauses"); + } + if (excludes == null) { + excludes = new HashSet<>(); + } + excludes.add(term); + } + + /** + * Adds a term to the set of allowed values - the boost defines the relative + * importance when pursuing connections in subsequent {@link Hop}s. The boost value + * appears as part of the query. + * @param term a required term + * @param boost an optional boost + */ + public void addInclude(String term, float boost) { + if (excludes != null) { + throw new IllegalArgumentException("Cannot have both include and exclude clauses"); + } + if (includes == null) { + includes = new HashMap<>(); + } + includes.put(term, new TermBoost(term, boost)); + } + + public TermBoost[] includeValues() { + return includes.values().toArray(new TermBoost[includes.size()]); + } + + public String[] includeValuesAsStringArray() { + String[] result = new String[includes.size()]; + int i = 0; + for (TermBoost tb : includes.values()) { + result[i++] = tb.term; + } + return result; + } + + public String[] excludesAsArray() { + return excludes.toArray(new String[excludes.size()]); + } + + public int minDocCount() { + return minDocCount; + } + + /** + * A "certainty" threshold which defines the weight-of-evidence required before + * a term found in this field is identified as a useful connection + * + * @param value The minimum number of documents that contain this term found in the samples used across all shards + */ + public VertexRequest minDocCount(int value) { + minDocCount = value; + return this; + } + + + public int shardMinDocCount() { + return Math.min(shardMinDocCount, minDocCount); + } + + /** + * A "certainty" threshold which defines the weight-of-evidence required before + * a term found in this field is identified as a useful connection + * + * @param value The minimum number of documents that contain this term found in the samples used across all shards + */ + public VertexRequest shardMinDocCount(int value) { + shardMinDocCount = value; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("field", fieldName); + if (size != DEFAULT_SIZE) { + builder.field("size", size); + } + if (minDocCount != DEFAULT_MIN_DOC_COUNT) { + builder.field("min_doc_count", minDocCount); + } + if (shardMinDocCount != DEFAULT_SHARD_MIN_DOC_COUNT) { + builder.field("shard_min_doc_count", shardMinDocCount); + } + if(includes!=null) { + builder.startArray("include"); + for (TermBoost tb : includes.values()) { + builder.startObject(); + builder.field("term", tb.term); + builder.field("boost", tb.boost); + builder.endObject(); + } + builder.endArray(); + } + if(excludes!=null) { + builder.startArray("exclude"); + for (String value : excludes) { + builder.value(value); + } + builder.endArray(); + } + builder.endObject(); + return builder; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java new file mode 100644 index 000000000000..5d5dd0f5ef61 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/graph/package-info.java @@ -0,0 +1,11 @@ +/* + * 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. + */ + +/** + * Request and Response objects for the default distribution's Graph + * APIs. + */ +package org.elasticsearch.protocol.xpack.graph; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/DeleteLicenseRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/DeleteLicenseRequest.java new file mode 100644 index 000000000000..62353b093b5b --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/DeleteLicenseRequest.java @@ -0,0 +1,18 @@ +/* + * 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.protocol.xpack.license; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.AcknowledgedRequest; + + +public class DeleteLicenseRequest extends AcknowledgedRequest { + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseRequest.java new file mode 100644 index 000000000000..926ce1d1d705 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseRequest.java @@ -0,0 +1,28 @@ +/* + * 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.protocol.xpack.license; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.MasterNodeReadRequest; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; + + +public class GetLicenseRequest extends MasterNodeReadRequest { + + public GetLicenseRequest() { + } + + public GetLicenseRequest(StreamInput in) throws IOException { + super(in); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseResponse.java new file mode 100644 index 000000000000..6d5e1b5653fe --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/GetLicenseResponse.java @@ -0,0 +1,25 @@ +/* + * 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.protocol.xpack.license; + +import org.elasticsearch.action.ActionResponse; + +public class GetLicenseResponse extends ActionResponse { + + private String license; + + GetLicenseResponse() { + } + + public GetLicenseResponse(String license) { + this.license = license; + } + + public String getLicenseDefinition() { + return license; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicenseStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicenseStatus.java new file mode 100644 index 000000000000..5bc66ab745e4 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicenseStatus.java @@ -0,0 +1,54 @@ +/* + * 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.protocol.xpack.license; + +import java.io.IOException; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; + +/** + * Status of an X-Pack license. + */ +public enum LicenseStatus implements Writeable { + + ACTIVE("active"), + INVALID("invalid"), + EXPIRED("expired"); + + private final String label; + + LicenseStatus(String label) { + this.label = label; + } + + public String label() { + return label; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(label); + } + + public static LicenseStatus readFrom(StreamInput in) throws IOException { + return fromString(in.readString()); + } + + public static LicenseStatus fromString(String value) { + switch (value) { + case "active": + return ACTIVE; + case "invalid": + return INVALID; + case "expired": + return EXPIRED; + default: + throw new IllegalArgumentException("unknown license status [" + value + "]"); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicensesStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicensesStatus.java new file mode 100644 index 000000000000..18745653e761 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/LicensesStatus.java @@ -0,0 +1,55 @@ +/* + * 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.protocol.xpack.license; + +import java.util.Locale; + +public enum LicensesStatus { + VALID((byte) 0), + INVALID((byte) 1), + EXPIRED((byte) 2); + + private final byte id; + + LicensesStatus(byte id) { + this.id = id; + } + + public int id() { + return id; + } + + public static LicensesStatus fromId(int id) { + if (id == 0) { + return VALID; + } else if (id == 1) { + return INVALID; + } else if (id == 2) { + return EXPIRED; + } else { + throw new IllegalStateException("no valid LicensesStatus for id=" + id); + } + } + + + @Override + public String toString() { + return this.name().toLowerCase(Locale.ROOT); + } + + public static LicensesStatus fromString(String value) { + switch (value) { + case "valid": + return VALID; + case "invalid": + return INVALID; + case "expired": + return EXPIRED; + default: + throw new IllegalArgumentException("unknown licenses status [" + value + "]"); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseRequest.java new file mode 100644 index 000000000000..342e6c296e7e --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseRequest.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.protocol.xpack.license; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.AcknowledgedRequest; + +public class PutLicenseRequest extends AcknowledgedRequest { + + private String licenseDefinition; + private boolean acknowledge = false; + + public PutLicenseRequest() { + + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public void setLicenseDefinition(String licenseDefinition) { + this.licenseDefinition = licenseDefinition; + } + + public String getLicenseDefinition() { + return licenseDefinition; + } + + public void setAcknowledge(boolean acknowledge) { + this.acknowledge = acknowledge; + } + + public boolean isAcknowledge() { + return acknowledge; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponse.java new file mode 100644 index 000000000000..206c5a3b3836 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponse.java @@ -0,0 +1,195 @@ +/* + * 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.protocol.xpack.license; + +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.common.ProtocolUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +public class PutLicenseResponse extends AcknowledgedResponse { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "put_license_response", true, (a, v) -> { + boolean acknowledged = (Boolean) a[0]; + LicensesStatus licensesStatus = LicensesStatus.fromString((String) a[1]); + @SuppressWarnings("unchecked") Tuple> acknowledgements = (Tuple>) a[2]; + if (acknowledgements == null) { + return new PutLicenseResponse(acknowledged, licensesStatus); + } else { + return new PutLicenseResponse(acknowledged, licensesStatus, acknowledgements.v1(), acknowledgements.v2()); + } + + }); + + static { + PARSER.declareBoolean(constructorArg(), new ParseField("acknowledged")); + PARSER.declareString(constructorArg(), new ParseField("license_status")); + PARSER.declareObject(optionalConstructorArg(), (parser, v) -> { + Map acknowledgeMessages = new HashMap<>(); + String message = null; + XContentParser.Token token; + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + if (currentFieldName == null) { + throw new XContentParseException(parser.getTokenLocation(), "expected message header or acknowledgement"); + } + if ("message".equals(currentFieldName)) { + if (token != XContentParser.Token.VALUE_STRING) { + throw new XContentParseException(parser.getTokenLocation(), "unexpected message header type"); + } + message = parser.text(); + } else { + if (token != XContentParser.Token.START_ARRAY) { + throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement type"); + } + List acknowledgeMessagesList = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + if (token != XContentParser.Token.VALUE_STRING) { + throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement text"); + } + acknowledgeMessagesList.add(parser.text()); + } + acknowledgeMessages.put(currentFieldName, acknowledgeMessagesList.toArray(new String[0])); + } + } + } + return new Tuple<>(message, acknowledgeMessages); + }, + new ParseField("acknowledge")); + } + + private LicensesStatus status; + private Map acknowledgeMessages; + private String acknowledgeHeader; + + public PutLicenseResponse() { + } + + public PutLicenseResponse(boolean acknowledged, LicensesStatus status) { + this(acknowledged, status, null, Collections.emptyMap()); + } + + public PutLicenseResponse(boolean acknowledged, LicensesStatus status, String acknowledgeHeader, + Map acknowledgeMessages) { + super(acknowledged); + this.status = status; + this.acknowledgeHeader = acknowledgeHeader; + this.acknowledgeMessages = acknowledgeMessages; + } + + public LicensesStatus status() { + return status; + } + + public Map acknowledgeMessages() { + return acknowledgeMessages; + } + + public String acknowledgeHeader() { + return acknowledgeHeader; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + status = LicensesStatus.fromId(in.readVInt()); + acknowledgeHeader = in.readOptionalString(); + int size = in.readVInt(); + Map acknowledgeMessages = new HashMap<>(size); + for (int i = 0; i < size; i++) { + String feature = in.readString(); + int nMessages = in.readVInt(); + String[] messages = new String[nMessages]; + for (int j = 0; j < nMessages; j++) { + messages[j] = in.readString(); + } + acknowledgeMessages.put(feature, messages); + } + this.acknowledgeMessages = acknowledgeMessages; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeVInt(status.id()); + out.writeOptionalString(acknowledgeHeader); + out.writeVInt(acknowledgeMessages.size()); + for (Map.Entry entry : acknowledgeMessages.entrySet()) { + out.writeString(entry.getKey()); + out.writeVInt(entry.getValue().length); + for (String message : entry.getValue()) { + out.writeString(message); + } + } + } + + @Override + protected void addCustomFields(XContentBuilder builder, Params params) throws IOException { + builder.field("license_status", status.toString()); + if (!acknowledgeMessages.isEmpty()) { + builder.startObject("acknowledge"); + builder.field("message", acknowledgeHeader); + for (Map.Entry entry : acknowledgeMessages.entrySet()) { + builder.startArray(entry.getKey()); + for (String message : entry.getValue()) { + builder.value(message); + } + builder.endArray(); + } + builder.endObject(); + } + } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } + + public static PutLicenseResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + PutLicenseResponse that = (PutLicenseResponse) o; + + return status == that.status && + ProtocolUtils.equals(acknowledgeMessages, that.acknowledgeMessages) && + Objects.equals(acknowledgeHeader, that.acknowledgeHeader); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), status, ProtocolUtils.hashCode(acknowledgeMessages), acknowledgeHeader); + } + + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/package-info.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/package-info.java new file mode 100644 index 000000000000..a0a80a9958b9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/license/package-info.java @@ -0,0 +1,11 @@ +/* + * 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. + */ + +/** + * Request and Response objects for the default distribution's License + * APIs. + */ +package org.elasticsearch.protocol.xpack.license; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequest.java new file mode 100644 index 000000000000..17afee59fa15 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequest.java @@ -0,0 +1,85 @@ +/* + * 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.protocol.xpack.migration; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.master.MasterNodeReadRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +public class IndexUpgradeInfoRequest extends MasterNodeReadRequest implements IndicesRequest.Replaceable { + + private String[] indices = Strings.EMPTY_ARRAY; + private IndicesOptions indicesOptions = IndicesOptions.fromOptions(false, true, true, true); + + public IndexUpgradeInfoRequest(String... indices) { + indices(indices); + } + + public IndexUpgradeInfoRequest(StreamInput in) throws IOException { + super(in); + indices = in.readStringArray(); + indicesOptions = IndicesOptions.readIndicesOptions(in); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeStringArray(indices); + indicesOptions.writeIndicesOptions(out); + } + + @Override + public String[] indices() { + return indices; + } + + @Override + public IndexUpgradeInfoRequest indices(String... indices) { + this.indices = Objects.requireNonNull(indices, "indices cannot be null"); + return this; + } + + @Override + public IndicesOptions indicesOptions() { + return indicesOptions; + } + + public void indicesOptions(IndicesOptions indicesOptions) { + this.indicesOptions = indicesOptions; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexUpgradeInfoRequest request = (IndexUpgradeInfoRequest) o; + return Arrays.equals(indices, request.indices) && + Objects.equals(indicesOptions.toString(), request.indicesOptions.toString()); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(indices), indicesOptions.toString()); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java new file mode 100644 index 000000000000..17115ac9b171 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java @@ -0,0 +1,120 @@ +/* + * 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.protocol.xpack.migration; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +public class IndexUpgradeInfoResponse extends ActionResponse implements ToXContentObject { + + private static final ParseField INDICES = new ParseField("indices"); + private static final ParseField ACTION_REQUIRED = new ParseField("action_required"); + + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("IndexUpgradeInfoResponse", + true, + (a, c) -> { + @SuppressWarnings("unchecked") + Map map = (Map)a[0]; + Map actionsRequired = map.entrySet().stream() + .filter(e -> { + if (e.getValue() instanceof Map == false) { + return false; + } + @SuppressWarnings("unchecked") + Map value =(Map)e.getValue(); + return value.containsKey(ACTION_REQUIRED.getPreferredName()); + }) + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> { + @SuppressWarnings("unchecked") + Map value = (Map) e.getValue(); + return UpgradeActionRequired.fromString((String)value.get(ACTION_REQUIRED.getPreferredName())); + } + )); + return new IndexUpgradeInfoResponse(actionsRequired); + }); + + static { + PARSER.declareObject(constructorArg(), (p, c) -> p.map(), INDICES); + } + + + private Map actions; + + public IndexUpgradeInfoResponse() { + + } + + public IndexUpgradeInfoResponse(Map actions) { + this.actions = actions; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + actions = in.readMap(StreamInput::readString, UpgradeActionRequired::readFromStream); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeMap(actions, StreamOutput::writeString, (out1, value) -> value.writeTo(out1)); + } + + public Map getActions() { + return actions; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.startObject(INDICES.getPreferredName()); + for (Map.Entry entry : actions.entrySet()) { + builder.startObject(entry.getKey()); + { + builder.field(ACTION_REQUIRED.getPreferredName(), entry.getValue().toString()); + } + builder.endObject(); + } + builder.endObject(); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndexUpgradeInfoResponse response = (IndexUpgradeInfoResponse) o; + return Objects.equals(actions, response.actions); + } + + @Override + public int hashCode() { + return Objects.hash(actions); + } + + public static IndexUpgradeInfoResponse fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/UpgradeActionRequired.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/UpgradeActionRequired.java new file mode 100644 index 000000000000..dce1c7d18f50 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/UpgradeActionRequired.java @@ -0,0 +1,42 @@ +/* + * 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.protocol.xpack.migration; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; + +import java.io.IOException; +import java.util.Locale; + +/** + * Indicates the type of the upgrade required for the index + */ +public enum UpgradeActionRequired implements Writeable { + NOT_APPLICABLE, // Indicates that the check is not applicable to this index type, the next check will be performed + UP_TO_DATE, // Indicates that the check finds this index to be up to date - no additional checks are required + REINDEX, // The index should be reindex + UPGRADE; // The index should go through the upgrade procedure + + public static UpgradeActionRequired fromString(String value) { + return UpgradeActionRequired.valueOf(value.toUpperCase(Locale.ROOT)); + } + + public static UpgradeActionRequired readFromStream(StreamInput in) throws IOException { + return in.readEnum(UpgradeActionRequired.class); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(this); + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/package-info.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/package-info.java new file mode 100644 index 000000000000..7c52f6a8fd4f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/package-info.java @@ -0,0 +1,11 @@ +/* + * 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. + */ + +/** + * Request and Response objects for the default distribution's Migration + * APIs. + */ +package org.elasticsearch.protocol.xpack.migration; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/package-info.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/package-info.java new file mode 100644 index 000000000000..3ed877d08ccc --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/package-info.java @@ -0,0 +1,10 @@ +/* + * 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. + */ + +/** + * Request and Response objects for miscellaneous X-Pack APIs. + */ +package org.elasticsearch.protocol.xpack; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java new file mode 100644 index 000000000000..e5b116a3a7a9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java @@ -0,0 +1,246 @@ +/* + * 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.protocol.xpack.security; + +import org.elasticsearch.Version; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +/** + * An authenticated user + */ +public class User implements ToXContentObject { + + private final String username; + private final String[] roles; + private final User authenticatedUser; + private final Map metadata; + private final boolean enabled; + + @Nullable private final String fullName; + @Nullable private final String email; + + public User(String username, String... roles) { + this(username, roles, null, null, null, true); + } + + public User(String username, String[] roles, User authenticatedUser) { + this(username, roles, null, null, null, true, authenticatedUser); + } + + public User(User user, User authenticatedUser) { + this(user.principal(), user.roles(), user.fullName(), user.email(), user.metadata(), user.enabled(), authenticatedUser); + } + + public User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled) { + this(username, roles, fullName, email, metadata, enabled, null); + } + + private User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled, + User authenticatedUser) { + this.username = username; + this.roles = roles == null ? Strings.EMPTY_ARRAY : roles; + this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + this.fullName = fullName; + this.email = email; + this.enabled = enabled; + assert (authenticatedUser == null || authenticatedUser.isRunAs() == false) : "the authenticated user should not be a run_as user"; + this.authenticatedUser = authenticatedUser; + } + + /** + * @return The principal of this user - effectively serving as the + * unique identity of of the user. + */ + public String principal() { + return this.username; + } + + /** + * @return The roles this user is associated with. The roles are + * identified by their unique names and each represents as + * set of permissions + */ + public String[] roles() { + return this.roles; + } + + /** + * @return The metadata that is associated with this user. Can never be {@code null}. + */ + public Map metadata() { + return metadata; + } + + /** + * @return The full name of this user. May be {@code null}. + */ + public String fullName() { + return fullName; + } + + /** + * @return The email of this user. May be {@code null}. + */ + public String email() { + return email; + } + + /** + * @return whether the user is enabled or not + */ + public boolean enabled() { + return enabled; + } + + /** + * @return The user that was originally authenticated. + * This may be the user itself, or a different user which used runAs. + */ + public User authenticatedUser() { + return authenticatedUser == null ? this : authenticatedUser; + } + + /** Return true if this user was not the originally authenticated user, false otherwise. */ + public boolean isRunAs() { + return authenticatedUser != null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("User[username=").append(username); + sb.append(",roles=[").append(Strings.arrayToCommaDelimitedString(roles)).append("]"); + sb.append(",fullName=").append(fullName); + sb.append(",email=").append(email); + sb.append(",metadata="); + sb.append(metadata); + if (authenticatedUser != null) { + sb.append(",authenticatedUser=[").append(authenticatedUser.toString()).append("]"); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof User == false) return false; + + User user = (User) o; + + if (!username.equals(user.username)) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(roles, user.roles)) return false; + if (authenticatedUser != null ? !authenticatedUser.equals(user.authenticatedUser) : user.authenticatedUser != null) return false; + if (!metadata.equals(user.metadata)) return false; + if (fullName != null ? !fullName.equals(user.fullName) : user.fullName != null) return false; + return !(email != null ? !email.equals(user.email) : user.email != null); + + } + + @Override + public int hashCode() { + int result = username.hashCode(); + result = 31 * result + Arrays.hashCode(roles); + result = 31 * result + (authenticatedUser != null ? authenticatedUser.hashCode() : 0); + result = 31 * result + metadata.hashCode(); + result = 31 * result + (fullName != null ? fullName.hashCode() : 0); + result = 31 * result + (email != null ? email.hashCode() : 0); + return result; + } + + @Override + public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Fields.USERNAME.getPreferredName(), principal()); + builder.array(Fields.ROLES.getPreferredName(), roles()); + builder.field(Fields.FULL_NAME.getPreferredName(), fullName()); + builder.field(Fields.EMAIL.getPreferredName(), email()); + builder.field(Fields.METADATA.getPreferredName(), metadata()); + builder.field(Fields.ENABLED.getPreferredName(), enabled()); + return builder.endObject(); + } + + public static User partialReadFrom(String username, StreamInput input) throws IOException { + String[] roles = input.readStringArray(); + Map metadata = input.readMap(); + String fullName = input.readOptionalString(); + String email = input.readOptionalString(); + boolean enabled = input.readBoolean(); + User outerUser = new User(username, roles, fullName, email, metadata, enabled, null); + boolean hasInnerUser = input.readBoolean(); + if (hasInnerUser) { + User innerUser = readFrom(input); + if (input.getVersion().onOrBefore(Version.V_5_4_0)) { + // backcompat: runas user was read first, so reverse outer and inner + return new User(innerUser, outerUser); + } else { + return new User(outerUser, innerUser); + } + } else { + return outerUser; + } + } + + public static User readFrom(StreamInput input) throws IOException { + final boolean isInternalUser = input.readBoolean(); + assert isInternalUser == false: "should always return false. Internal users should use the InternalUserSerializationHelper"; + final String username = input.readString(); + return partialReadFrom(username, input); + } + + public static void writeTo(User user, StreamOutput output) throws IOException { + if (user.authenticatedUser == null) { + // no backcompat necessary, since there is no inner user + writeUser(user, output); + } else if (output.getVersion().onOrBefore(Version.V_5_4_0)) { + // backcompat: write runas user as the "inner" user + writeUser(user.authenticatedUser, output); + output.writeBoolean(true); + writeUser(user, output); + } else { + writeUser(user, output); + output.writeBoolean(true); + writeUser(user.authenticatedUser, output); + } + output.writeBoolean(false); // last user written, regardless of bwc, does not have an inner user + } + + /** Write just the given {@link User}, but not the inner {@link #authenticatedUser}. */ + private static void writeUser(User user, StreamOutput output) throws IOException { + output.writeBoolean(false); // not a system user + output.writeString(user.username); + output.writeStringArray(user.roles); + output.writeMap(user.metadata); + output.writeOptionalString(user.fullName); + output.writeOptionalString(user.email); + output.writeBoolean(user.enabled); + } + + public interface Fields { + ParseField USERNAME = new ParseField("username"); + ParseField PASSWORD = new ParseField("password"); + ParseField PASSWORD_HASH = new ParseField("password_hash"); + ParseField ROLES = new ParseField("roles"); + ParseField FULL_NAME = new ParseField("full_name"); + ParseField EMAIL = new ParseField("email"); + ParseField METADATA = new ParseField("metadata"); + ParseField ENABLED = new ParseField("enabled"); + ParseField TYPE = new ParseField("type"); + } +} + diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java new file mode 100644 index 000000000000..ce627b267f31 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java @@ -0,0 +1,11 @@ +/* + * 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. + */ + +/** + * Request and Response objects for the default distribution's Security + * APIs. + */ +package org.elasticsearch.protocol.xpack.security; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchRequest.java new file mode 100644 index 000000000000..4a458b69a750 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchRequest.java @@ -0,0 +1,76 @@ +/* + * 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.protocol.xpack.watcher; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ValidateActions; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lucene.uid.Versions; + +import java.io.IOException; + +/** + * A delete watch request to delete an watch by name (id) + */ +public class DeleteWatchRequest extends ActionRequest { + + private String id; + private long version = Versions.MATCH_ANY; + + public DeleteWatchRequest() { + this(null); + } + + public DeleteWatchRequest(String id) { + this.id = id; + } + + /** + * @return The name of the watch to be deleted + */ + public String getId() { + return id; + } + + /** + * Sets the name of the watch to be deleted + */ + public void setId(String id) { + this.id = id; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (id == null){ + validationException = ValidateActions.addValidationError("watch id is missing", validationException); + } else if (PutWatchRequest.isValidId(id) == false) { + validationException = ValidateActions.addValidationError("watch id contains whitespace", validationException); + } + return validationException; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + id = in.readString(); + version = in.readLong(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(id); + out.writeLong(version); + } + + @Override + public String toString() { + return "delete [" + id + "]"; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponse.java new file mode 100644 index 000000000000..39cd5e966fa1 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponse.java @@ -0,0 +1,110 @@ +/* + * 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.protocol.xpack.watcher; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +public class DeleteWatchResponse extends ActionResponse implements ToXContentObject { + + private static final ObjectParser PARSER + = new ObjectParser<>("x_pack_delete_watch_response", DeleteWatchResponse::new); + static { + PARSER.declareString(DeleteWatchResponse::setId, new ParseField("_id")); + PARSER.declareLong(DeleteWatchResponse::setVersion, new ParseField("_version")); + PARSER.declareBoolean(DeleteWatchResponse::setFound, new ParseField("found")); + } + + private String id; + private long version; + private boolean found; + + public DeleteWatchResponse() { + } + + public DeleteWatchResponse(String id, long version, boolean found) { + this.id = id; + this.version = version; + this.found = found; + } + + public String getId() { + return id; + } + + public long getVersion() { + return version; + } + + public boolean isFound() { + return found; + } + + private void setId(String id) { + this.id = id; + } + + private void setVersion(long version) { + this.version = version; + } + + private void setFound(boolean found) { + this.found = found; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DeleteWatchResponse that = (DeleteWatchResponse) o; + + return Objects.equals(id, that.id) && Objects.equals(version, that.version) && Objects.equals(found, that.found); + } + + @Override + public int hashCode() { + return Objects.hash(id, version, found); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + id = in.readString(); + version = in.readVLong(); + found = in.readBoolean(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(id); + out.writeVLong(version); + out.writeBoolean(found); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field("_id", id) + .field("_version", version) + .field("found", found) + .endObject(); + } + + public static DeleteWatchResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchRequest.java new file mode 100644 index 000000000000..7997d853db37 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchRequest.java @@ -0,0 +1,145 @@ +/* + * 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.protocol.xpack.watcher; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ValidateActions; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lucene.uid.Versions; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.util.regex.Pattern; + +/** + * This request class contains the data needed to create a watch along with the name of the watch. + * The name of the watch will become the ID of the indexed document. + */ +public final class PutWatchRequest extends ActionRequest { + + private static final Pattern NO_WS_PATTERN = Pattern.compile("\\S+"); + + private String id; + private BytesReference source; + private XContentType xContentType = XContentType.JSON; + private boolean active = true; + private long version = Versions.MATCH_ANY; + + public PutWatchRequest() {} + + public PutWatchRequest(StreamInput in) throws IOException { + readFrom(in); + } + + public PutWatchRequest(String id, BytesReference source, XContentType xContentType) { + this.id = id; + this.source = source; + this.xContentType = xContentType; + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + id = in.readString(); + source = in.readBytesReference(); + active = in.readBoolean(); + xContentType = in.readEnum(XContentType.class); + version = in.readZLong(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(id); + out.writeBytesReference(source); + out.writeBoolean(active); + out.writeEnum(xContentType); + out.writeZLong(version); + } + + /** + * @return The name that will be the ID of the indexed document + */ + public String getId() { + return id; + } + + /** + * Set the watch name + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return The source of the watch + */ + public BytesReference getSource() { + return source; + } + + /** + * Set the source of the watch + */ + public void setSource(BytesReference source, XContentType xContentType) { + this.source = source; + this.xContentType = xContentType; + } + + /** + * @return The initial active state of the watch (defaults to {@code true}, e.g. "active") + */ + public boolean isActive() { + return active; + } + + /** + * Sets the initial active state of the watch + */ + public void setActive(boolean active) { + this.active = active; + } + + /** + * Get the content type for the source + */ + public XContentType xContentType() { + return xContentType; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (id == null) { + validationException = ValidateActions.addValidationError("watch id is missing", validationException); + } else if (isValidId(id) == false) { + validationException = ValidateActions.addValidationError("watch id contains whitespace", validationException); + } + if (source == null) { + validationException = ValidateActions.addValidationError("watch source is missing", validationException); + } + if (xContentType == null) { + validationException = ValidateActions.addValidationError("request body is missing", validationException); + } + return validationException; + } + + public static boolean isValidId(String id) { + return Strings.isEmpty(id) == false && NO_WS_PATTERN.matcher(id).matches(); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponse.java new file mode 100644 index 000000000000..f6e55ff55533 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponse.java @@ -0,0 +1,111 @@ +/* + * 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.protocol.xpack.watcher; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +public class PutWatchResponse extends ActionResponse implements ToXContentObject { + + private static final ObjectParser PARSER + = new ObjectParser<>("x_pack_put_watch_response", PutWatchResponse::new); + static { + PARSER.declareString(PutWatchResponse::setId, new ParseField("_id")); + PARSER.declareLong(PutWatchResponse::setVersion, new ParseField("_version")); + PARSER.declareBoolean(PutWatchResponse::setCreated, new ParseField("created")); + } + + private String id; + private long version; + private boolean created; + + public PutWatchResponse() { + } + + public PutWatchResponse(String id, long version, boolean created) { + this.id = id; + this.version = version; + this.created = created; + } + + private void setId(String id) { + this.id = id; + } + + private void setVersion(long version) { + this.version = version; + } + + private void setCreated(boolean created) { + this.created = created; + } + + public String getId() { + return id; + } + + public long getVersion() { + return version; + } + + public boolean isCreated() { + return created; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PutWatchResponse that = (PutWatchResponse) o; + + return Objects.equals(id, that.id) && Objects.equals(version, that.version) && Objects.equals(created, that.created); + } + + @Override + public int hashCode() { + return Objects.hash(id, version, created); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(id); + out.writeVLong(version); + out.writeBoolean(created); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + id = in.readString(); + version = in.readVLong(); + created = in.readBoolean(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field("_id", id) + .field("_version", version) + .field("created", created) + .endObject(); + } + + public static PutWatchResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/package-info.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/package-info.java new file mode 100644 index 000000000000..0d9edf3b5c03 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/watcher/package-info.java @@ -0,0 +1,11 @@ +/* + * 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. + */ + +/** + * Request and Response objects for the default distribution's Watcher + * APIs. + */ +package org.elasticsearch.protocol.xpack.watcher; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/XPackInfoResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/XPackInfoResponseTests.java new file mode 100644 index 000000000000..fac99959c536 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/XPackInfoResponseTests.java @@ -0,0 +1,146 @@ +/* + * 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.protocol.xpack; + +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.protocol.xpack.XPackInfoResponse.BuildInfo; +import org.elasticsearch.protocol.xpack.XPackInfoResponse.LicenseInfo; +import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo; +import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo.FeatureSet; +import org.elasticsearch.protocol.xpack.license.LicenseStatus; +import org.elasticsearch.test.AbstractStreamableXContentTestCase; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.io.IOException; + +public class XPackInfoResponseTests extends AbstractStreamableXContentTestCase { + @Override + protected XPackInfoResponse doParseInstance(XContentParser parser) throws IOException { + return XPackInfoResponse.fromXContent(parser); + } + + @Override + protected XPackInfoResponse createBlankInstance() { + return new XPackInfoResponse(); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return path -> path.equals("features") + || (path.startsWith("features") && path.endsWith("native_code_info")); + } + + @Override + protected ToXContent.Params getToXContentParams() { + Map params = new HashMap<>(); + if (randomBoolean()) { + params.put("human", randomBoolean() ? "true" : "false"); + } + if (randomBoolean()) { + params.put("categories", "_none"); + } + return new ToXContent.MapParams(params); + } + + @Override + protected XPackInfoResponse createTestInstance() { + return new XPackInfoResponse( + randomBoolean() ? null : randomBuildInfo(), + randomBoolean() ? null : randomLicenseInfo(), + randomBoolean() ? null : randomFeatureSetsInfo()); + } + + @Override + protected XPackInfoResponse mutateInstance(XPackInfoResponse response) { + @SuppressWarnings("unchecked") + Function mutator = randomFrom( + r -> new XPackInfoResponse( + mutateBuildInfo(r.getBuildInfo()), + r.getLicenseInfo(), + r.getFeatureSetsInfo()), + r -> new XPackInfoResponse( + r.getBuildInfo(), + mutateLicenseInfo(r.getLicenseInfo()), + r.getFeatureSetsInfo()), + r -> new XPackInfoResponse( + r.getBuildInfo(), + r.getLicenseInfo(), + mutateFeatureSetsInfo(r.getFeatureSetsInfo()))); + return mutator.apply(response); + } + + private BuildInfo randomBuildInfo() { + return new BuildInfo( + randomAlphaOfLength(10), + randomAlphaOfLength(15)); + } + + private BuildInfo mutateBuildInfo(BuildInfo buildInfo) { + if (buildInfo == null) { + return randomBuildInfo(); + } + return null; + } + + private LicenseInfo randomLicenseInfo() { + return new LicenseInfo( + randomAlphaOfLength(10), + randomAlphaOfLength(4), + randomAlphaOfLength(5), + randomFrom(LicenseStatus.values()), + randomLong()); + } + + private LicenseInfo mutateLicenseInfo(LicenseInfo licenseInfo) { + if (licenseInfo == null) { + return randomLicenseInfo(); + } + return null; + } + + private FeatureSetsInfo randomFeatureSetsInfo() { + int size = between(0, 10); + Set featureSets = new HashSet<>(size); + while (featureSets.size() < size) { + featureSets.add(randomFeatureSet()); + } + return new FeatureSetsInfo(featureSets); + } + + private FeatureSetsInfo mutateFeatureSetsInfo(FeatureSetsInfo featureSetsInfo) { + if (featureSetsInfo == null) { + return randomFeatureSetsInfo(); + } + return null; + } + + private FeatureSet randomFeatureSet() { + return new FeatureSet( + randomAlphaOfLength(5), + randomBoolean() ? null : randomAlphaOfLength(20), + randomBoolean(), + randomBoolean(), + randomNativeCodeInfo()); + } + + private Map randomNativeCodeInfo() { + if (randomBoolean()) { + return null; + } + int size = between(0, 10); + Map nativeCodeInfo = new HashMap<>(size); + while (nativeCodeInfo.size() < size) { + nativeCodeInfo.put(randomAlphaOfLength(5), randomAlphaOfLength(5)); + } + return nativeCodeInfo; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/common/ProtocolUtilsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/common/ProtocolUtilsTests.java new file mode 100644 index 000000000000..c4e29d7c2300 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/common/ProtocolUtilsTests.java @@ -0,0 +1,58 @@ +/* + * 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.protocol.xpack.common; + +import org.elasticsearch.test.ESTestCase; + +import java.util.HashMap; +import java.util.Map; + +public class ProtocolUtilsTests extends ESTestCase { + + public void testMapStringEqualsAndHash() { + assertTrue(ProtocolUtils.equals(null, null)); + assertFalse(ProtocolUtils.equals(null, new HashMap<>())); + assertFalse(ProtocolUtils.equals(new HashMap<>(), null)); + + Map a = new HashMap<>(); + a.put("foo", new String[] { "a", "b" }); + a.put("bar", new String[] { "b", "c" }); + + Map b = new HashMap<>(); + b.put("foo", new String[] { "a", "b" }); + + assertFalse(ProtocolUtils.equals(a, b)); + assertFalse(ProtocolUtils.equals(b, a)); + + b.put("bar", new String[] { "c", "b" }); + + assertFalse(ProtocolUtils.equals(a, b)); + assertFalse(ProtocolUtils.equals(b, a)); + + b.put("bar", new String[] { "b", "c" }); + + assertTrue(ProtocolUtils.equals(a, b)); + assertTrue(ProtocolUtils.equals(b, a)); + assertEquals(ProtocolUtils.hashCode(a), ProtocolUtils.hashCode(b)); + + b.put("baz", new String[] { "b", "c" }); + + assertFalse(ProtocolUtils.equals(a, b)); + assertFalse(ProtocolUtils.equals(b, a)); + + a.put("non", null); + + assertFalse(ProtocolUtils.equals(a, b)); + assertFalse(ProtocolUtils.equals(b, a)); + + b.put("non", null); + b.remove("baz"); + + assertTrue(ProtocolUtils.equals(a, b)); + assertTrue(ProtocolUtils.equals(b, a)); + assertEquals(ProtocolUtils.hashCode(a), ProtocolUtils.hashCode(b)); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java new file mode 100644 index 000000000000..4331bdd37807 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.protocol.xpack.graph; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.ShardOperationFailedException; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; + +public class GraphExploreResponseTests extends AbstractXContentTestCase< GraphExploreResponse> { + + @Override + protected GraphExploreResponse createTestInstance() { + return createInstance(0); + } + private static GraphExploreResponse createInstance(int numFailures) { + int numItems = randomIntBetween(4, 128); + boolean timedOut = randomBoolean(); + boolean showDetails = randomBoolean(); + long overallTookInMillis = randomNonNegativeLong(); + Map vertices = new HashMap<>(); + Map connections = new HashMap<>(); + ShardOperationFailedException [] failures = new ShardOperationFailedException [numFailures]; + for (int i = 0; i < failures.length; i++) { + failures[i] = new ShardSearchFailure(new ElasticsearchException("an error")); + } + + //Create random set of vertices + for (int i = 0; i < numItems; i++) { + Vertex v = new Vertex("field1", randomAlphaOfLength(5), randomDouble(), 0, + showDetails?randomIntBetween(100, 200):0, + showDetails?randomIntBetween(1, 100):0); + vertices.put(v.getId(), v); + } + + //Wire up half the vertices randomly + Vertex[] vs = vertices.values().toArray(new Vertex[vertices.size()]); + for (int i = 0; i < numItems/2; i++) { + Vertex v1 = vs[randomIntBetween(0, vs.length-1)]; + Vertex v2 = vs[randomIntBetween(0, vs.length-1)]; + if(v1 != v2) { + Connection conn = new Connection(v1, v2, randomDouble(), randomLongBetween(1, 10)); + connections.put(conn.getId(), conn); + } + } + return new GraphExploreResponse(overallTookInMillis, timedOut, failures, vertices, connections, showDetails); + } + + + private static GraphExploreResponse createTestInstanceWithFailures() { + return createInstance(randomIntBetween(1, 128)); + } + + @Override + protected GraphExploreResponse doParseInstance(XContentParser parser) throws IOException { + return GraphExploreResponse.fromXContext(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { + return field -> field.startsWith("responses"); + } + + @Override + protected void assertEqualInstances( GraphExploreResponse expectedInstance, GraphExploreResponse newInstance) { + assertThat(newInstance.getTook(), equalTo(expectedInstance.getTook())); + assertThat(newInstance.isTimedOut(), equalTo(expectedInstance.isTimedOut())); + + Connection[] newConns = newInstance.getConnections().toArray(new Connection[0]); + Connection[] expectedConns = expectedInstance.getConnections().toArray(new Connection[0]); + assertArrayEquals(expectedConns, newConns); + + Vertex[] newVertices = newInstance.getVertices().toArray(new Vertex[0]); + Vertex[] expectedVertices = expectedInstance.getVertices().toArray(new Vertex[0]); + assertArrayEquals(expectedVertices, newVertices); + + ShardOperationFailedException[] newFailures = newInstance.getShardFailures(); + ShardOperationFailedException[] expectedFailures = expectedInstance.getShardFailures(); + assertEquals(expectedFailures.length, newFailures.length); + + } + + /** + * Test parsing {@link GraphExploreResponse} with inner failures as they don't support asserting on xcontent equivalence, given + * exceptions are not parsed back as the same original class. We run the usual {@link AbstractXContentTestCase#testFromXContent()} + * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. + */ + public void testFromXContentWithFailures() throws IOException { + Supplier< GraphExploreResponse> instanceSupplier = GraphExploreResponseTests::createTestInstanceWithFailures; + //with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, + //but that does not bother our assertions, as we only want to test that we don't break. + boolean supportsUnknownFields = true; + //exceptions are not of the same type whenever parsed back + boolean assertToXContentEquivalence = false; + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, + this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/LicenseStatusTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/LicenseStatusTests.java new file mode 100644 index 000000000000..7149477d0076 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/LicenseStatusTests.java @@ -0,0 +1,17 @@ +/* + * 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.protocol.xpack.license; + +import java.io.IOException; + +import org.elasticsearch.test.ESTestCase; + +public class LicenseStatusTests extends ESTestCase { + public void testSerialization() throws IOException { + LicenseStatus status = randomFrom(LicenseStatus.values()); + assertSame(status, copyWriteable(status, writableRegistry(), LicenseStatus::readFrom)); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponseTests.java new file mode 100644 index 000000000000..a09fd6fb99b4 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponseTests.java @@ -0,0 +1,112 @@ +/* + * 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.protocol.xpack.license; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractStreamableXContentTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +public class PutLicenseResponseTests extends AbstractStreamableXContentTestCase { + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + // The structure of the response is such that unknown fields inside acknowledge cannot be supported since they + // are treated as messages from new services + return p -> p.startsWith("acknowledge"); + } + + @Override + protected PutLicenseResponse createTestInstance() { + boolean acknowledged = randomBoolean(); + LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED); + String messageHeader; + Map ackMessages; + if (randomBoolean()) { + messageHeader = randomAlphaOfLength(10); + ackMessages = randomAckMessages(); + } else { + messageHeader = null; + ackMessages = Collections.emptyMap(); + } + + return new PutLicenseResponse(acknowledged, status, messageHeader, ackMessages); + } + + private static Map randomAckMessages() { + int nFeatures = randomIntBetween(1, 5); + + Map ackMessages = new HashMap<>(); + + for (int i = 0; i < nFeatures; i++) { + String feature = randomAlphaOfLengthBetween(9, 15); + int nMessages = randomIntBetween(1, 5); + String[] messages = new String[nMessages]; + for (int j = 0; j < nMessages; j++) { + messages[j] = randomAlphaOfLengthBetween(10, 30); + } + ackMessages.put(feature, messages); + } + + return ackMessages; + } + + @Override + protected PutLicenseResponse doParseInstance(XContentParser parser) throws IOException { + return PutLicenseResponse.fromXContent(parser); + } + + @Override + protected PutLicenseResponse createBlankInstance() { + return new PutLicenseResponse(); + } + + @Override + protected PutLicenseResponse mutateInstance(PutLicenseResponse response) { + @SuppressWarnings("unchecked") + Function mutator = randomFrom( + r -> new PutLicenseResponse( + r.isAcknowledged() == false, + r.status(), + r.acknowledgeHeader(), + r.acknowledgeMessages()), + r -> new PutLicenseResponse( + r.isAcknowledged(), + mutateStatus(r.status()), + r.acknowledgeHeader(), + r.acknowledgeMessages()), + r -> { + if (r.acknowledgeMessages().isEmpty()) { + return new PutLicenseResponse( + r.isAcknowledged(), + r.status(), + randomAlphaOfLength(10), + randomAckMessages() + ); + } else { + return new PutLicenseResponse(r.isAcknowledged(), r.status()); + } + } + + ); + return mutator.apply(response); + } + + private LicensesStatus mutateStatus(LicensesStatus status) { + return randomValueOtherThan(status, () -> randomFrom(LicensesStatus.values())); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequestTests.java new file mode 100644 index 000000000000..0e09a05fb967 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoRequestTests.java @@ -0,0 +1,36 @@ +/* + * 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.protocol.xpack.migration; + +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +public class IndexUpgradeInfoRequestTests extends AbstractWireSerializingTestCase { + @Override + protected IndexUpgradeInfoRequest createTestInstance() { + int indexCount = randomInt(4); + String[] indices = new String[indexCount]; + for (int i = 0; i < indexCount; i++) { + indices[i] = randomAlphaOfLength(10); + } + IndexUpgradeInfoRequest request = new IndexUpgradeInfoRequest(indices); + if (randomBoolean()) { + request.indicesOptions(IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), randomBoolean(), randomBoolean())); + } + return request; + } + + @Override + protected Writeable.Reader instanceReader() { + return IndexUpgradeInfoRequest::new; + } + + public void testNullIndices() { + expectThrows(NullPointerException.class, () -> new IndexUpgradeInfoRequest((String[])null)); + expectThrows(NullPointerException.class, () -> new IndexUpgradeInfoRequest().indices((String[])null)); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java new file mode 100644 index 000000000000..57f01a4454e0 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java @@ -0,0 +1,54 @@ +/* + * 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.protocol.xpack.migration; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractStreamableXContentTestCase; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class IndexUpgradeInfoResponseTests extends AbstractStreamableXContentTestCase { + @Override + protected IndexUpgradeInfoResponse doParseInstance(XContentParser parser) { + return IndexUpgradeInfoResponse.fromXContent(parser); + } + + @Override + protected IndexUpgradeInfoResponse createBlankInstance() { + return new IndexUpgradeInfoResponse(); + } + + @Override + protected IndexUpgradeInfoResponse createTestInstance() { + return randomIndexUpgradeInfoResponse(randomIntBetween(0, 10)); + } + + private static IndexUpgradeInfoResponse randomIndexUpgradeInfoResponse(int numIndices) { + Map actions = new HashMap<>(); + for (int i = 0; i < numIndices; i++) { + actions.put(randomAlphaOfLength(5), randomFrom(UpgradeActionRequired.values())); + } + return new IndexUpgradeInfoResponse(actions); + } + + @Override + protected IndexUpgradeInfoResponse mutateInstance(IndexUpgradeInfoResponse instance) { + if (instance.getActions().size() == 0) { + return randomIndexUpgradeInfoResponse(1); + } + Map actions = new HashMap<>(instance.getActions()); + if (randomBoolean()) { + Iterator> iterator = actions.entrySet().iterator(); + iterator.next(); + iterator.remove(); + } else { + actions.put(randomAlphaOfLength(5), randomFrom(UpgradeActionRequired.values())); + } + return new IndexUpgradeInfoResponse(actions); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java new file mode 100644 index 000000000000..28a27e639985 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java @@ -0,0 +1,25 @@ +/* + * 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.protocol.xpack.security; + +import org.elasticsearch.test.ESTestCase; + +import java.util.Collections; + +import static org.hamcrest.Matchers.is; + +public class UserTests extends ESTestCase { + + public void testUserToString() { + User user = new User("u1", "r1"); + assertThat(user.toString(), is("User[username=u1,roles=[r1],fullName=null,email=null,metadata={}]")); + user = new User("u1", new String[] { "r1", "r2" }, "user1", "user1@domain.com", Collections.singletonMap("key", "val"), true); + assertThat(user.toString(), is("User[username=u1,roles=[r1,r2],fullName=user1,email=user1@domain.com,metadata={key=val}]")); + user = new User("u1", new String[] {"r1"}, new User("u2", "r2", "r3")); + assertThat(user.toString(), is("User[username=u1,roles=[r1],fullName=null,email=null,metadata={}," + + "authenticatedUser=[User[username=u2,roles=[r2,r3],fullName=null,email=null,metadata={}]]]")); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponseTests.java new file mode 100644 index 000000000000..209bc790a8c5 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/DeleteWatchResponseTests.java @@ -0,0 +1,32 @@ +/* + * 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.protocol.xpack.watcher; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class DeleteWatchResponseTests extends AbstractXContentTestCase { + + @Override + protected DeleteWatchResponse createTestInstance() { + String id = randomAlphaOfLength(10); + long version = randomLongBetween(1, 10); + boolean found = randomBoolean(); + return new DeleteWatchResponse(id, version, found); + } + + @Override + protected DeleteWatchResponse doParseInstance(XContentParser parser) throws IOException { + return DeleteWatchResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponseTests.java new file mode 100644 index 000000000000..1fc2f61b684c --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/PutWatchResponseTests.java @@ -0,0 +1,32 @@ +/* + * 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.protocol.xpack.watcher; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class PutWatchResponseTests extends AbstractXContentTestCase { + + @Override + protected PutWatchResponse createTestInstance() { + String id = randomAlphaOfLength(10); + long version = randomLongBetween(1, 10); + boolean created = randomBoolean(); + return new PutWatchResponse(id, version, created); + } + + @Override + protected PutWatchResponse doParseInstance(XContentParser parser) throws IOException { + return PutWatchResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} From 0da981a6a9d3c687dbff6de6f866997b5a8c30d2 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 23 Aug 2018 11:43:48 -0400 Subject: [PATCH 126/283] [TEST] Add some ACL yaml tests for Rollup (#33035) These two tests compliment the existing unit tests which check Rollup's ACL/security integration. The first test creates to indices, puts a document in each one, and then assigns a role to the test user that can only access one of the indices. A rollup job is created with a pattern that would match both indices, and we verify that only the allowed document was rolled up (e.g. verifying that the unpermissioned index stays hidden). The second test creates a single index with two documents tagged by the keyword "public"/"private". An attribute-based role is created that only allows viewing "public" documents. We then verify the rollup job only rolled the "public" doc, and not the "private" one. --- .../test/rollup/security_tests.yml | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/security_tests.yml diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/security_tests.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/security_tests.yml new file mode 100644 index 000000000000..57bfd821ea24 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/security_tests.yml @@ -0,0 +1,343 @@ +setup: + - skip: + features: headers + + - do: + cluster.health: + wait_for_status: yellow + + + +--- +teardown: + - do: + xpack.security.delete_user: + username: "test_user" + ignore: 404 + + - do: + xpack.security.delete_role: + name: "foo_only_access" + ignore: 404 + +--- +"Index-based access": + + - do: + xpack.security.put_role: + name: "foo_only_access" + body: > + { + "cluster": [ "all" ], + "indices": [ + { "names": ["foo"], "privileges": ["all"] }, + { "names": ["rollup"], "privileges": ["all"] } + ] + } + + - do: + xpack.security.put_user: + username: "test_user" + body: > + { + "password" : "x-pack-test-password", + "roles" : [ "foo_only_access" ], + "full_name" : "foo only" + } + + - do: + indices.create: + index: foo + body: + mappings: + _doc: + properties: + timestamp: + type: date + value_field: + type: integer + - do: + headers: + Authorization: "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" # run as x_pack_rest_user, i.e. the test setup superuser + index: + index: foo + type: _doc + body: + timestamp: 123 + value_field: 1232 + + - do: + indices.create: + index: foobar + body: + mappings: + _doc: + properties: + timestamp: + type: date + value_field: + type: integer + - do: + headers: + Authorization: "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" # run as x_pack_rest_user, i.e. the test setup superuser + index: + index: foobar + type: _doc + body: + timestamp: 123 + value_field: 456 + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + indices.refresh: + index: foo + + # This index pattern will match both indices, but we only have permission to read one + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.rollup.put_job: + id: foo + body: > + { + "index_pattern": "foo*", + "rollup_index": "rollup", + "cron": "*/1 * * * * ?", + "page_size" :10, + "groups" : { + "date_histogram": { + "field": "timestamp", + "interval": "1s" + } + }, + "metrics": [ + { + "field": "value_field", + "metrics": ["min", "max", "sum"] + } + ] + } + + - is_true: acknowledged + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.rollup.start_job: + id: foo + - is_true: started + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + indices.refresh: + index: rollup + + # this is a hacky way to sleep for 5s, since we will never have 10 nodes + - do: + catch: request_timeout + cluster.health: + wait_for_nodes: 10 + timeout: "5s" + - match: + timed_out: true + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.rollup.get_jobs: + id: foo + - match: + jobs.0.stats.documents_processed: 1 + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + search: + index: foo + body: + query: + match_all: {} + + - match: + hits.total: 1 + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + search: + index: rollup + body: + query: + match_all: {} + + - match: + hits.total: 1 + - match: + hits.hits.0._id: "foo$VxMkzTqILshClbtbFi4-rQ" + - match: + hits.hits.0._source: + timestamp.date_histogram.time_zone: "UTC" + timestamp.date_histogram.timestamp: 0 + value_field.max.value: 1232.0 + _rollup.version: 2 + timestamp.date_histogram.interval: "1s" + value_field.sum.value: 1232.0 + value_field.min.value: 1232.0 + timestamp.date_histogram._count: 1 + _rollup.id: "foo" + + +--- +"Attribute-based access": + + - do: + xpack.security.put_role: + name: "foo_only_access" + body: > + { + "cluster": [ "all" ], + "indices": [ + { + "names": ["foo"], + "privileges": ["all"], + "query": { + "template": { + "source": "{\"bool\":{\"filter\":[{\"term\":{\"visibility\":\"public\"}}]}}" + } + } + }, + { "names": ["rollup"], "privileges": ["all"] } + ] + } + + - do: + xpack.security.put_user: + username: "test_user" + body: > + { + "password" : "x-pack-test-password", + "roles" : [ "foo_only_access" ], + "full_name" : "foo only" + } + + - do: + indices.create: + index: foo + body: + mappings: + _doc: + properties: + timestamp: + type: date + value_field: + type: integer + visibility: + type: keyword + - do: + headers: + Authorization: "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" # run as x_pack_rest_user, i.e. the test setup superuser + index: + index: foo + type: _doc + body: + timestamp: 123 + value_field: 1232 + visibility: "public" + - do: + headers: + Authorization: "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" # run as x_pack_rest_user, i.e. the test setup superuser + index: + index: foobar + type: _doc + body: + timestamp: 123 + value_field: 456 + visibility: "private" + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + indices.refresh: + index: foo + + # Index contains two docs, but we should only be able to see one of them + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.rollup.put_job: + id: foo + body: > + { + "index_pattern": "foo", + "rollup_index": "rollup", + "cron": "*/1 * * * * ?", + "page_size" :10, + "groups" : { + "date_histogram": { + "field": "timestamp", + "interval": "1s" + } + }, + "metrics": [ + { + "field": "value_field", + "metrics": ["min", "max", "sum"] + } + ] + } + - is_true: acknowledged + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.rollup.start_job: + id: foo + - is_true: started + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + indices.refresh: + index: rollup + + # this is a hacky way to sleep for 5s, since we will never have 10 nodes + - do: + catch: request_timeout + cluster.health: + wait_for_nodes: 10 + timeout: "5s" + - match: + timed_out: true + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + xpack.rollup.get_jobs: + id: foo + - match: + jobs.0.stats.documents_processed: 1 + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + search: + index: foo + body: + query: + match_all: {} + + - match: + hits.total: 1 + + - do: + headers: { Authorization: "Basic dGVzdF91c2VyOngtcGFjay10ZXN0LXBhc3N3b3Jk" } # test_user + search: + index: rollup + body: + query: + match_all: {} + + - match: + hits.total: 1 + - match: + hits.hits.0._id: "foo$VxMkzTqILshClbtbFi4-rQ" + - match: + hits.hits.0._source: + timestamp.date_histogram.time_zone: "UTC" + timestamp.date_histogram.timestamp: 0 + value_field.max.value: 1232.0 + _rollup.version: 2 + timestamp.date_histogram.interval: "1s" + value_field.sum.value: 1232.0 + value_field.min.value: 1232.0 + timestamp.date_histogram._count: 1 + _rollup.id: "foo" From 8f8d3a5556abaaabbfbfd588fb96d908cc330ff9 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 23 Aug 2018 16:15:37 -0400 Subject: [PATCH 127/283] [Rollup] Return empty response when aggs are missing (#32796) If a search request doesn't contain aggs (or an empty agg object), we should just retun an empty response. This is how the normal search API works if you specify zero hits and empty aggs. The existing behavior throws an exception because it tries to send an empty msearch. Closes #32256 --- .../en/rest-api/rollup/rollup-search.asciidoc | 2 + .../rollup/RollupResponseTranslator.java | 44 ++++++++++++++----- .../action/TransportRollupSearchAction.java | 17 ++++--- .../RollupResponseTranslationTests.java | 9 ++-- .../rollup/action/SearchActionTests.java | 13 +++--- .../test/rollup/rollup_search.yml | 14 ++++++ 6 files changed, 74 insertions(+), 25 deletions(-) diff --git a/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc b/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc index f595d52ec10a..115ef8fb0438 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc +++ b/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc @@ -101,6 +101,7 @@ GET /sensor_rollup/_rollup_search -------------------------------------------------- // CONSOLE // TEST[setup:sensor_prefab_data] +// TEST[s/_rollup_search/_rollup_search?filter_path=took,timed_out,terminated_early,_shards,hits,aggregations/] The query is targeting the `sensor_rollup` data, since this contains the rollup data as configured in the job. A `max` aggregation has been used on the `temperature` field, yielding the following response: @@ -194,6 +195,7 @@ GET sensor-1,sensor_rollup/_rollup_search <1> -------------------------------------------------- // CONSOLE // TEST[continued] +// TEST[s/_rollup_search/_rollup_search?filter_path=took,timed_out,terminated_early,_shards,hits,aggregations/] <1> Note the URI now searches `sensor-1` and `sensor_rollup` at the same time When the search is executed, the Rollup Search endpoint will do two things: diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java index 4042e98ef93f..a38adf5d9de3 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupResponseTranslator.java @@ -238,11 +238,23 @@ private static SearchResponse doCombineResponse(SearchResponse liveResponse, Lis ? (InternalAggregations)liveResponse.getAggregations() : InternalAggregations.EMPTY; - rolledResponses.forEach(r -> { - if (r == null || r.getAggregations() == null || r.getAggregations().asList().size() == 0) { - throw new RuntimeException("Expected to find aggregations in rollup response, but none found."); + int missingRollupAggs = rolledResponses.stream().mapToInt(searchResponse -> { + if (searchResponse == null + || searchResponse.getAggregations() == null + || searchResponse.getAggregations().asList().size() == 0) { + return 1; } - }); + return 0; + }).sum(); + + // We had no rollup aggs, so there is nothing to process + if (missingRollupAggs == rolledResponses.size()) { + // Return an empty response, but make sure we include all the shard, failure, etc stats + return mergeFinalResponse(liveResponse, rolledResponses, InternalAggregations.EMPTY); + } else if (missingRollupAggs > 0 && missingRollupAggs != rolledResponses.size()) { + // We were missing some but not all the aggs, unclear how to handle this. Bail. + throw new RuntimeException("Expected to find aggregations in rollup response, but none found."); + } // The combination process returns a tree that is identical to the non-rolled // which means we can use aggregation's reduce method to combine, just as if @@ -275,27 +287,39 @@ private static SearchResponse doCombineResponse(SearchResponse liveResponse, Lis new InternalAggregation.ReduceContext(reduceContext.bigArrays(), reduceContext.scriptService(), true)); } - // TODO allow profiling in the future - InternalSearchResponse combinedInternal = new InternalSearchResponse(SearchHits.empty(), currentTree, null, null, - rolledResponses.stream().anyMatch(SearchResponse::isTimedOut), - rolledResponses.stream().anyMatch(SearchResponse::isTimedOut), - rolledResponses.stream().mapToInt(SearchResponse::getNumReducePhases).sum()); + return mergeFinalResponse(liveResponse, rolledResponses, currentTree); + } + + private static SearchResponse mergeFinalResponse(SearchResponse liveResponse, List rolledResponses, + InternalAggregations aggs) { int totalShards = rolledResponses.stream().mapToInt(SearchResponse::getTotalShards).sum(); int sucessfulShards = rolledResponses.stream().mapToInt(SearchResponse::getSuccessfulShards).sum(); int skippedShards = rolledResponses.stream().mapToInt(SearchResponse::getSkippedShards).sum(); long took = rolledResponses.stream().mapToLong(r -> r.getTook().getMillis()).sum() ; + boolean isTimedOut = rolledResponses.stream().anyMatch(SearchResponse::isTimedOut); + boolean isTerminatedEarly = rolledResponses.stream() + .filter(r -> r.isTerminatedEarly() != null) + .anyMatch(SearchResponse::isTerminatedEarly); + int numReducePhases = rolledResponses.stream().mapToInt(SearchResponse::getNumReducePhases).sum(); + if (liveResponse != null) { totalShards += liveResponse.getTotalShards(); sucessfulShards += liveResponse.getSuccessfulShards(); skippedShards += liveResponse.getSkippedShards(); took = Math.max(took, liveResponse.getTook().getMillis()); + isTimedOut = isTimedOut && liveResponse.isTimedOut(); + isTerminatedEarly = isTerminatedEarly && liveResponse.isTerminatedEarly(); + numReducePhases += liveResponse.getNumReducePhases(); } + InternalSearchResponse combinedInternal = new InternalSearchResponse(SearchHits.empty(), aggs, null, null, + isTimedOut, isTerminatedEarly, numReducePhases); + // Shard failures are ignored atm, so returning an empty array is fine return new SearchResponse(combinedInternal, null, totalShards, sucessfulShards, skippedShards, - took, ShardSearchFailure.EMPTY_ARRAY, rolledResponses.get(0).getClusters()); + took, ShardSearchFailure.EMPTY_ARRAY, rolledResponses.get(0).getClusters()); } /** diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java index c63ab96fa259..ea0319c34328 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java @@ -155,6 +155,18 @@ static MultiSearchRequest createMSearchRequest(SearchRequest request, NamedWrite rolledSearchSource.size(0); AggregatorFactories.Builder sourceAgg = request.source().aggregations(); + // If there are no aggs in the request, our translation won't create any msearch. + // So just add an dummy request to the msearch and return. This is a bit silly + // but maintains how the regular search API behaves + if (sourceAgg == null || sourceAgg.count() == 0) { + + // Note: we can't apply any query rewriting or filtering on the query because there + // are no validated caps, so we have no idea what job is intended here. The only thing + // this affects is doc count, since hits and aggs will both be empty it doesn't really matter. + msearch.add(new SearchRequest(context.getRollupIndices(), request.source()).types(request.types())); + return msearch; + } + // Find our list of "best" job caps Set validatedCaps = new HashSet<>(); sourceAgg.getAggregatorFactories() @@ -248,11 +260,6 @@ static void validateSearchRequest(SearchRequest request) { if (request.source().explain() != null && request.source().explain()) { throw new IllegalArgumentException("Rollup search does not support explaining."); } - - // Rollup is only useful if aggregations are set, throw an exception otherwise - if (request.source().aggregations() == null) { - throw new IllegalArgumentException("Rollup requires at least one aggregation to be set."); - } } static QueryBuilder rewriteQuery(QueryBuilder builder, Set jobCaps) { diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java index 35d9f0d133a3..73a4d0665c4e 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java @@ -198,10 +198,11 @@ public void testRolledMissingAggs() { BigArrays bigArrays = new MockBigArrays(new MockPageCacheRecycler(Settings.EMPTY), new NoneCircuitBreakerService()); ScriptService scriptService = mock(ScriptService.class); - Exception e = expectThrows(RuntimeException.class, - () -> RollupResponseTranslator.combineResponses(msearch, - new InternalAggregation.ReduceContext(bigArrays, scriptService, true))); - assertThat(e.getMessage(), equalTo("Expected to find aggregations in rollup response, but none found.")); + SearchResponse response = RollupResponseTranslator.combineResponses(msearch, + new InternalAggregation.ReduceContext(bigArrays, scriptService, true)); + assertNotNull(response); + Aggregations responseAggs = response.getAggregations(); + assertThat(responseAggs.asList().size(), equalTo(0)); } public void testMissingRolledIndex() { diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java index 069e23e4093d..3cc6190db30d 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java @@ -307,21 +307,22 @@ public void testExplain() { assertThat(e.getMessage(), equalTo("Rollup search does not support explaining.")); } - public void testNoAgg() { - String[] normalIndices = new String[]{randomAlphaOfLength(10)}; + public void testNoRollupAgg() { + String[] normalIndices = new String[]{}; String[] rollupIndices = new String[]{randomAlphaOfLength(10)}; TransportRollupSearchAction.RollupSearchContext ctx = new TransportRollupSearchAction.RollupSearchContext(normalIndices, rollupIndices, Collections.emptySet()); SearchSourceBuilder source = new SearchSourceBuilder(); source.query(new MatchAllQueryBuilder()); source.size(0); - SearchRequest request = new SearchRequest(normalIndices, source); + SearchRequest request = new SearchRequest(rollupIndices, source); NamedWriteableRegistry registry = mock(NamedWriteableRegistry.class); - Exception e = expectThrows(IllegalArgumentException.class, - () -> TransportRollupSearchAction.createMSearchRequest(request, registry, ctx)); - assertThat(e.getMessage(), equalTo("Rollup requires at least one aggregation to be set.")); + MultiSearchRequest msearch = TransportRollupSearchAction.createMSearchRequest(request, registry, ctx); + assertThat(msearch.requests().size(), equalTo(1)); + assertThat(msearch.requests().get(0), equalTo(request)); } + public void testNoLiveNoRollup() { String[] normalIndices = new String[0]; String[] rollupIndices = new String[0]; diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/rollup_search.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/rollup_search.yml index d401d5c69bac..e2f1174665ea 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/rollup_search.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/rollup_search.yml @@ -152,6 +152,20 @@ setup: - match: { aggregations.histo.buckets.3.key_as_string: "2017-01-01T08:00:00.000Z" } - match: { aggregations.histo.buckets.3.doc_count: 20 } +--- +"Empty aggregation": + + - do: + xpack.rollup.rollup_search: + index: "foo_rollup" + body: + size: 0 + aggs: {} + + - length: { hits.hits: 0 } + - match: { hits.total: 0 } + - is_false: aggregations + --- "Search with Metric": From fdff8f3db0093fa15cfa161f7dec80b715a48a43 Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Thu, 23 Aug 2018 16:46:47 -0400 Subject: [PATCH 128/283] Do NOT allow termvectors on nested fields (#32728) Requesting _termvectors on a nested field or any sub-fields of a nested field returns empty results. Closes #21625 --- docs/reference/docs/termvectors.asciidoc | 4 ++ .../test/termvectors/50_nested.yml | 49 +++++++++++++++++++ .../index/termvectors/TermVectorsService.java | 17 +++++-- 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml diff --git a/docs/reference/docs/termvectors.asciidoc b/docs/reference/docs/termvectors.asciidoc index 3cd21b21df4d..0e6078ad7b23 100644 --- a/docs/reference/docs/termvectors.asciidoc +++ b/docs/reference/docs/termvectors.asciidoc @@ -30,6 +30,10 @@ in similar way to the <> [WARNING] Note that the usage of `/_termvector` is deprecated in 2.0, and replaced by `/_termvectors`. +[WARNING] +Term Vectors API doesn't work on nested fields. `/_termvectors` on a nested +field and any sub-fields of a nested field returns empty results. + [float] === Return values diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml new file mode 100644 index 000000000000..a10fc7b504bf --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml @@ -0,0 +1,49 @@ +setup: + - do: + indices.create: + index: testidx + body: + mappings: + _doc: + properties: + nested1: + type : nested + properties: + nested1-text: + type: text + object1: + properties: + object1-text: + type: text + object1-nested1: + type: nested + properties: + object1-nested1-text: + type: text + - do: + index: + index: testidx + type: _doc + id: 1 + body: + "nested1" : [{ "nested1-text": "text1" }] + "object1" : [{ "object1-text": "text2" }, "object1-nested1" : [{"object1-nested1-text" : "text3"}]] + + - do: + indices.refresh: {} + +--- +"Termvectors on nested fields should return empty results": + + - do: + termvectors: + index: testidx + type: _doc + id: 1 + fields: ["nested1", "nested1.nested1-text", "object1.object1-nested1", "object1.object1-nested1.object1-nested1-text", "object1.object1-text"] + + - is_false: term_vectors.nested1 + - is_false: term_vectors.nested1\.nested1-text # escaping as the field name contains dot + - is_false: term_vectors.object1\.object1-nested1 + - is_false: term_vectors.object1\.object1-nested1\.object1-nested1-text + - is_true: term_vectors.object1\.object1-text diff --git a/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java b/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java index bc77626b9427..43f1a278f54c 100644 --- a/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java +++ b/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java @@ -45,6 +45,7 @@ import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.ObjectMapper; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SourceFieldMapper; @@ -160,7 +161,7 @@ private static void handleFieldWildcards(IndexShard indexShard, TermVectorsReque request.selectedFields(fieldNames.toArray(Strings.EMPTY_ARRAY)); } - private static boolean isValidField(MappedFieldType fieldType) { + private static boolean isValidField(MappedFieldType fieldType, IndexShard indexShard) { // must be a string if (fieldType instanceof StringFieldType == false) { return false; @@ -169,6 +170,16 @@ private static boolean isValidField(MappedFieldType fieldType) { if (fieldType.indexOptions() == IndexOptions.NONE) { return false; } + // and must not be under nested field + int dotIndex = fieldType.name().indexOf('.'); + while (dotIndex > -1) { + String parentField = fieldType.name().substring(0, dotIndex); + ObjectMapper mapper = indexShard.mapperService().getObjectMapper(parentField); + if (mapper != null && mapper.nested().isNested()) { + return false; + } + dotIndex = fieldType.name().indexOf('.', dotIndex + 1); + } return true; } @@ -177,7 +188,7 @@ private static Fields addGeneratedTermVectors(IndexShard indexShard, Engine.GetR Set validFields = new HashSet<>(); for (String field : selectedFields) { MappedFieldType fieldType = indexShard.mapperService().fullName(field); - if (!isValidField(fieldType)) { + if (isValidField(fieldType, indexShard) == false) { continue; } // already retrieved, only if the analyzer hasn't been overridden at the field @@ -284,7 +295,7 @@ private static Fields generateTermVectorsFromDoc(IndexShard indexShard, TermVect Collection documentFields = new HashSet<>(); for (IndexableField field : doc.getFields()) { MappedFieldType fieldType = indexShard.mapperService().fullName(field.name()); - if (!isValidField(fieldType)) { + if (isValidField(fieldType, indexShard) == false) { continue; } if (request.selectedFields() != null && !request.selectedFields().contains(field.name())) { From 8f16696fe12bd406327b0f715f513a33c642ec37 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Thu, 23 Aug 2018 15:45:25 -0500 Subject: [PATCH 129/283] Add versions 5.6.12 and 6.4.1 --- server/src/main/java/org/elasticsearch/Version.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index a815a9711d02..1afe88f8d43e 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -122,6 +122,8 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_5_6_10 = new Version(V_5_6_10_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); public static final int V_5_6_11_ID = 5061199; public static final Version V_5_6_11 = new Version(V_5_6_11_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); + public static final int V_5_6_12_ID = 5061299; + public static final Version V_5_6_12 = new Version(V_5_6_12_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); public static final int V_6_0_0_alpha1_ID = 6000001; public static final Version V_6_0_0_alpha1 = new Version(V_6_0_0_alpha1_ID, org.apache.lucene.util.Version.LUCENE_7_0_0); @@ -174,10 +176,10 @@ public class Version implements Comparable, ToXContentFragment { public static final Version V_6_3_1 = new Version(V_6_3_1_ID, org.apache.lucene.util.Version.LUCENE_7_3_1); public static final int V_6_3_2_ID = 6030299; public static final Version V_6_3_2 = new Version(V_6_3_2_ID, org.apache.lucene.util.Version.LUCENE_7_3_1); - public static final int V_6_3_3_ID = 6030399; - public static final Version V_6_3_3 = new Version(V_6_3_3_ID, org.apache.lucene.util.Version.LUCENE_7_3_1); public static final int V_6_4_0_ID = 6040099; public static final Version V_6_4_0 = new Version(V_6_4_0_ID, org.apache.lucene.util.Version.LUCENE_7_4_0); + public static final int V_6_4_1_ID = 6040199; + public static final Version V_6_4_1 = new Version(V_6_4_1_ID, org.apache.lucene.util.Version.LUCENE_7_4_0); public static final int V_6_5_0_ID = 6050099; public static final Version V_6_5_0 = new Version(V_6_5_0_ID, org.apache.lucene.util.Version.LUCENE_7_5_0); public static final int V_7_0_0_alpha1_ID = 7000001; @@ -200,10 +202,10 @@ public static Version fromId(int id) { return V_7_0_0_alpha1; case V_6_5_0_ID: return V_6_5_0; + case V_6_4_1_ID: + return V_6_4_1; case V_6_4_0_ID: return V_6_4_0; - case V_6_3_3_ID: - return V_6_3_3; case V_6_3_2_ID: return V_6_3_2; case V_6_3_1_ID: @@ -246,6 +248,8 @@ public static Version fromId(int id) { return V_6_0_0_alpha2; case V_6_0_0_alpha1_ID: return V_6_0_0_alpha1; + case V_5_6_12_ID: + return V_5_6_12; case V_5_6_11_ID: return V_5_6_11; case V_5_6_10_ID: From a211d24bda99b810868a79215d4d053332600cd4 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 24 Aug 2018 11:04:02 +1000 Subject: [PATCH 130/283] [DOCS] Add docs for Application Privileges (#32635) --- docs/reference/redirects.asciidoc | 7 +- x-pack/docs/build.gradle | 19 ++ x-pack/docs/en/rest-api/security.asciidoc | 48 ++++-- .../security/create-role-mappings.asciidoc | 4 +- .../rest-api/security/create-roles.asciidoc | 22 ++- .../rest-api/security/create-users.asciidoc | 4 +- .../security/delete-app-privileges.asciidoc | 59 +++++++ .../security/get-app-privileges.asciidoc | 94 ++++++++++ ...leges.asciidoc => has-privileges.asciidoc} | 32 +++- .../security/put-app-privileges.asciidoc | 163 ++++++++++++++++++ .../docs/en/rest-api/security/users.asciidoc | 0 .../authorization/managing-roles.asciidoc | 71 +++++++- .../api/xpack.security.has_privileges.json | 2 +- 13 files changed, 495 insertions(+), 30 deletions(-) create mode 100644 x-pack/docs/en/rest-api/security/delete-app-privileges.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/get-app-privileges.asciidoc rename x-pack/docs/en/rest-api/security/{privileges.asciidoc => has-privileges.asciidoc} (69%) create mode 100644 x-pack/docs/en/rest-api/security/put-app-privileges.asciidoc delete mode 100644 x-pack/docs/en/rest-api/security/users.asciidoc diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index 6498637873a5..b88c7bf4547b 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -538,4 +538,9 @@ native realm: You can use the following APIs to add, remove, and retrieve role mappings: * <>, <> -* <> \ No newline at end of file +* <> + +[role="exclude",id="security-api-privileges"] +=== Privilege APIs + +See <>. diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index 7ef17715e06a..6cca05c4a0ef 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -750,3 +750,22 @@ setups['jacknich_user'] = ''' "metadata" : { "intelligence" : 7 } } ''' +setups['app0102_privileges'] = ''' + - do: + xpack.security.put_privileges: + body: > + { + "myapp": { + "read": { + "application": "myapp", + "name": "read", + "actions": [ + "data:read/*", + "action:login" ], + "metadata": { + "description": "Read access to myapp" + } + } + } + } +''' diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index f34f119ba795..3ba582d5d785 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -6,28 +6,41 @@ You can use the following APIs to perform {security} activities. * <> * <> -* <> +* <> * <> [float] -[[security-role-apis]] -=== Roles +[[security-api-app-privileges]] +=== Application privileges -You can use the following APIs to add, remove, and retrieve roles in the native realm: +You can use the following APIs to add, update, retrieve, and remove application +privileges: -* <>, <> -* <> -* <> +* <> +* <> +* <> [float] [[security-role-mapping-apis]] === Role mappings -You can use the following APIs to add, remove, and retrieve role mappings: +You can use the following APIs to add, remove, update, and retrieve role mappings: -* <>, <> +* <> +* <> * <> +[float] +[[security-role-apis]] +=== Roles + +You can use the following APIs to add, remove, update, and retrieve roles in the native realm: + +* <> +* <> +* <> +* <> + [float] [[security-token-apis]] === Tokens @@ -35,20 +48,25 @@ You can use the following APIs to add, remove, and retrieve role mappings: You can use the following APIs to create and invalidate bearer tokens for access without requiring basic authentication: -* <>, <> +* <> +* <> [float] [[security-user-apis]] === Users -You can use the following APIs to create, read, update, and delete users from the +You can use the following APIs to add, remove, update, or retrieve users in the native realm: -* <>, <> -* <>, <> +* <> * <> +* <> +* <> +* <> * <> + +include::security/put-app-privileges.asciidoc[] include::security/authenticate.asciidoc[] include::security/change-password.asciidoc[] include::security/clear-cache.asciidoc[] @@ -56,15 +74,17 @@ include::security/create-role-mappings.asciidoc[] include::security/clear-roles-cache.asciidoc[] include::security/create-roles.asciidoc[] include::security/create-users.asciidoc[] +include::security/delete-app-privileges.asciidoc[] include::security/delete-role-mappings.asciidoc[] include::security/delete-roles.asciidoc[] include::security/delete-tokens.asciidoc[] include::security/delete-users.asciidoc[] include::security/disable-users.asciidoc[] include::security/enable-users.asciidoc[] +include::security/get-app-privileges.asciidoc[] include::security/get-role-mappings.asciidoc[] include::security/get-roles.asciidoc[] include::security/get-tokens.asciidoc[] include::security/get-users.asciidoc[] -include::security/privileges.asciidoc[] +include::security/has-privileges.asciidoc[] include::security/ssl.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc index b16ac6ee4dc4..87dedbba4f7c 100644 --- a/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-role-mappings.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[security-api-put-role-mapping]] -=== Add role mappings API +=== Create or update role mappings API -Adds and updates role mappings. +Creates and updates role mappings. ==== Request diff --git a/x-pack/docs/en/rest-api/security/create-roles.asciidoc b/x-pack/docs/en/rest-api/security/create-roles.asciidoc index 749676b4e836..fc3c613557ef 100644 --- a/x-pack/docs/en/rest-api/security/create-roles.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-roles.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[security-api-put-role]] -=== Create roles API +=== Create or update roles API -Adds roles in the native realm. +Adds and updates roles in the native realm. ==== Request @@ -29,9 +29,20 @@ file-based role management. For more information about the native realm, see The following parameters can be specified in the body of a PUT or POST request and pertain to adding a role: +`applications`:: (list) A list of application privilege entries. +`application` (required)::: (string) The name of the application to which this entry applies +`privileges`::: (list) A list of strings, where each element is the name of an application +privilege or action. +`resources`::: (list) A list resources to which the privileges are applied. + `cluster`:: (list) A list of cluster privileges. These privileges define the cluster level actions that users with this role are able to execute. +`global`:: (object) An object defining global privileges. A global privilege is +a form of cluster privilege that is request-aware. Support for global privileges +is currently limited to the management of application privileges. +This field is optional. + `indices`:: (list) A list of indices permissions entries. `field_security`::: (list) The document fields that the owners of the role have read access to. For more information, see @@ -79,6 +90,13 @@ POST /_xpack/security/role/my_admin_role "query": "{\"match\": {\"title\": \"foo\"}}" // optional } ], + "applications": [ + { + "application": "myapp", + "privileges": [ "admin", "read" ], + "resources": [ "*" ] + } + ], "run_as": [ "other_user" ], // optional "metadata" : { // optional "version" : 1 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 5015d0401c22..91171b0e57eb 100644 --- a/x-pack/docs/en/rest-api/security/create-users.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-users.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[security-api-put-user]] -=== Create users API +=== Create or update users API -Creates and updates users in the native realm. These users are commonly referred +Adds and updates users in the native realm. These users are commonly referred to as _native users_. diff --git a/x-pack/docs/en/rest-api/security/delete-app-privileges.asciidoc b/x-pack/docs/en/rest-api/security/delete-app-privileges.asciidoc new file mode 100644 index 000000000000..d7f001721b1f --- /dev/null +++ b/x-pack/docs/en/rest-api/security/delete-app-privileges.asciidoc @@ -0,0 +1,59 @@ +[role="xpack"] +[[security-api-delete-privilege]] +=== Delete application privileges API + +Removes +{stack-ov}/security-privileges.html#application-privileges[application privileges]. + +==== Request + +`DELETE /_xpack/security/privilege//` + +//==== Description + +==== Path Parameters + +`application` (required):: + (string) The name of the application. Application privileges are always + associated with exactly one application. + +`privilege` (required):: + (string) The name of the privilege. + +// ==== Request Body + +==== Authorization + +To use this API, you must have either: + +- the `manage_security` cluster privilege (or a greater privilege such as `all`); _or_ +- the _"Manage Application Privileges"_ global privilege for the application being referenced + in the request + +==== Examples + +The following example deletes the `read` application privilege from the +`myapp` application: + +[source,js] +-------------------------------------------------- +DELETE /_xpack/security/privilege/myapp/read +-------------------------------------------------- +// CONSOLE +// TEST[setup:app0102_privileges] + +If the role is successfully deleted, the request returns `{"found": true}`. +Otherwise, `found` is set to false. + +[source,js] +-------------------------------------------------- +{ + "myapp": { + "read": { + "found" : true + } + } +} +-------------------------------------------------- +// TESTRESPONSE + diff --git a/x-pack/docs/en/rest-api/security/get-app-privileges.asciidoc b/x-pack/docs/en/rest-api/security/get-app-privileges.asciidoc new file mode 100644 index 000000000000..5412a4bdceb8 --- /dev/null +++ b/x-pack/docs/en/rest-api/security/get-app-privileges.asciidoc @@ -0,0 +1,94 @@ +[role="xpack"] +[[security-api-get-privileges]] +=== Get application privileges API + +Retrieves +{stack-ov}/security-privileges.html#application-privileges[application privileges]. + +==== Request + +`GET /_xpack/security/privilege` + + +`GET /_xpack/security/privilege/` + + +`GET /_xpack/security/privilege//` + + +==== Description + +To check a user's application privileges, use the +<>. + + +==== Path Parameters + +`application`:: + (string) The name of the application. Application privileges are always + associated with exactly one application. + If you do not specify this parameter, the API returns information about all + privileges for all applications. + +`privilege`:: + (string) The name of the privilege. If you do not specify this parameter, the + API returns information about all privileges for the requested application. + +//==== Request Body + +==== Authorization + +To use this API, you must have either: + +- the `manage_security` cluster privilege (or a greater privilege such as `all`); _or_ +- the _"Manage Application Privileges"_ global privilege for the application being referenced + in the request + +==== Examples + +The following example retrieves information about the `read` privilege for the +`app01` application: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/privilege/myapp/read +-------------------------------------------------- +// CONSOLE +// TEST[setup:app0102_privileges] + +A successful call returns an object keyed by application name and privilege +name. If the privilege is not defined, the request responds with a 404 status. + +[source,js] +-------------------------------------------------- +{ + "myapp": { + "read": { + "application": "myapp", + "name": "read", + "actions": [ + "data:read/*", + "action:login" + ], + "metadata": { + "description": "Read access to myapp" + } + } + } +} +-------------------------------------------------- +// TESTRESPONSE + +To retrieve all privileges for an application, omit the privilege name: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/privilege/myapp/ +-------------------------------------------------- +// CONSOLE + +To retrieve every privilege, omit both the application and privilege names: + +[source,js] +-------------------------------------------------- +GET /_xpack/security/privilege/ +-------------------------------------------------- +// CONSOLE diff --git a/x-pack/docs/en/rest-api/security/privileges.asciidoc b/x-pack/docs/en/rest-api/security/has-privileges.asciidoc similarity index 69% rename from x-pack/docs/en/rest-api/security/privileges.asciidoc rename to x-pack/docs/en/rest-api/security/has-privileges.asciidoc index adaf27e97073..cae1bc4d303f 100644 --- a/x-pack/docs/en/rest-api/security/privileges.asciidoc +++ b/x-pack/docs/en/rest-api/security/has-privileges.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] -[[security-api-privileges]] -=== Privilege APIs +[[security-api-has-privileges]] +=== Has Privileges API [[security-api-has-privilege]] @@ -15,7 +15,7 @@ a specified list of privileges. ==== Description For a list of the privileges that you can specify in this API, -see {xpack-ref}/security-privileges.html[Security Privileges]. +see {stack-ov}/security-privileges.html[Security privileges]. A successful call returns a JSON structure that shows whether each specified privilege is assigned to the user. @@ -30,6 +30,14 @@ privilege is assigned to the user. `privileges`::: (list) A list of the privileges that you want to check for the specified indices. +`application`:: +`application`::: (string) The name of the application. +`privileges`::: (list) A list of the privileges that you want to check for the +specified resources. May be either application privilege names, or the names of +actions that are granted by those privileges +`resources`::: (list) A list of resource names against which the privileges +should be checked + ==== Authorization All users can use this API, but only to determine their own privileges. @@ -41,7 +49,7 @@ more information, see ==== Examples The following example checks whether the current user has a specific set of -cluster and indices privileges: +cluster, index, and application privileges: [source,js] -------------------------------------------------- @@ -57,6 +65,13 @@ GET _xpack/security/user/_has_privileges "names": [ "inventory" ], "privileges" : [ "read", "write" ] } + ], + "application": [ + { + "application": "inventory_manager", + "privileges" : [ "read", "data:write/inventory" ], + "resources" : [ "product/1852563" ] + } ] } -------------------------------------------------- @@ -85,7 +100,14 @@ The following example output indicates which privileges the "rdeniro" user has: "write" : false } }, - "application" : {} + "application" : { + "inventory_manager" : { + "product/1852563" : { + "read": false, + "data:write/inventory": false + } + } + } } -------------------------------------------------- // TESTRESPONSE[s/"rdeniro"/"$body.username"/] diff --git a/x-pack/docs/en/rest-api/security/put-app-privileges.asciidoc b/x-pack/docs/en/rest-api/security/put-app-privileges.asciidoc new file mode 100644 index 000000000000..f715a80014be --- /dev/null +++ b/x-pack/docs/en/rest-api/security/put-app-privileges.asciidoc @@ -0,0 +1,163 @@ +[role="xpack"] +[[security-api-put-privileges]] +=== Create or update application privileges API + +Adds or updates +{stack-ov}/security-privileges.html#application-privileges[application privileges]. + +==== Request + +`POST /_xpack/security/privilege` + + +`PUT /_xpack/security/privilege` + + +==== Description + +This API creates or updates privileges. To remove privileges, use the +<>. + +For more information, see +{stack-ov}/defining-roles.html#roles-application-priv[Application privileges]. + +To check a user's application privileges, use the +<>. + +==== Request Body + +The body is a JSON object where the names of the fields are the application +names and the value of each field is an object. The fields in this inner +object are the names of the privileges and each value is a JSON object that +includes the following fields: + +`actions`:: (array-of-string) A list of action names that are granted by this +privilege. This field must exist and cannot be an empty array. + +`metadata`:: (object) Optional meta-data. Within the `metadata` object, keys +that begin with `_` are reserved for system usage. + + +[[security-api-app-privileges-validation]] +==== Validation + +Application Names:: + Application names are formed from a _prefix_, with an optional _suffix_ that + conform to the following rules: + * The prefix must begin with a lowercase ASCII letter + * The prefix must contain only ASCII letters or digits + * The prefix must be at least 3 characters long + * If the suffix exists, it must begin with either `-` or `_` + * The suffix cannot contain any of the following characters: + `\\`, `/`, `*`, `?`, `"`, `<`, `>`, `|`, `,`, `*` + * No part of the name can contain whitespace. + +Privilege Names:: + Privilege names must begin with a lowercase ASCII letter and must contain + only ASCII letters and digits along with the characters `_`, `-` and `.` + +Action Names:: + Action names can contain any number of printable ASCII characters and must + contain at least one of the following characters: `/` `*`, `:` + +==== Authorization + +To use this API, you must have either: + +- the `manage_security` cluster privilege (or a greater privilege such as `all`); _or_ +- the _"Manage Application Privileges"_ global privilege for the application being referenced + in the request + +==== Examples + +To add a single privilege, submit a PUT or POST request to the +`/_xpack/security/privilege//` endpoint. For example: + +[source,js] +-------------------------------------------------- +PUT /_xpack/security/privilege +{ + "myapp": { + "read": { + "actions": [ <1> + "data:read/*" , <2> + "action:login" ], + "metadata": { <3> + "description": "Read access to myapp" + } + } + } +} +-------------------------------------------------- +// CONSOLE +<1> These strings have significance within the "myapp" application. {es} does not + assign any meaning to them. +<2> The use of a wildcard here (`*`) means that this privilege grants access to + all actions that start with `data:read/`. {es} does not assign any meaning + to these actions. However, if the request includes an application privilege + such as `data:read/users` or `data:read/settings`, the + <> respects the use of a + wildcard and returns `true`. +<3> The metadata object is optional. + +A successful call returns a JSON structure that shows whether the privilege has +been created or updated. + +[source,js] +-------------------------------------------------- +{ + "myapp": { + "read": { + "created": true <1> + } + } +} +-------------------------------------------------- +// TESTRESPONSE +<1> When an existing privilege is updated, `created` is set to false. + +To add multiple privileges, submit a POST request to the +`/_xpack/security/privilege/` endpoint. For example: + +[source,js] +-------------------------------------------------- +PUT /_xpack/security/privilege +{ + "app01": { + "read": { + "actions": [ "action:login", "data:read/*" ] + }, + "write": { + "actions": [ "action:login", "data:write/*" ] + } + }, + "app02": { + "all": { + "actions": [ "*" ] + } + } +} +-------------------------------------------------- +// CONSOLE + +A successful call returns a JSON structure that shows whether the privileges +have been created or updated. + +[source,js] +-------------------------------------------------- +{ + "app02": { + "all": { + "created": true + } + }, + "app01": { + "read": { + "created": true + }, + "write": { + "created": true + } + } +} +-------------------------------------------------- +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/security/users.asciidoc b/x-pack/docs/en/rest-api/security/users.asciidoc deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/x-pack/docs/en/security/authorization/managing-roles.asciidoc b/x-pack/docs/en/security/authorization/managing-roles.asciidoc index f550c900edce..7b30284f5836 100644 --- a/x-pack/docs/en/security/authorization/managing-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/managing-roles.asciidoc @@ -9,7 +9,10 @@ A role is defined by the following JSON structure: { "run_as": [ ... ], <1> "cluster": [ ... ], <2> - "indices": [ ... ] <3> + "global": { ... }, <3> + "indices": [ ... ], <4> + "applications": [ ... ] <5> + } ----- // NOTCONSOLE @@ -19,8 +22,15 @@ A role is defined by the following JSON structure: cluster level actions users with this role are able to execute. This field is optional (missing `cluster` privileges effectively mean no cluster level permissions). -<3> A list of indices permissions entries. This field is optional (missing `indices` +<3> An object defining global privileges. A global privilege is a form of + cluster privilege that is request sensitive. A standard cluster privilege + makes authorization decisions based solely on the action being executed. + A global privilege also considers the parameters included in the request. + Support for global privileges is currently limited to the management of + application privileges. This field is optional. +<4> A list of indices permissions entries. This field is optional (missing `indices` privileges effectively mean no index level permissions). +<5> A list of application privilege entries. This field is optional. [[valid-role-name]] NOTE: Role names must be at least 1 and no more than 1024 characters. They can @@ -28,6 +38,9 @@ NOTE: Role names must be at least 1 and no more than 1024 characters. They can punctuation, and printable symbols in the https://en.wikipedia.org/wiki/Basic_Latin_(Unicode_block)[Basic Latin (ASCII) block]. Leading or trailing whitespace is not allowed. +[[roles-indices-priv]] +==== Indices Privileges + The following describes the structure of an indices permissions entry: [source,js] @@ -77,8 +90,60 @@ names or regular expressions that refer to multiple indices. ------------------------------------------------------------------------------ ============================================================================== -The following snippet shows an example definition of a `clicks_admin` role: +[[roles-global-priv]] +==== Global Privileges +The following describes the structure of a global privileges entry: + +[source,js] +------- +{ + "application": { + "manage": { <1> + "applications": [ ... ] <2> + } + } +} +------- +// NOTCONSOLE + +<1> The only supported global privilege is the ability to manage application + privileges +<2> The list of application names that may be managed. This list supports + wildcards (e.g. `"myapp-*"`) and regular expressions (e.g. + `"/app[0-9]*/"`) + +[[roles-application-priv]] +==== Application Privileges +The following describes the structure of an application privileges entry: +[source,js] +------- +{ + "application": "my_app", <1> + "privileges": [ ... ], <2> + "resources": [ ... ] <3> +} +------- +// NOTCONSOLE + +<1> The name of the application. +<2> The list of the names of the application privileges to grant to this role. +<3> The resources to which those privileges apply. These are handled in the same + way as index name pattern in `indices` permissions. These resources do not + have any special meaning to {security}. + +For details about the validation rules for these fields, see the +{ref}/security-api-put-privileges.html[add application privileges API]. + +A role may refer to application privileges that do not exist - that is, they +have not yet been defined through the add application privileges API (or they +were defined, but have since been deleted). In this case, the privilege has +no effect, and will not grant any actions in the +{ref}/security-api-has-privileges.html[has privileges API]. + +==== Example + +The following snippet shows an example definition of a `clicks_admin` role: [source,js] ----------- POST /_xpack/security/role/clicks_admin diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json index 64b15ae9c022..9c75b40e4d1a 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/xpack.security.has_privileges.json @@ -1,6 +1,6 @@ { "xpack.security.has_privileges": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-privileges.html", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-has-privileges.html", "methods": [ "GET", "POST" ], "url": { "path": "/_xpack/security/user/_has_privileges", From 575f33941cae81e1d4b49f3424eb8f2e550bb9cb Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 24 Aug 2018 12:51:26 +0700 Subject: [PATCH 131/283] Required changes after merging in master branch. --- x-pack/plugin/ccr/build.gradle | 2 +- .../ccr/qa/multi-cluster-with-incompatible-license/build.gradle | 2 +- x-pack/plugin/ccr/qa/multi-cluster-with-security/build.gradle | 2 +- x-pack/plugin/ccr/qa/multi-cluster/build.gradle | 2 +- .../xpack/ccr/index/engine/FollowingEngineTests.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/ccr/build.gradle b/x-pack/plugin/ccr/build.gradle index d066769645b5..0b1f889a2c12 100644 --- a/x-pack/plugin/ccr/build.gradle +++ b/x-pack/plugin/ccr/build.gradle @@ -47,7 +47,7 @@ gradle.projectsEvaluated { dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" - compileOnly project(path: xpackModule('core'), configuration: 'shadow') + compileOnly project(path: xpackModule('core'), configuration: 'default') testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') } diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle index e9e57762c37e..18f1eba3b6d4 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle @@ -3,7 +3,7 @@ import org.elasticsearch.gradle.test.RestIntegTestTask apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('ccr'), configuration: 'runtime') } 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 97c019d4c73a..970c400e7327 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 @@ -3,7 +3,7 @@ import org.elasticsearch.gradle.test.RestIntegTestTask apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('ccr'), configuration: 'runtime') } diff --git a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle index 537584f7b59f..9d59a18ab520 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle +++ b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle @@ -3,7 +3,7 @@ import org.elasticsearch.gradle.test.RestIntegTestTask apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile project(path: xpackModule('core'), configuration: 'shadow') + testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') testCompile project(path: xpackModule('ccr'), configuration: 'runtime') } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java index ff877724ce4b..677b8955490d 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/index/engine/FollowingEngineTests.java @@ -280,7 +280,7 @@ private FollowingEngine createEngine(Store store, EngineConfig config) throws IO SequenceNumbers.NO_OPS_PERFORMED, shardId, 1L); store.associateIndexWithNewTranslog(translogUuid); FollowingEngine followingEngine = new FollowingEngine(config); - followingEngine.recoverFromTranslog(); + followingEngine.recoverFromTranslog(Long.MAX_VALUE); return followingEngine; } From f4e9729d647add3d42b5bdd091278dcb3af727af Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 24 Aug 2018 09:51:21 +0200 Subject: [PATCH 132/283] Remove unsupported Version.V_5_* (#32937) This change removes the es 5x version constants and their usages. --- .../gradle/VersionCollection.groovy | 8 +- .../gradle/VersionCollectionTests.groovy | 4 +- .../common/CommonAnalysisPluginTests.java | 4 +- .../HtmlStripCharFilterFactoryTests.java | 2 +- .../mustache/MultiSearchTemplateRequest.java | 19 +- .../join/query/HasChildQueryBuilder.java | 11 +- .../join/query/HasParentQueryBuilder.java | 11 +- .../join/query/HasChildQueryBuilderTests.java | 4 - .../query/HasParentQueryBuilderTests.java | 4 - .../percolator/PercolateQueryBuilder.java | 8 +- .../PercolateQueryBuilderTests.java | 23 --- .../percolator/QueryBuilderStoreTests.java | 2 +- .../reindex/remote/RemoteRequestBuilders.java | 9 +- .../index/reindex/RoundTripTests.java | 9 +- .../remote/RemoteRequestBuildersTests.java | 13 +- .../RemoteScrollableHitSourceTests.java | 6 +- .../ICUCollationKeywordFieldMapper.java | 11 +- .../ICUCollationKeywordFieldMapperTests.java | 46 ----- .../mapper/murmur3/Murmur3FieldMapper.java | 5 - .../index/mapper/size/SizeFieldMapper.java | 4 - .../upgrades/FullClusterRestartIT.java | 3 - .../elasticsearch/ElasticsearchException.java | 30 +--- .../main/java/org/elasticsearch/Version.java | 168 +----------------- .../ClusterAllocationExplainRequest.java | 10 -- .../shards/ClusterSearchShardsRequest.java | 9 - .../shards/ClusterSearchShardsResponse.java | 25 ++- .../snapshots/get/GetSnapshotsRequest.java | 9 +- .../storedscripts/PutStoredScriptRequest.java | 10 +- .../indices/analyze/AnalyzeResponse.java | 18 +- .../indices/create/CreateIndexResponse.java | 9 +- .../mapping/put/PutMappingRequest.java | 4 - .../template/put/PutIndexTemplateRequest.java | 5 - .../validate/query/QueryExplanation.java | 10 +- .../validate/query/ValidateQueryRequest.java | 9 +- .../action/bulk/BulkItemResponse.java | 16 +- .../fieldcaps/FieldCapabilitiesRequest.java | 19 +- .../fieldcaps/FieldCapabilitiesResponse.java | 12 +- .../action/ingest/PutPipelineRequest.java | 11 +- .../ingest/SimulatePipelineRequest.java | 11 +- .../action/search/SearchRequest.java | 12 +- .../action/search/SearchResponse.java | 8 +- .../action/search/SearchTransportService.java | 14 +- .../termvectors/TermVectorsRequest.java | 12 +- .../cluster/SnapshotDeletionsInProgress.java | 4 +- .../cluster/SnapshotsInProgress.java | 15 +- .../cluster/block/ClusterBlock.java | 11 +- .../allocation/NodeAllocationResult.java | 17 +- .../index/query/InnerHitBuilder.java | 97 +--------- .../index/query/MoreLikeThisQueryBuilder.java | 12 +- .../index/query/NestedQueryBuilder.java | 11 +- .../index/query/QueryStringQueryBuilder.java | 38 +--- .../index/query/RangeQueryBuilder.java | 25 ++- .../index/query/SimpleQueryStringBuilder.java | 41 +---- .../index/reindex/BulkByScrollTask.java | 25 +-- .../index/reindex/RemoteInfo.java | 15 +- .../indices/flush/SyncedFlushService.java | 3 - .../ingest/PipelineConfiguration.java | 13 +- .../org/elasticsearch/monitor/os/OsStats.java | 10 +- .../PersistentTasksCustomMetaData.java | 2 +- .../org/elasticsearch/plugins/PluginInfo.java | 10 +- .../blobstore/BlobStoreRepository.java | 3 +- .../java/org/elasticsearch/script/Script.java | 138 ++------------ .../elasticsearch/script/ScriptMetaData.java | 51 +----- .../search/SearchShardTarget.java | 11 +- .../bucket/terms/IncludeExclude.java | 16 +- .../search/builder/SearchSourceBuilder.java | 8 +- .../search/collapse/CollapseBuilder.java | 22 +-- .../highlight/AbstractHighlighterBuilder.java | 25 +-- .../internal/ShardSearchLocalRequest.java | 28 +-- .../elasticsearch/snapshots/SnapshotInfo.java | 46 ++--- .../ExceptionSerializationTests.java | 83 +-------- .../java/org/elasticsearch/VersionTests.java | 83 +++++---- .../cluster/node/stats/NodeStatsTests.java | 1 - .../ClusterSearchShardsRequestTests.java | 2 +- .../ClusterSearchShardsResponseTests.java | 8 +- .../create/CreateIndexResponseTests.java | 22 --- .../mapping/put/PutMappingRequestTests.java | 27 --- .../put/PutIndexTemplateRequestTests.java | 84 --------- .../ingest/SimulatePipelineRequestTests.java | 20 --- .../CanMatchPreFilterSearchPhaseTests.java | 12 -- .../action/search/SearchResponseTests.java | 26 --- .../termvectors/TermVectorsUnitTests.java | 32 ---- .../metadata/IndexTemplateMetaDataTests.java | 50 ------ .../MetaDataIndexUpgradeServiceTests.java | 2 +- .../allocation/FailedNodeRoutingTests.java | 2 +- .../allocation/FailedShardsRoutingTests.java | 4 +- .../ResizeAllocationDeciderTests.java | 44 ----- .../common/unit/ByteSizeValueTests.java | 6 - .../common/util/IndexFolderUpgraderTests.java | 8 +- .../discovery/zen/MembershipActionTests.java | 6 +- .../org/elasticsearch/get/GetActionIT.java | 2 +- .../index/IndexSortSettingsTests.java | 11 -- .../index/analysis/AnalysisRegistryTests.java | 2 +- .../index/analysis/PreBuiltAnalyzerTests.java | 14 +- .../index/mapper/DynamicTemplateTests.java | 21 +-- .../mapper/ExternalFieldMapperTests.java | 6 +- .../index/mapper/TypeFieldMapperTests.java | 2 +- .../index/query/MatchQueryBuilderTests.java | 4 - .../query/MoreLikeThisQueryBuilderTests.java | 24 --- .../index/query/NestedQueryBuilderTests.java | 4 - .../reindex/BulkByScrollTaskStatusTests.java | 39 ++-- .../index/shard/ShardGetServiceTests.java | 44 ----- .../indices/analysis/AnalysisModuleTests.java | 8 +- .../indices/stats/IndexStatsIT.java | 2 +- .../plugins/PluginsServiceTests.java | 4 +- .../aggregations/bucket/GeoDistanceIT.java | 2 +- .../aggregations/bucket/GeoHashGridIT.java | 2 +- .../functionscore/DecayFunctionScoreIT.java | 2 +- .../search/geo/GeoBoundingBoxIT.java | 6 +- .../search/geo/GeoDistanceIT.java | 2 +- .../elasticsearch/search/geo/GeoFilterIT.java | 2 +- .../search/geo/GeoPolygonIT.java | 2 +- .../search/sort/GeoDistanceIT.java | 8 +- .../search/sort/GeoDistanceSortBuilderIT.java | 6 +- .../transport/RemoteClusterServiceTests.java | 5 - .../transport/TcpTransportTests.java | 21 ++- .../org/elasticsearch/test/OldIndexUtils.java | 31 ++-- .../section/ClientYamlTestSectionTests.java | 6 +- .../section/ClientYamlTestSuiteTests.java | 10 +- .../rest/yaml/section/SetupSectionTests.java | 6 +- .../rest/yaml/section/SkipSectionTests.java | 12 +- .../yaml/section/TeardownSectionTests.java | 6 +- .../protocol/xpack/XPackInfoResponse.java | 8 +- .../protocol/xpack/security/User.java | 13 +- .../xpack/core/ml/MlMetadata.java | 2 +- .../core/ml/action/DeleteDatafeedAction.java | 9 +- .../xpack/core/ml/action/DeleteJobAction.java | 9 +- .../xpack/core/ml/action/FlushJobAction.java | 17 +- .../core/ml/action/GetBucketsAction.java | 15 +- .../xpack/core/ml/action/OpenJobAction.java | 8 - .../core/ml/datafeed/DatafeedConfig.java | 8 - .../xpack/core/ml/datafeed/DatafeedState.java | 9 - .../core/ml/datafeed/DatafeedUpdate.java | 9 - .../xpack/core/ml/job/config/Detector.java | 12 +- .../xpack/core/ml/job/config/Job.java | 38 ++-- .../xpack/core/ml/job/config/JobState.java | 5 - .../output/FlushAcknowledgement.java | 9 +- .../autodetect/state/ModelSnapshot.java | 7 +- .../core/ml/job/results/AnomalyRecord.java | 9 - .../xpack/core/ml/job/results/Bucket.java | 16 -- .../core/ml/job/results/BucketInfluencer.java | 9 - .../xpack/core/ml/job/results/Influencer.java | 9 - .../xpack/core/ml/job/results/ModelPlot.java | 40 +---- .../core/security/authz/RoleDescriptor.java | 11 +- .../security/user/LogstashSystemUser.java | 3 - .../license/XPackLicenseStateTests.java | 2 +- .../xpack/core/ml/job/config/JobTests.java | 14 -- .../action/role/PutRoleRequestTests.java | 2 +- .../IndexDeprecationChecksTests.java | 151 +--------------- .../TransportIsolateDatafeedAction.java | 7 - .../ml/action/TransportKillProcessAction.java | 8 - .../ml/action/TransportOpenJobAction.java | 12 -- .../action/TransportOpenJobActionTests.java | 48 ----- .../xpack/ml/datafeed/DatafeedStateTests.java | 42 ----- .../xpack/ml/job/config/JobStateTests.java | 42 ----- .../action/MonitoringBulkDocTests.java | 20 --- .../action/MonitoringBulkRequestTests.java | 48 ----- .../monitoring/collector/CollectorTests.java | 28 --- .../exporter/BaseMonitoringDocTestCase.java | 22 --- .../authc/esnative/ReservedRealm.java | 4 +- .../transport/ServerTransportFilter.java | 53 ++---- .../filter/SecurityActionFilterTests.java | 2 +- .../authc/esnative/ReservedRealmTests.java | 8 - .../accesscontrol/IndicesPermissionTests.java | 4 +- .../support/SecurityIndexManagerTests.java | 6 +- .../transport/ServerTransportFilterTests.java | 44 ----- .../security/user/UserSerializationTests.java | 42 ----- .../xpack/upgrade/IndexUpgradeService.java | 2 +- .../elasticsearch/xpack/upgrade/Upgrade.java | 2 +- .../upgrade/IndexUpgradeServiceTests.java | 2 +- .../upgrade/InternalIndexReindexerIT.java | 4 +- .../protocol/xpack/XPackInfoResponse.java | 8 +- .../xpack/ml/job/process/ModelSnapshot.java | 6 +- .../protocol/xpack/security/User.java | 13 +- 174 files changed, 444 insertions(+), 2649 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/VersionCollection.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/VersionCollection.groovy index 7d5b793254fe..daab0efc8c69 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/VersionCollection.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/VersionCollection.groovy @@ -138,9 +138,8 @@ class VersionCollection { break } } - // caveat 0 - now dip back 2 versions to get the last supported snapshot version of the line - Version highestMinor = getHighestPreviousMinor(currentVersion.major - 1) - maintenanceBugfixSnapshot = replaceAsSnapshot(highestMinor) + // caveat 0 - the last supported snapshot of the line is on a version that we don't support (N-2) + maintenanceBugfixSnapshot = null } else { // caveat 3 did not apply. version is not a X.0.0, so we are somewhere on a X.Y line // only check till minor == 0 of the major @@ -293,7 +292,8 @@ class VersionCollection { * If you have a list [5.0.2, 5.1.2, 6.0.1, 6.1.1] and pass in 6 for the nextMajorVersion, it will return you 5.1.2 */ private Version getHighestPreviousMinor(Integer nextMajorVersion) { - return versionSet.headSet(Version.fromString("${nextMajorVersion}.0.0")).last() + SortedSet result = versionSet.headSet(Version.fromString("${nextMajorVersion}.0.0")) + return result.isEmpty() ? null : result.last() } /** diff --git a/buildSrc/src/test/groovy/org/elasticsearch/gradle/VersionCollectionTests.groovy b/buildSrc/src/test/groovy/org/elasticsearch/gradle/VersionCollectionTests.groovy index ad36c8407839..f6b9cb5fc95b 100644 --- a/buildSrc/src/test/groovy/org/elasticsearch/gradle/VersionCollectionTests.groovy +++ b/buildSrc/src/test/groovy/org/elasticsearch/gradle/VersionCollectionTests.groovy @@ -26,7 +26,7 @@ class VersionCollectionTests extends GradleUnitTestCase { assertEquals(vc.nextMinorSnapshot, Version.fromString("6.3.0-SNAPSHOT")) assertEquals(vc.stagedMinorSnapshot, Version.fromString("6.2.0-SNAPSHOT")) assertEquals(vc.nextBugfixSnapshot, Version.fromString("6.1.1-SNAPSHOT")) - assertEquals(vc.maintenanceBugfixSnapshot, Version.fromString("5.2.1-SNAPSHOT")) + assertNull(vc.maintenanceBugfixSnapshot) vc.indexCompatible.containsAll(vc.versions) @@ -65,7 +65,7 @@ class VersionCollectionTests extends GradleUnitTestCase { assertEquals(vc.nextMinorSnapshot, Version.fromString("6.3.0-SNAPSHOT")) assertEquals(vc.stagedMinorSnapshot, null) assertEquals(vc.nextBugfixSnapshot, Version.fromString("6.2.1-SNAPSHOT")) - assertEquals(vc.maintenanceBugfixSnapshot, Version.fromString("5.2.1-SNAPSHOT")) + assertNull(vc.maintenanceBugfixSnapshot) vc.indexCompatible.containsAll(vc.versions) diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java index 1d2b8a36810e..b5dc23fbdb89 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/CommonAnalysisPluginTests.java @@ -64,7 +64,7 @@ public void testNGramDeprecationWarning() throws IOException { public void testNGramNoDeprecationWarningPre6_4() throws IOException { Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) .put(IndexMetaData.SETTING_VERSION_CREATED, - VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.V_6_3_0)) + VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.V_6_3_0)) .build(); IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); @@ -104,7 +104,7 @@ public void testEdgeNGramDeprecationWarning() throws IOException { public void testEdgeNGramNoDeprecationWarningPre6_4() throws IOException { Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) .put(IndexMetaData.SETTING_VERSION_CREATED, - VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.V_6_3_0)) + VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.V_6_3_0)) .build(); IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/HtmlStripCharFilterFactoryTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/HtmlStripCharFilterFactoryTests.java index 0d5389a6d659..e28487797885 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/HtmlStripCharFilterFactoryTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/HtmlStripCharFilterFactoryTests.java @@ -60,7 +60,7 @@ public void testDeprecationWarning() throws IOException { public void testNoDeprecationWarningPre6_3() throws IOException { Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) .put(IndexMetaData.SETTING_VERSION_CREATED, - VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.V_6_2_4)) + VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.V_6_2_4)) .build(); IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java index caa9fa4831ad..eea9e31d4a79 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MultiSearchTemplateRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.script.mustache; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.CompositeIndicesRequest; @@ -120,21 +119,17 @@ public MultiSearchTemplateRequest indicesOptions(IndicesOptions indicesOptions) @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - maxConcurrentSearchRequests = in.readVInt(); - } + maxConcurrentSearchRequests = in.readVInt(); requests = in.readStreamableList(SearchTemplateRequest::new); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeVInt(maxConcurrentSearchRequests); - } + out.writeVInt(maxConcurrentSearchRequests); out.writeStreamableList(requests); } - + @Override public boolean equals(Object o) { if (this == o) return true; @@ -148,9 +143,9 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(maxConcurrentSearchRequests, requests, indicesOptions); - } - - public static byte[] writeMultiLineFormat(MultiSearchTemplateRequest multiSearchTemplateRequest, + } + + public static byte[] writeMultiLineFormat(MultiSearchTemplateRequest multiSearchTemplateRequest, XContent xContent) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); for (SearchTemplateRequest templateRequest : multiSearchTemplateRequest.requests()) { @@ -168,5 +163,5 @@ public static byte[] writeMultiLineFormat(MultiSearchTemplateRequest multiSearch } return output.toByteArray(); } - + } 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 3381356da417..e37a79600913 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 @@ -27,7 +27,6 @@ import org.apache.lucene.search.join.JoinUtil; import org.apache.lucene.search.join.ScoreMode; import org.apache.lucene.search.similarities.Similarity; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; @@ -125,15 +124,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeInt(maxChildren); out.writeVInt(scoreMode.ordinal()); out.writeNamedWriteable(query); - if (out.getVersion().before(Version.V_5_5_0)) { - final boolean hasInnerHit = innerHitBuilder != null; - out.writeBoolean(hasInnerHit); - if (hasInnerHit) { - innerHitBuilder.writeToParentChildBWC(out, query, type); - } - } else { - out.writeOptionalWriteable(innerHitBuilder); - } + out.writeOptionalWriteable(innerHitBuilder); out.writeBoolean(ignoreUnmapped); } diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java index 4e328ea2c984..e98fdb9e9699 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java @@ -21,7 +21,6 @@ import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.join.ScoreMode; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; @@ -97,15 +96,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeString(type); out.writeBoolean(score); out.writeNamedWriteable(query); - if (out.getVersion().before(Version.V_5_5_0)) { - final boolean hasInnerHit = innerHitBuilder != null; - out.writeBoolean(hasInnerHit); - if (hasInnerHit) { - innerHitBuilder.writeToParentChildBWC(out, query, type); - } - } else { - out.writeOptionalWriteable(innerHitBuilder); - } + out.writeOptionalWriteable(innerHitBuilder); out.writeBoolean(ignoreUnmapped); } diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java index 546677a2be4f..6e4e79d16e5a 100644 --- a/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java +++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java @@ -196,10 +196,6 @@ protected void doAssertLuceneQuery(HasChildQueryBuilder queryBuilder, Query quer public void testSerializationBWC() throws IOException { for (Version version : VersionUtils.allReleasedVersions()) { HasChildQueryBuilder testQuery = createTestQueryBuilder(); - if (version.before(Version.V_5_2_0) && testQuery.innerHit() != null) { - // ignore unmapped for inner_hits has been added on 5.2 - testQuery.innerHit().setIgnoreUnmapped(false); - } assertSerialization(testQuery, version); } } diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java index 6d6822007eee..164405f65344 100644 --- a/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java +++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java @@ -171,10 +171,6 @@ protected void doAssertLuceneQuery(HasParentQueryBuilder queryBuilder, Query que public void testSerializationBWC() throws IOException { for (Version version : VersionUtils.allReleasedVersions()) { HasParentQueryBuilder testQuery = createTestQueryBuilder(); - if (version.before(Version.V_5_2_0) && testQuery.innerHit() != null) { - // ignore unmapped for inner_hits has been added on 5.2 - testQuery.innerHit().setIgnoreUnmapped(false); - } assertSerialization(testQuery, version); } } diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java index f18efe4585bc..445076b8eba0 100644 --- a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java +++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java @@ -272,11 +272,7 @@ public PercolateQueryBuilder(String field, String documentType, String indexedDo documents = document != null ? Collections.singletonList(document) : Collections.emptyList(); } if (documents.isEmpty() == false) { - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - documentXContentType = in.readEnum(XContentType.class); - } else { - documentXContentType = XContentHelper.xContentType(documents.iterator().next()); - } + documentXContentType = in.readEnum(XContentType.class); } else { documentXContentType = null; } @@ -329,7 +325,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { BytesReference doc = documents.isEmpty() ? null : documents.iterator().next(); out.writeOptionalBytesReference(doc); } - if (documents.isEmpty() == false && out.getVersion().onOrAfter(Version.V_5_3_0)) { + if (documents.isEmpty() == false) { out.writeEnum(documentXContentType); } } diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolateQueryBuilderTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolateQueryBuilderTests.java index e7163edef94c..eb7af5f30d06 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolateQueryBuilderTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolateQueryBuilderTests.java @@ -27,7 +27,6 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.elasticsearch.ResourceNotFoundException; -import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; @@ -36,7 +35,6 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; @@ -57,7 +55,6 @@ import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -294,26 +291,6 @@ public void testCreateMultiDocumentSearcher() throws Exception { assertThat(result.clauses().get(1).getOccur(), equalTo(BooleanClause.Occur.MUST_NOT)); } - public void testSerializationBwc() throws IOException { - final byte[] data = Base64.getDecoder().decode("P4AAAAAFZmllbGQEdHlwZQAAAAAAAA57ImZvbyI6ImJhciJ9AAAAAA=="); - final Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_1, Version.V_5_0_2, - Version.V_5_1_1, Version.V_5_1_2, Version.V_5_2_0); - try (StreamInput in = StreamInput.wrap(data)) { - in.setVersion(version); - PercolateQueryBuilder queryBuilder = new PercolateQueryBuilder(in); - assertEquals("type", queryBuilder.getDocumentType()); - assertEquals("field", queryBuilder.getField()); - assertEquals("{\"foo\":\"bar\"}", queryBuilder.getDocuments().iterator().next().utf8ToString()); - assertEquals(XContentType.JSON, queryBuilder.getXContentType()); - - try (BytesStreamOutput out = new BytesStreamOutput()) { - out.setVersion(version); - queryBuilder.writeTo(out); - assertArrayEquals(data, out.bytes().toBytesRef().bytes); - } - } - } - private static BytesReference randomSource(Set usedFields) { try { // If we create two source that have the same field, but these fields have different kind of values (str vs. lng) then diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java index 5e97eadae83e..1c7ae3681ac6 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java @@ -74,7 +74,7 @@ public void testStoringQueryBuilders() throws IOException { BinaryFieldMapper fieldMapper = PercolatorFieldMapper.Builder.createQueryBuilderFieldBuilder( new Mapper.BuilderContext(settings, new ContentPath(0))); - Version version = randomBoolean() ? Version.V_5_6_0 : Version.V_6_0_0_beta2; + Version version = Version.V_6_0_0_beta2; try (IndexWriter indexWriter = new IndexWriter(directory, config)) { for (int i = 0; i < queryBuilders.length; i++) { queryBuilders[i] = new TermQueryBuilder(randomAlphaOfLength(4), randomAlphaOfLength(8)); diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuilders.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuilders.java index e8e3760882ee..d20be7479806 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuilders.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuilders.java @@ -61,7 +61,8 @@ static Request initialSearch(SearchRequest searchRequest, BytesReference query, if (searchRequest.scroll() != null) { TimeValue keepAlive = searchRequest.scroll().keepAlive(); - if (remoteVersion.before(Version.V_5_0_0)) { + // V_5_0_0 + if (remoteVersion.before(Version.fromId(5000099))) { /* Versions of Elasticsearch before 5.0 couldn't parse nanos or micros * so we toss out that resolution, rounding up because more scroll * timeout seems safer than less. */ @@ -117,7 +118,8 @@ static Request initialSearch(SearchRequest searchRequest, BytesReference query, for (int i = 1; i < searchRequest.source().storedFields().fieldNames().size(); i++) { fields.append(',').append(searchRequest.source().storedFields().fieldNames().get(i)); } - String storedFieldsParamName = remoteVersion.before(Version.V_5_0_0_alpha4) ? "fields" : "stored_fields"; + // V_5_0_0 + String storedFieldsParamName = remoteVersion.before(Version.fromId(5000099)) ? "fields" : "stored_fields"; request.addParameter(storedFieldsParamName, fields.toString()); } @@ -186,7 +188,8 @@ private static String sortToUri(SortBuilder sort) { static Request scroll(String scroll, TimeValue keepAlive, Version remoteVersion) { Request request = new Request("POST", "/_search/scroll"); - if (remoteVersion.before(Version.V_5_0_0)) { + // V_5_0_0 + if (remoteVersion.before(Version.fromId(5000099))) { /* Versions of Elasticsearch before 5.0 couldn't parse nanos or micros * so we toss out that resolution, rounding up so we shouldn't end up * with 0s. */ diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java index 97809c9bc8dc..0efedf449b56 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java @@ -155,13 +155,8 @@ private void assertRequestEquals(Version version, ReindexRequest request, Reinde assertEquals(request.getRemoteInfo().getUsername(), tripped.getRemoteInfo().getUsername()); assertEquals(request.getRemoteInfo().getPassword(), tripped.getRemoteInfo().getPassword()); assertEquals(request.getRemoteInfo().getHeaders(), tripped.getRemoteInfo().getHeaders()); - if (version.onOrAfter(Version.V_5_2_0)) { - assertEquals(request.getRemoteInfo().getSocketTimeout(), tripped.getRemoteInfo().getSocketTimeout()); - assertEquals(request.getRemoteInfo().getConnectTimeout(), tripped.getRemoteInfo().getConnectTimeout()); - } else { - assertEquals(RemoteInfo.DEFAULT_SOCKET_TIMEOUT, tripped.getRemoteInfo().getSocketTimeout()); - assertEquals(RemoteInfo.DEFAULT_CONNECT_TIMEOUT, tripped.getRemoteInfo().getConnectTimeout()); - } + assertEquals(request.getRemoteInfo().getSocketTimeout(), tripped.getRemoteInfo().getSocketTimeout()); + assertEquals(request.getRemoteInfo().getConnectTimeout(), tripped.getRemoteInfo().getConnectTimeout()); } } diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuildersTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuildersTests.java index b51525f20e3c..2f801811327b 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuildersTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteRequestBuildersTests.java @@ -136,13 +136,15 @@ public void testInitialSearchParamsFields() { // Test stored_fields for versions that support it searchRequest = new SearchRequest().source(new SearchSourceBuilder()); searchRequest.source().storedField("_source").storedField("_id"); - remoteVersion = Version.fromId(between(Version.V_5_0_0_alpha4_ID, Version.CURRENT.id)); + // V_5_0_0_alpha4 => current + remoteVersion = Version.fromId(between(5000004, Version.CURRENT.id)); assertThat(initialSearch(searchRequest, query, remoteVersion).getParameters(), hasEntry("stored_fields", "_source,_id")); // Test fields for versions that support it searchRequest = new SearchRequest().source(new SearchSourceBuilder()); searchRequest.source().storedField("_source").storedField("_id"); - remoteVersion = Version.fromId(between(2000099, Version.V_5_0_0_alpha4_ID - 1)); + // V_2_0_0 => V_5_0_0_alpha3 + remoteVersion = Version.fromId(between(2000099, 5000003)); assertThat(initialSearch(searchRequest, query, remoteVersion).getParameters(), hasEntry("fields", "_source,_id")); // Test extra fields for versions that need it @@ -190,7 +192,8 @@ public void testInitialSearchParamsMisc() { } private void assertScroll(Version remoteVersion, Map params, TimeValue requested) { - if (remoteVersion.before(Version.V_5_0_0)) { + // V_5_0_0 + if (remoteVersion.before(Version.fromId(5000099))) { // Versions of Elasticsearch prior to 5.0 can't parse nanos or micros in TimeValue. assertThat(params.get("scroll"), not(either(endsWith("nanos")).or(endsWith("micros")))); if (requested.getStringRep().endsWith("nanos") || requested.getStringRep().endsWith("micros")) { @@ -242,7 +245,7 @@ public void testScrollParams() { public void testScrollEntity() throws IOException { String scroll = randomAlphaOfLength(30); - HttpEntity entity = scroll(scroll, timeValueMillis(between(1, 1000)), Version.V_5_0_0).getEntity(); + HttpEntity entity = scroll(scroll, timeValueMillis(between(1, 1000)), Version.fromString("5.0.0")).getEntity(); assertEquals(ContentType.APPLICATION_JSON.toString(), entity.getContentType().getValue()); assertThat(Streams.copyToString(new InputStreamReader(entity.getContent(), StandardCharsets.UTF_8)), containsString("\"" + scroll + "\"")); @@ -255,7 +258,7 @@ public void testScrollEntity() throws IOException { public void testClearScroll() throws IOException { String scroll = randomAlphaOfLength(30); - Request request = clearScroll(scroll, Version.V_5_0_0); + Request request = clearScroll(scroll, Version.fromString("5.0.0")); assertEquals(ContentType.APPLICATION_JSON.toString(), request.getEntity().getContentType().getValue()); assertThat(Streams.copyToString(new InputStreamReader(request.getEntity().getContent(), StandardCharsets.UTF_8)), containsString("\"" + scroll + "\"")); diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteScrollableHitSourceTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteScrollableHitSourceTests.java index 92f370f8f636..d3d3cefea45e 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteScrollableHitSourceTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/remote/RemoteScrollableHitSourceTests.java @@ -150,13 +150,15 @@ public void testLookupRemoteVersion() throws Exception { assertTrue(called.get()); called.set(false); sourceWithMockedRemoteCall(false, ContentType.APPLICATION_JSON, "main/5_0_0_alpha_3.json").lookupRemoteVersion(v -> { - assertEquals(Version.V_5_0_0_alpha3, v); + // V_5_0_0_alpha3 + assertEquals(Version.fromId(5000003), v); called.set(true); }); assertTrue(called.get()); called.set(false); sourceWithMockedRemoteCall(false, ContentType.APPLICATION_JSON, "main/with_unknown_fields.json").lookupRemoteVersion(v -> { - assertEquals(Version.V_5_0_0_alpha3, v); + // V_5_0_0_alpha3 + assertEquals(Version.fromId(5000003), v); called.set(true); }); assertTrue(called.get()); diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java index c4c44222f470..0235e6e81368 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapper.java @@ -25,7 +25,6 @@ import com.ibm.icu.util.ULocale; import org.apache.lucene.document.Field; -import org.apache.lucene.document.SortedDocValuesField; import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexableField; @@ -35,7 +34,6 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.settings.Settings; @@ -56,7 +54,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.LongSupplier; public class ICUCollationKeywordFieldMapper extends FieldMapper { @@ -571,7 +568,6 @@ public static class TypeParser implements Mapper.TypeParser { private final String variableTop; private final boolean hiraganaQuaternaryMode; private final Collator collator; - private final BiFunction getDVField; protected ICUCollationKeywordFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, Settings indexSettings, MultiFields multiFields, CopyTo copyTo, String rules, String language, @@ -593,11 +589,6 @@ protected ICUCollationKeywordFieldMapper(String simpleName, MappedFieldType fiel this.variableTop = variableTop; this.hiraganaQuaternaryMode = hiraganaQuaternaryMode; this.collator = collator; - if (indexCreatedVersion.onOrAfter(Version.V_5_6_0)) { - getDVField = SortedSetDocValuesField::new; - } else { - getDVField = SortedDocValuesField::new; - } } @Override @@ -754,7 +745,7 @@ protected void parseCreateField(ParseContext context, List field } if (fieldType().hasDocValues()) { - fields.add(getDVField.apply(fieldType().name(), binaryValue)); + fields.add(new SortedSetDocValuesField(fieldType().name(), binaryValue)); } else if (fieldType().indexOptions() != IndexOptions.NONE || fieldType().stored()) { createFieldNamesField(context, fields); } diff --git a/plugins/analysis-icu/src/test/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapperTests.java b/plugins/analysis-icu/src/test/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapperTests.java index fff255970113..f39ae886dc45 100644 --- a/plugins/analysis-icu/src/test/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapperTests.java +++ b/plugins/analysis-icu/src/test/java/org/elasticsearch/index/mapper/ICUCollationKeywordFieldMapperTests.java @@ -28,11 +28,9 @@ import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.IndexableFieldType; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.Version; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; @@ -106,50 +104,6 @@ public void testDefaults() throws Exception { assertEquals(DocValuesType.SORTED_SET, fieldType.docValuesType()); } - public void testBackCompat() throws Exception { - indexService = createIndex("oldindex", Settings.builder().put("index.version.created", Version.V_5_5_0).build()); - parser = indexService.mapperService().documentMapperParser(); - - String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") - .startObject("properties").startObject("field").field("type", FIELD_TYPE).endObject().endObject() - .endObject().endObject()); - - DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping)); - - assertEquals(mapping, mapper.mappingSource().toString()); - - ParsedDocument doc = mapper.parse(SourceToParse.source("oldindex", "type", "1", BytesReference - .bytes(XContentFactory.jsonBuilder() - .startObject() - .field("field", "1234") - .endObject()), - XContentType.JSON)); - - IndexableField[] fields = doc.rootDoc().getFields("field"); - assertEquals(2, fields.length); - - Collator collator = Collator.getInstance(ULocale.ROOT); - RawCollationKey key = collator.getRawCollationKey("1234", null); - BytesRef expected = new BytesRef(key.bytes, 0, key.size); - - assertEquals(expected, fields[0].binaryValue()); - IndexableFieldType fieldType = fields[0].fieldType(); - assertThat(fieldType.omitNorms(), equalTo(true)); - assertFalse(fieldType.tokenized()); - assertFalse(fieldType.stored()); - assertThat(fieldType.indexOptions(), equalTo(IndexOptions.DOCS)); - assertThat(fieldType.storeTermVectors(), equalTo(false)); - assertThat(fieldType.storeTermVectorOffsets(), equalTo(false)); - assertThat(fieldType.storeTermVectorPositions(), equalTo(false)); - assertThat(fieldType.storeTermVectorPayloads(), equalTo(false)); - assertEquals(DocValuesType.NONE, fieldType.docValuesType()); - - assertEquals(expected, fields[1].binaryValue()); - fieldType = fields[1].fieldType(); - assertThat(fieldType.indexOptions(), equalTo(IndexOptions.NONE)); - assertEquals(DocValuesType.SORTED, fieldType.docValuesType()); - } - public void testNullValue() throws IOException { String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") .startObject("properties").startObject("field").field("type", FIELD_TYPE).endObject().endObject() diff --git a/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java b/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java index a6dc27b1f8a1..50af824fae9b 100644 --- a/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java +++ b/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java @@ -26,7 +26,6 @@ import org.apache.lucene.search.DocValuesFieldExistsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.Version; import org.elasticsearch.common.hash.MurmurHash3; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.fielddata.IndexFieldData; @@ -93,10 +92,6 @@ public static class TypeParser implements Mapper.TypeParser { throw new MapperParsingException("Setting [index] cannot be modified for field [" + name + "]"); } - if (parserContext.indexVersionCreated().before(Version.V_5_0_0_alpha2)) { - node.remove("precision_step"); - } - TypeParsers.parseField(builder, name, node, parserContext); return builder; diff --git a/plugins/mapper-size/src/main/java/org/elasticsearch/index/mapper/size/SizeFieldMapper.java b/plugins/mapper-size/src/main/java/org/elasticsearch/index/mapper/size/SizeFieldMapper.java index 04ab7ecd245f..ac5afeb3a109 100644 --- a/plugins/mapper-size/src/main/java/org/elasticsearch/index/mapper/size/SizeFieldMapper.java +++ b/plugins/mapper-size/src/main/java/org/elasticsearch/index/mapper/size/SizeFieldMapper.java @@ -82,10 +82,6 @@ public Builder enabled(EnabledAttributeMapper enabled) { @Override public SizeFieldMapper build(BuilderContext context) { setupFieldType(context); - if (context.indexCreatedVersion().onOrBefore(Version.V_5_0_0_alpha4)) { - // Make sure that the doc_values are disabled on indices created before V_5_0_0_alpha4 - fieldType.setHasDocValues(false); - } return new SizeFieldMapper(enabledState, fieldType, context.indexSettings()); } } 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 0b936e44e5be..d7111f64a1ba 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 @@ -908,9 +908,6 @@ public void testHistoryUUIDIsAdded() throws Exception { private void checkSnapshot(String snapshotName, int count, Version tookOnVersion) throws IOException { // Check the snapshot metadata, especially the version Request listSnapshotRequest = new Request("GET", "/_snapshot/repo/" + snapshotName); - if (false == (runningAgainstOldCluster && oldClusterVersion.before(Version.V_5_5_0))) { - listSnapshotRequest.addParameter("verbose", "true"); - } Map listSnapshotResponse = entityAsMap(client().performRequest(listSnapshotRequest)); assertEquals(singletonList(snapshotName), XContentMapValues.extractValue("snapshots.snapshot", listSnapshotResponse)); assertEquals(singletonList("SUCCESS"), XContentMapValues.extractValue("snapshots.state", listSnapshotResponse)); diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java index 9a02b76b3e03..c009bb3818cc 100644 --- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -44,7 +44,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -137,17 +136,7 @@ public ElasticsearchException(StreamInput in) throws IOException { super(in.readOptionalString(), in.readException()); readStackTrace(this, in); headers.putAll(in.readMapOfLists(StreamInput::readString, StreamInput::readString)); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - metadata.putAll(in.readMapOfLists(StreamInput::readString, StreamInput::readString)); - } else { - for (Iterator>> iterator = headers.entrySet().iterator(); iterator.hasNext(); ) { - Map.Entry> header = iterator.next(); - if (header.getKey().startsWith("es.")) { - metadata.put(header.getKey(), header.getValue()); - iterator.remove(); - } - } - } + metadata.putAll(in.readMapOfLists(StreamInput::readString, StreamInput::readString)); } /** @@ -287,15 +276,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(this.getMessage()); out.writeException(this.getCause()); writeStackTraces(this, out); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeMapOfLists(headers, StreamOutput::writeString, StreamOutput::writeString); - out.writeMapOfLists(metadata, StreamOutput::writeString, StreamOutput::writeString); - } else { - Map> finalHeaders = new HashMap<>(headers.size() + metadata.size()); - finalHeaders.putAll(headers); - finalHeaders.putAll(metadata); - out.writeMapOfLists(finalHeaders, StreamOutput::writeString, StreamOutput::writeString); - } + out.writeMapOfLists(headers, StreamOutput::writeString, StreamOutput::writeString); + out.writeMapOfLists(metadata, StreamOutput::writeString, StreamOutput::writeString); } public static ElasticsearchException readException(StreamInput input, int id) throws IOException { @@ -1018,11 +1000,11 @@ private enum ElasticsearchExceptionHandle { STATUS_EXCEPTION(org.elasticsearch.ElasticsearchStatusException.class, org.elasticsearch.ElasticsearchStatusException::new, 145, UNKNOWN_VERSION_ADDED), TASK_CANCELLED_EXCEPTION(org.elasticsearch.tasks.TaskCancelledException.class, - org.elasticsearch.tasks.TaskCancelledException::new, 146, Version.V_5_1_1), + org.elasticsearch.tasks.TaskCancelledException::new, 146, UNKNOWN_VERSION_ADDED), SHARD_LOCK_OBTAIN_FAILED_EXCEPTION(org.elasticsearch.env.ShardLockObtainFailedException.class, - org.elasticsearch.env.ShardLockObtainFailedException::new, 147, Version.V_5_0_2), + org.elasticsearch.env.ShardLockObtainFailedException::new, 147, UNKNOWN_VERSION_ADDED), UNKNOWN_NAMED_OBJECT_EXCEPTION(org.elasticsearch.common.xcontent.UnknownNamedObjectException.class, - org.elasticsearch.common.xcontent.UnknownNamedObjectException::new, 148, Version.V_5_2_0), + org.elasticsearch.common.xcontent.UnknownNamedObjectException::new, 148, UNKNOWN_VERSION_ADDED), TOO_MANY_BUCKETS_EXCEPTION(MultiBucketConsumerService.TooManyBucketsException.class, MultiBucketConsumerService.TooManyBucketsException::new, 149, Version.V_7_0_0_alpha1); diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index 1afe88f8d43e..7303e8d34c90 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -43,87 +43,6 @@ public class Version implements Comparable, ToXContentFragment { * values below 25 are for alpha builder (since 5.0), and above 25 and below 50 are beta builds, and below 99 are RC builds, with 99 * indicating a release the (internal) format of the id is there so we can easily do after/before checks on the id */ - public static final int V_5_0_0_alpha1_ID = 5000001; - public static final Version V_5_0_0_alpha1 = new Version(V_5_0_0_alpha1_ID, org.apache.lucene.util.Version.LUCENE_6_0_0); - public static final int V_5_0_0_alpha2_ID = 5000002; - public static final Version V_5_0_0_alpha2 = new Version(V_5_0_0_alpha2_ID, org.apache.lucene.util.Version.LUCENE_6_0_0); - public static final int V_5_0_0_alpha3_ID = 5000003; - public static final Version V_5_0_0_alpha3 = new Version(V_5_0_0_alpha3_ID, org.apache.lucene.util.Version.LUCENE_6_0_0); - public static final int V_5_0_0_alpha4_ID = 5000004; - public static final Version V_5_0_0_alpha4 = new Version(V_5_0_0_alpha4_ID, org.apache.lucene.util.Version.LUCENE_6_1_0); - public static final int V_5_0_0_alpha5_ID = 5000005; - public static final Version V_5_0_0_alpha5 = new Version(V_5_0_0_alpha5_ID, org.apache.lucene.util.Version.LUCENE_6_1_0); - public static final int V_5_0_0_beta1_ID = 5000026; - public static final Version V_5_0_0_beta1 = new Version(V_5_0_0_beta1_ID, org.apache.lucene.util.Version.LUCENE_6_2_0); - public static final int V_5_0_0_rc1_ID = 5000051; - public static final Version V_5_0_0_rc1 = new Version(V_5_0_0_rc1_ID, org.apache.lucene.util.Version.LUCENE_6_2_0); - public static final int V_5_0_0_ID = 5000099; - public static final Version V_5_0_0 = new Version(V_5_0_0_ID, org.apache.lucene.util.Version.LUCENE_6_2_0); - public static final int V_5_0_1_ID = 5000199; - public static final Version V_5_0_1 = new Version(V_5_0_1_ID, org.apache.lucene.util.Version.LUCENE_6_2_1); - public static final int V_5_0_2_ID = 5000299; - public static final Version V_5_0_2 = new Version(V_5_0_2_ID, org.apache.lucene.util.Version.LUCENE_6_2_1); - // no version constant for 5.1.0 due to inadvertent release - public static final int V_5_1_1_ID = 5010199; - public static final Version V_5_1_1 = new Version(V_5_1_1_ID, org.apache.lucene.util.Version.LUCENE_6_3_0); - public static final int V_5_1_2_ID = 5010299; - public static final Version V_5_1_2 = new Version(V_5_1_2_ID, org.apache.lucene.util.Version.LUCENE_6_3_0); - public static final int V_5_2_0_ID = 5020099; - public static final Version V_5_2_0 = new Version(V_5_2_0_ID, org.apache.lucene.util.Version.LUCENE_6_4_0); - public static final int V_5_2_1_ID = 5020199; - public static final Version V_5_2_1 = new Version(V_5_2_1_ID, org.apache.lucene.util.Version.LUCENE_6_4_1); - public static final int V_5_2_2_ID = 5020299; - public static final Version V_5_2_2 = new Version(V_5_2_2_ID, org.apache.lucene.util.Version.LUCENE_6_4_1); - public static final int V_5_3_0_ID = 5030099; - public static final Version V_5_3_0 = new Version(V_5_3_0_ID, org.apache.lucene.util.Version.LUCENE_6_4_1); - public static final int V_5_3_1_ID = 5030199; - public static final Version V_5_3_1 = new Version(V_5_3_1_ID, org.apache.lucene.util.Version.LUCENE_6_4_2); - public static final int V_5_3_2_ID = 5030299; - public static final Version V_5_3_2 = new Version(V_5_3_2_ID, org.apache.lucene.util.Version.LUCENE_6_4_2); - public static final int V_5_3_3_ID = 5030399; - public static final Version V_5_3_3 = new Version(V_5_3_3_ID, org.apache.lucene.util.Version.LUCENE_6_4_2); - public static final int V_5_4_0_ID = 5040099; - public static final Version V_5_4_0 = new Version(V_5_4_0_ID, org.apache.lucene.util.Version.LUCENE_6_5_0); - public static final int V_5_4_1_ID = 5040199; - public static final Version V_5_4_1 = new Version(V_5_4_1_ID, org.apache.lucene.util.Version.LUCENE_6_5_1); - public static final int V_5_4_2_ID = 5040299; - public static final Version V_5_4_2 = new Version(V_5_4_2_ID, org.apache.lucene.util.Version.LUCENE_6_5_1); - public static final int V_5_4_3_ID = 5040399; - public static final Version V_5_4_3 = new Version(V_5_4_3_ID, org.apache.lucene.util.Version.LUCENE_6_5_1); - public static final int V_5_5_0_ID = 5050099; - public static final Version V_5_5_0 = new Version(V_5_5_0_ID, org.apache.lucene.util.Version.LUCENE_6_6_0); - public static final int V_5_5_1_ID = 5050199; - public static final Version V_5_5_1 = new Version(V_5_5_1_ID, org.apache.lucene.util.Version.LUCENE_6_6_0); - public static final int V_5_5_2_ID = 5050299; - public static final Version V_5_5_2 = new Version(V_5_5_2_ID, org.apache.lucene.util.Version.LUCENE_6_6_0); - public static final int V_5_5_3_ID = 5050399; - public static final Version V_5_5_3 = new Version(V_5_5_3_ID, org.apache.lucene.util.Version.LUCENE_6_6_0); - public static final int V_5_6_0_ID = 5060099; - public static final Version V_5_6_0 = new Version(V_5_6_0_ID, org.apache.lucene.util.Version.LUCENE_6_6_0); - public static final int V_5_6_1_ID = 5060199; - public static final Version V_5_6_1 = new Version(V_5_6_1_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_2_ID = 5060299; - public static final Version V_5_6_2 = new Version(V_5_6_2_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_3_ID = 5060399; - public static final Version V_5_6_3 = new Version(V_5_6_3_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_4_ID = 5060499; - public static final Version V_5_6_4 = new Version(V_5_6_4_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_5_ID = 5060599; - public static final Version V_5_6_5 = new Version(V_5_6_5_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_6_ID = 5060699; - public static final Version V_5_6_6 = new Version(V_5_6_6_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_7_ID = 5060799; - public static final Version V_5_6_7 = new Version(V_5_6_7_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_8_ID = 5060899; - public static final Version V_5_6_8 = new Version(V_5_6_8_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_9_ID = 5060999; - public static final Version V_5_6_9 = new Version(V_5_6_9_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_10_ID = 5061099; - public static final Version V_5_6_10 = new Version(V_5_6_10_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_11_ID = 5061199; - public static final Version V_5_6_11 = new Version(V_5_6_11_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); - public static final int V_5_6_12_ID = 5061299; - public static final Version V_5_6_12 = new Version(V_5_6_12_ID, org.apache.lucene.util.Version.LUCENE_6_6_1); public static final int V_6_0_0_alpha1_ID = 6000001; public static final Version V_6_0_0_alpha1 = new Version(V_6_0_0_alpha1_ID, org.apache.lucene.util.Version.LUCENE_7_0_0); @@ -248,86 +167,6 @@ public static Version fromId(int id) { return V_6_0_0_alpha2; case V_6_0_0_alpha1_ID: return V_6_0_0_alpha1; - case V_5_6_12_ID: - return V_5_6_12; - case V_5_6_11_ID: - return V_5_6_11; - case V_5_6_10_ID: - return V_5_6_10; - case V_5_6_9_ID: - return V_5_6_9; - case V_5_6_8_ID: - return V_5_6_8; - case V_5_6_7_ID: - return V_5_6_7; - case V_5_6_6_ID: - return V_5_6_6; - case V_5_6_5_ID: - return V_5_6_5; - case V_5_6_4_ID: - return V_5_6_4; - case V_5_6_3_ID: - return V_5_6_3; - case V_5_6_2_ID: - return V_5_6_2; - case V_5_6_1_ID: - return V_5_6_1; - case V_5_6_0_ID: - return V_5_6_0; - case V_5_5_3_ID: - return V_5_5_3; - case V_5_5_2_ID: - return V_5_5_2; - case V_5_5_1_ID: - return V_5_5_1; - case V_5_5_0_ID: - return V_5_5_0; - case V_5_4_3_ID: - return V_5_4_3; - case V_5_4_2_ID: - return V_5_4_2; - case V_5_4_1_ID: - return V_5_4_1; - case V_5_4_0_ID: - return V_5_4_0; - case V_5_3_3_ID: - return V_5_3_3; - case V_5_3_2_ID: - return V_5_3_2; - case V_5_3_1_ID: - return V_5_3_1; - case V_5_3_0_ID: - return V_5_3_0; - case V_5_2_2_ID: - return V_5_2_2; - case V_5_2_1_ID: - return V_5_2_1; - case V_5_2_0_ID: - return V_5_2_0; - case V_5_1_2_ID: - return V_5_1_2; - case V_5_1_1_ID: - return V_5_1_1; - case V_5_0_2_ID: - return V_5_0_2; - case V_5_0_1_ID: - return V_5_0_1; - case V_5_0_0_ID: - return V_5_0_0; - case V_5_0_0_rc1_ID: - return V_5_0_0_rc1; - case V_5_0_0_beta1_ID: - return V_5_0_0_beta1; - case V_5_0_0_alpha5_ID: - return V_5_0_0_alpha5; - case V_5_0_0_alpha4_ID: - return V_5_0_0_alpha4; - case V_5_0_0_alpha3_ID: - return V_5_0_0_alpha3; - case V_5_0_0_alpha2_ID: - return V_5_0_0_alpha2; - case V_5_0_0_alpha1_ID: - return V_5_0_0_alpha1; default: return new Version(id, org.apache.lucene.util.Version.LATEST); } @@ -477,8 +316,11 @@ private static class DeclaredVersionsHolder { * is a beta or RC release then the version itself is returned. */ public Version minimumCompatibilityVersion() { - if (major >= 6) { - // all major versions from 6 onwards are compatible with last minor series of the previous major + if (major == 6) { + // force the minimum compatibility for version 6 to 5.6 since we don't reference version 5 anymore + return Version.fromId(5060099); + } else if (major >= 7) { + // all major versions from 7 onwards are compatible with last minor series of the previous major Version bwcVersion = null; for (int i = DeclaredVersionsHolder.DECLARED_VERSIONS.size() - 1; i >= 0; i--) { diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java index 40960c336208..b6959afba5d8 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/allocation/ClusterAllocationExplainRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.admin.cluster.allocation; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.MasterNodeRequest; import org.elasticsearch.common.Nullable; @@ -69,7 +68,6 @@ public ClusterAllocationExplainRequest() { public ClusterAllocationExplainRequest(StreamInput in) throws IOException { super(in); - checkVersion(in.getVersion()); this.index = in.readOptionalString(); this.shard = in.readOptionalVInt(); this.primary = in.readOptionalBoolean(); @@ -94,7 +92,6 @@ public ClusterAllocationExplainRequest(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { - checkVersion(out.getVersion()); super.writeTo(out); out.writeOptionalString(index); out.writeOptionalVInt(shard); @@ -251,11 +248,4 @@ public static ClusterAllocationExplainRequest parse(XContentParser parser) throw public void readFrom(StreamInput in) throws IOException { throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); } - - private void checkVersion(Version version) { - if (version.before(Version.V_5_2_0)) { - throw new IllegalArgumentException("cannot explain shards in a mixed-cluster with pre-" + Version.V_5_2_0 + - " nodes, node version [" + version + "]"); - } - } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequest.java index 3ae5c2d683a2..4798aeb67c19 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.admin.cluster.shards; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndicesOptions; @@ -59,10 +58,6 @@ public ClusterSearchShardsRequest(StreamInput in) throws IOException { routing = in.readOptionalString(); preference = in.readOptionalString(); - if (in.getVersion().onOrBefore(Version.V_5_1_1)) { - //types - in.readStringArray(); - } indicesOptions = IndicesOptions.readIndicesOptions(in); } @@ -78,10 +73,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(routing); out.writeOptionalString(preference); - if (out.getVersion().onOrBefore(Version.V_5_1_1)) { - //types - out.writeStringArray(Strings.EMPTY_ARRAY); - } indicesOptions.writeIndicesOptions(out); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponse.java index 28c7903efde8..f8d448d0fe11 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponse.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.admin.cluster.shards; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.StreamInput; @@ -77,14 +76,12 @@ public void readFrom(StreamInput in) throws IOException { for (int i = 0; i < nodes.length; i++) { nodes[i] = new DiscoveryNode(in); } - if (in.getVersion().onOrAfter(Version.V_5_1_1)) { - int size = in.readVInt(); - indicesAndFilters = new HashMap<>(); - for (int i = 0; i < size; i++) { - String index = in.readString(); - AliasFilter aliasFilter = new AliasFilter(in); - indicesAndFilters.put(index, aliasFilter); - } + int size = in.readVInt(); + indicesAndFilters = new HashMap<>(); + for (int i = 0; i < size; i++) { + String index = in.readString(); + AliasFilter aliasFilter = new AliasFilter(in); + indicesAndFilters.put(index, aliasFilter); } } @@ -99,12 +96,10 @@ public void writeTo(StreamOutput out) throws IOException { for (DiscoveryNode node : nodes) { node.writeTo(out); } - if (out.getVersion().onOrAfter(Version.V_5_1_1)) { - out.writeVInt(indicesAndFilters.size()); - for (Map.Entry entry : indicesAndFilters.entrySet()) { - out.writeString(entry.getKey()); - entry.getValue().writeTo(out); - } + out.writeVInt(indicesAndFilters.size()); + for (Map.Entry entry : indicesAndFilters.entrySet()) { + out.writeString(entry.getKey()); + entry.getValue().writeTo(out); } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java index b3b24b570eed..41ae57031d32 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java @@ -28,7 +28,6 @@ import java.io.IOException; import static org.elasticsearch.action.ValidateActions.addValidationError; -import static org.elasticsearch.snapshots.SnapshotInfo.VERBOSE_INTRODUCED; /** * Get snapshot request @@ -75,9 +74,7 @@ public GetSnapshotsRequest(StreamInput in) throws IOException { repository = in.readString(); snapshots = in.readStringArray(); ignoreUnavailable = in.readBoolean(); - if (in.getVersion().onOrAfter(VERBOSE_INTRODUCED)) { - verbose = in.readBoolean(); - } + verbose = in.readBoolean(); } @Override @@ -86,9 +83,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(repository); out.writeStringArray(snapshots); out.writeBoolean(ignoreUnavailable); - if (out.getVersion().onOrAfter(VERBOSE_INTRODUCED)) { - out.writeBoolean(verbose); - } + out.writeBoolean(verbose); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/PutStoredScriptRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/PutStoredScriptRequest.java index 6f702cbbe7c0..d02d6272c951 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/PutStoredScriptRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/PutStoredScriptRequest.java @@ -121,11 +121,7 @@ public void readFrom(StreamInput in) throws IOException { } id = in.readOptionalString(); content = in.readBytesReference(); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - xContentType = in.readEnum(XContentType.class); - } else { - xContentType = XContentHelper.xContentType(content); - } + xContentType = in.readEnum(XContentType.class); if (in.getVersion().onOrAfter(Version.V_6_0_0_alpha2)) { context = in.readOptionalString(); source = new StoredScriptSource(in); @@ -143,9 +139,7 @@ public void writeTo(StreamOutput out) throws IOException { } out.writeOptionalString(id); out.writeBytesReference(content); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeEnum(xContentType); - } + out.writeEnum(xContentType); if (out.getVersion().onOrAfter(Version.V_6_0_0_alpha2)) { out.writeOptionalString(context); source.writeTo(out); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/analyze/AnalyzeResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/analyze/AnalyzeResponse.java index d45ab2682a5e..e571db951cbc 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/analyze/AnalyzeResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/analyze/AnalyzeResponse.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.action.admin.indices.analyze; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; @@ -191,15 +190,10 @@ public void readFrom(StreamInput in) throws IOException { startOffset = in.readInt(); endOffset = in.readInt(); position = in.readVInt(); - if (in.getVersion().onOrAfter(Version.V_5_2_0)) { - Integer len = in.readOptionalVInt(); - if (len != null) { - positionLength = len; - } else { - positionLength = 1; - } - } - else { + Integer len = in.readOptionalVInt(); + if (len != null) { + positionLength = len; + } else { positionLength = 1; } type = in.readOptionalString(); @@ -212,9 +206,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeInt(startOffset); out.writeInt(endOffset); out.writeVInt(position); - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - out.writeOptionalVInt(positionLength > 1 ? positionLength : null); - } + out.writeOptionalVInt(positionLength > 1 ? positionLength : null); out.writeOptionalString(type); out.writeMapWithConsistentOrder(attributes); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponse.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponse.java index c858d0bb1065..79192693620d 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponse.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.admin.indices.create; -import org.elasticsearch.Version; import org.elasticsearch.action.support.master.ShardsAcknowledgedResponse; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; @@ -66,18 +65,14 @@ protected CreateIndexResponse(boolean acknowledged, boolean shardsAcknowledged, public void readFrom(StreamInput in) throws IOException { super.readFrom(in); readShardsAcknowledged(in); - if (in.getVersion().onOrAfter(Version.V_5_6_0)) { - index = in.readString(); - } + index = in.readString(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); writeShardsAcknowledged(out); - if (out.getVersion().onOrAfter(Version.V_5_6_0)) { - out.writeString(index); - } + out.writeString(index); } public String index() { diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java index 1556ee2341d2..a827444acb8c 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java @@ -297,10 +297,6 @@ public void readFrom(StreamInput in) throws IOException { indicesOptions = IndicesOptions.readIndicesOptions(in); type = in.readOptionalString(); source = in.readString(); - if (in.getVersion().before(Version.V_5_3_0)) { - // we do not know the format from earlier versions so convert if necessary - source = XContentHelper.convertToJson(new BytesArray(source), false, false, XContentFactory.xContentType(source)); - } if (in.getVersion().before(Version.V_7_0_0_alpha1)) { in.readBoolean(); // updateAllTypes } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java index d194b9acd1b7..f9431a3ad02b 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java @@ -492,11 +492,6 @@ public void readFrom(StreamInput in) throws IOException { for (int i = 0; i < size; i++) { final String type = in.readString(); String mappingSource = in.readString(); - if (in.getVersion().before(Version.V_5_3_0)) { - // we do not know the incoming type so convert it if needed - mappingSource = - XContentHelper.convertToJson(new BytesArray(mappingSource), false, false, XContentFactory.xContentType(mappingSource)); - } mappings.put(type, mappingSource); } int customSize = in.readVInt(); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java index d0a62fe771d1..b60bc407ce70 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/QueryExplanation.java @@ -120,11 +120,7 @@ public void readFrom(StreamInput in) throws IOException { } else { index = in.readString(); } - if (in.getVersion().onOrAfter(Version.V_5_4_0)) { - shard = in.readInt(); - } else { - shard = RANDOM_SHARD; - } + shard = in.readInt(); valid = in.readBoolean(); explanation = in.readOptionalString(); error = in.readOptionalString(); @@ -137,9 +133,7 @@ public void writeTo(StreamOutput out) throws IOException { } else { out.writeString(index); } - if (out.getVersion().onOrAfter(Version.V_5_4_0)) { - out.writeInt(shard); - } + out.writeInt(shard); out.writeBoolean(valid); out.writeOptionalString(explanation); out.writeOptionalString(error); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java index 7694e7583c89..a30c9ba84610 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/ValidateQueryRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.admin.indices.validate.query; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ValidateActions; import org.elasticsearch.action.support.IndicesOptions; @@ -156,9 +155,7 @@ public void readFrom(StreamInput in) throws IOException { } explain = in.readBoolean(); rewrite = in.readBoolean(); - if (in.getVersion().onOrAfter(Version.V_5_4_0)) { - allShards = in.readBoolean(); - } + allShards = in.readBoolean(); } @Override @@ -171,9 +168,7 @@ public void writeTo(StreamOutput out) throws IOException { } out.writeBoolean(explain); out.writeBoolean(rewrite); - if (out.getVersion().onOrAfter(Version.V_5_4_0)) { - out.writeBoolean(allShards); - } + out.writeBoolean(allShards); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java b/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java index fb535d312cf6..9b9be3a41476 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java @@ -244,8 +244,8 @@ public void writeTo(StreamOutput out) throws IOException { } private static boolean supportsAbortedFlag(Version version) { - // The "aborted" flag was added for 5.5.3 and 5.6.0, but was not in 6.0.0-beta2 - return version.after(Version.V_6_0_0_beta2) || (version.major == 5 && version.onOrAfter(Version.V_5_5_3)); + // The "aborted" flag was not in 6.0.0-beta2 + return version.after(Version.V_6_0_0_beta2); } /** @@ -447,11 +447,7 @@ public static BulkItemResponse readBulkItem(StreamInput in) throws IOException { @Override public void readFrom(StreamInput in) throws IOException { id = in.readVInt(); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - opType = OpType.fromId(in.readByte()); - } else { - opType = OpType.fromString(in.readString()); - } + opType = OpType.fromId(in.readByte()); byte type = in.readByte(); if (type == 0) { @@ -474,11 +470,7 @@ public void readFrom(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { out.writeVInt(id); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeByte(opType.getId()); - } else { - out.writeString(opType.getLowercase()); - } + out.writeByte(opType.getId()); if (response == null) { out.writeByte((byte) 2); diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java index 636af6101ae0..22d231d3711b 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.fieldcaps; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; @@ -81,24 +80,18 @@ void setMergeResults(boolean mergeResults) { public void readFrom(StreamInput in) throws IOException { super.readFrom(in); fields = in.readStringArray(); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - indices = in.readStringArray(); - indicesOptions = IndicesOptions.readIndicesOptions(in); - mergeResults = in.readBoolean(); - } else { - mergeResults = true; - } + indices = in.readStringArray(); + indicesOptions = IndicesOptions.readIndicesOptions(in); + mergeResults = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeStringArray(fields); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeStringArray(indices); - indicesOptions.writeIndicesOptions(out); - out.writeBoolean(mergeResults); - } + out.writeStringArray(indices); + indicesOptions.writeIndicesOptions(out); + out.writeBoolean(mergeResults); } /** diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java index 959b4e572b71..178639bd4348 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.fieldcaps; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.Tuple; @@ -95,11 +94,7 @@ public void readFrom(StreamInput in) throws IOException { super.readFrom(in); this.responseMap = in.readMap(StreamInput::readString, FieldCapabilitiesResponse::readField); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - indexResponses = in.readList(FieldCapabilitiesIndexResponse::new); - } else { - indexResponses = Collections.emptyList(); - } + indexResponses = in.readList(FieldCapabilitiesIndexResponse::new); } private static Map readField(StreamInput in) throws IOException { @@ -110,10 +105,7 @@ private static Map readField(StreamInput in) throws I public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeMap(responseMap, StreamOutput::writeString, FieldCapabilitiesResponse::writeField); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeList(indexResponses); - } - + out.writeList(indexResponses); } private static void writeField(StreamOutput out, diff --git a/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineRequest.java b/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineRequest.java index 6447b0557db0..abff28bcf553 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineRequest.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/PutPipelineRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.ingest; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.AcknowledgedRequest; import org.elasticsearch.common.bytes.BytesReference; @@ -82,11 +81,7 @@ public void readFrom(StreamInput in) throws IOException { super.readFrom(in); id = in.readString(); source = in.readBytesReference(); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - xContentType = in.readEnum(XContentType.class); - } else { - xContentType = XContentHelper.xContentType(source); - } + xContentType = in.readEnum(XContentType.class); } @Override @@ -94,9 +89,7 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeString(id); out.writeBytesReference(source); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeEnum(xContentType); - } + out.writeEnum(xContentType); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java index 8405bb85b4b1..fecee5f265fe 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.ingest; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.bytes.BytesReference; @@ -76,11 +75,7 @@ public SimulatePipelineRequest(BytesReference source, XContentType xContentType) id = in.readOptionalString(); verbose = in.readBoolean(); source = in.readBytesReference(); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - xContentType = in.readEnum(XContentType.class); - } else { - xContentType = XContentHelper.xContentType(source); - } + xContentType = in.readEnum(XContentType.class); } @Override @@ -123,9 +118,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(id); out.writeBoolean(verbose); out.writeBytesReference(source); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeEnum(xContentType); - } + out.writeEnum(xContentType); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index e67517c4852b..e560e53ed7b6 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -135,10 +135,8 @@ public SearchRequest(StreamInput in) throws IOException { indicesOptions = IndicesOptions.readIndicesOptions(in); requestCache = in.readOptionalBoolean(); batchedReduceSize = in.readVInt(); - if (in.getVersion().onOrAfter(Version.V_5_6_0)) { - maxConcurrentShardRequests = in.readVInt(); - preFilterShardSize = in.readVInt(); - } + maxConcurrentShardRequests = in.readVInt(); + preFilterShardSize = in.readVInt(); if (in.getVersion().onOrAfter(Version.V_6_3_0)) { allowPartialSearchResults = in.readOptionalBoolean(); } @@ -160,10 +158,8 @@ public void writeTo(StreamOutput out) throws IOException { indicesOptions.writeIndicesOptions(out); out.writeOptionalBoolean(requestCache); out.writeVInt(batchedReduceSize); - if (out.getVersion().onOrAfter(Version.V_5_6_0)) { - out.writeVInt(maxConcurrentShardRequests); - out.writeVInt(preFilterShardSize); - } + out.writeVInt(maxConcurrentShardRequests); + out.writeVInt(preFilterShardSize); if (out.getVersion().onOrAfter(Version.V_6_3_0)) { out.writeOptionalBoolean(allowPartialSearchResults); } diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java index 2a97798764e5..0273d5e58219 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java @@ -374,9 +374,7 @@ public void readFrom(StreamInput in) throws IOException { } scrollId = in.readOptionalString(); tookInMillis = in.readVLong(); - if (in.getVersion().onOrAfter(Version.V_5_6_0)) { - skippedShards = in.readVInt(); - } + skippedShards = in.readVInt(); } @Override @@ -395,9 +393,7 @@ public void writeTo(StreamOutput out) throws IOException { } out.writeOptionalString(scrollId); out.writeVLong(tookInMillis); - if(out.getVersion().onOrAfter(Version.V_5_6_0)) { - out.writeVInt(skippedShards); - } + out.writeVInt(skippedShards); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java b/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java index 133d0291df59..a4ea2616e0a2 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.search; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListenerResponseHandler; import org.elasticsearch.action.IndicesRequest; @@ -113,17 +112,8 @@ public void sendFreeContext(Transport.Connection connection, long contextId, fin public void sendCanMatch(Transport.Connection connection, final ShardSearchTransportRequest request, SearchTask task, final ActionListener listener) { - if (connection.getNode().getVersion().onOrAfter(Version.V_5_6_0)) { - transportService.sendChildRequest(connection, QUERY_CAN_MATCH_NAME, request, task, - TransportRequestOptions.EMPTY, new ActionListenerResponseHandler<>(listener, CanMatchResponse::new)); - } else { - // this might look weird but if we are in a CrossClusterSearch environment we can get a connection - // to a pre 5.latest node which is proxied by a 5.latest node under the hood since we are only compatible with 5.latest - // instead of sending the request we shortcut it here and let the caller deal with this -- see #25704 - // also failing the request instead of returning a fake answer might trigger a retry on a replica which might be on a - // compatible node - throw new IllegalArgumentException("can_match is not supported on pre 5.6 nodes"); - } + transportService.sendChildRequest(connection, QUERY_CAN_MATCH_NAME, request, task, + TransportRequestOptions.EMPTY, new ActionListenerResponseHandler<>(listener, CanMatchResponse::new)); } public void sendClearAllScrollContexts(Transport.Connection connection, final ActionListener listener) { diff --git a/server/src/main/java/org/elasticsearch/action/termvectors/TermVectorsRequest.java b/server/src/main/java/org/elasticsearch/action/termvectors/TermVectorsRequest.java index f416627c1e08..d6bf911e572c 100644 --- a/server/src/main/java/org/elasticsearch/action/termvectors/TermVectorsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/termvectors/TermVectorsRequest.java @@ -498,14 +498,10 @@ public void readFrom(StreamInput in) throws IOException { if (in.readBoolean()) { doc = in.readBytesReference(); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - xContentType = in.readEnum(XContentType.class); - } else { - xContentType = XContentHelper.xContentType(doc); - } + xContentType = in.readEnum(XContentType.class); } routing = in.readOptionalString(); - + if (in.getVersion().before(Version.V_7_0_0_alpha1)) { in.readOptionalString(); // _parent } @@ -546,9 +542,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(doc != null); if (doc != null) { out.writeBytesReference(doc); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeEnum(xContentType); - } + out.writeEnum(xContentType); } out.writeOptionalString(routing); if (out.getVersion().before(Version.V_7_0_0_alpha1)) { diff --git a/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java b/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java index 234d1ef9f17f..0134b798c72f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/SnapshotDeletionsInProgress.java @@ -40,8 +40,6 @@ public class SnapshotDeletionsInProgress extends AbstractNamedDiffable implements Custom { public static final String TYPE = "snapshot_deletions"; - // the version where SnapshotDeletionsInProgress was introduced - public static final Version VERSION_INTRODUCED = Version.V_5_2_0; // the list of snapshot deletion request entries private final List entries; @@ -135,7 +133,7 @@ public static NamedDiff readDiffFrom(StreamInput in) throws IOException @Override public Version getMinimalSupportedVersion() { - return VERSION_INTRODUCED; + return Version.CURRENT.minimumCompatibilityVersion(); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java index 87563c968af1..565c5134d1b3 100644 --- a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java +++ b/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java @@ -48,12 +48,6 @@ public class SnapshotsInProgress extends AbstractNamedDiffable implements Custom { public static final String TYPE = "snapshots"; - // denotes an undefined repository state id, which will happen when receiving a cluster state with - // a snapshot in progress from a pre 5.2.x node - public static final long UNDEFINED_REPOSITORY_STATE_ID = -2L; - // the version where repository state ids were introduced - private static final Version REPOSITORY_ID_INTRODUCED_VERSION = Version.V_5_2_0; - @Override public boolean equals(Object o) { if (this == o) return true; @@ -432,10 +426,7 @@ public SnapshotsInProgress(StreamInput in) throws IOException { builder.put(shardId, new ShardSnapshotStatus(nodeId, shardState, reason)); } } - long repositoryStateId = UNDEFINED_REPOSITORY_STATE_ID; - if (in.getVersion().onOrAfter(REPOSITORY_ID_INTRODUCED_VERSION)) { - repositoryStateId = in.readLong(); - } + long repositoryStateId = in.readLong(); entries[i] = new Entry(snapshot, includeGlobalState, partial, @@ -471,9 +462,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeByte(shardEntry.value.state().value()); } } - if (out.getVersion().onOrAfter(REPOSITORY_ID_INTRODUCED_VERSION)) { - out.writeLong(entry.repositoryStateId); - } + out.writeLong(entry.repositoryStateId); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlock.java b/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlock.java index efbd262b16dd..fc09741f4d9c 100644 --- a/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlock.java +++ b/server/src/main/java/org/elasticsearch/cluster/block/ClusterBlock.java @@ -19,7 +19,6 @@ package org.elasticsearch.cluster.block; -import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; @@ -138,11 +137,7 @@ public void readFrom(StreamInput in) throws IOException { retryable = in.readBoolean(); disableStatePersistence = in.readBoolean(); status = RestStatus.readFrom(in); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - allowReleaseResources = in.readBoolean(); - } else { - allowReleaseResources = false; - } + allowReleaseResources = in.readBoolean(); } @Override @@ -156,9 +151,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(retryable); out.writeBoolean(disableStatePersistence); RestStatus.writeTo(out, status); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeBoolean(allowReleaseResources); - } + out.writeBoolean(allowReleaseResources); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/NodeAllocationResult.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/NodeAllocationResult.java index 153fc2cbe3e7..8b97f1357fa0 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/NodeAllocationResult.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/NodeAllocationResult.java @@ -20,7 +20,6 @@ package org.elasticsearch.cluster.routing.allocation; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.Version; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.allocation.decider.Decision; import org.elasticsearch.common.Nullable; @@ -82,11 +81,7 @@ public NodeAllocationResult(DiscoveryNode node, Decision decision, int weightRan public NodeAllocationResult(StreamInput in) throws IOException { node = new DiscoveryNode(in); shardStoreInfo = in.readOptionalWriteable(ShardStoreInfo::new); - if (in.getVersion().before(Version.V_5_2_1)) { - canAllocateDecision = Decision.readFrom(in); - } else { - canAllocateDecision = in.readOptionalWriteable(Decision::readFrom); - } + canAllocateDecision = in.readOptionalWriteable(Decision::readFrom); nodeDecision = AllocationDecision.readFrom(in); weightRanking = in.readVInt(); } @@ -95,15 +90,7 @@ public NodeAllocationResult(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { node.writeTo(out); out.writeOptionalWriteable(shardStoreInfo); - if (out.getVersion().before(Version.V_5_2_1)) { - if (canAllocateDecision == null) { - Decision.NO.writeTo(out); - } else { - canAllocateDecision.writeTo(out); - } - } else { - out.writeOptionalWriteable(canAllocateDecision); - } + out.writeOptionalWriteable(canAllocateDecision); nodeDecision.writeTo(out); out.writeVInt(weightRanking); } diff --git a/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java b/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java index 8b2db374c8da..894a886182d3 100644 --- a/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java @@ -150,13 +150,7 @@ public InnerHitBuilder(String name) { */ public InnerHitBuilder(StreamInput in) throws IOException { name = in.readOptionalString(); - if (in.getVersion().before(Version.V_5_5_0)) { - in.readOptionalString(); - in.readOptionalString(); - } - if (in.getVersion().onOrAfter(Version.V_5_2_0)) { - ignoreUnmapped = in.readBoolean(); - } + ignoreUnmapped = in.readBoolean(); from = in.readVInt(); size = in.readVInt(); explain = in.readBoolean(); @@ -191,14 +185,6 @@ public InnerHitBuilder(StreamInput in) throws IOException { } } highlightBuilder = in.readOptionalWriteable(HighlightBuilder::new); - if (in.getVersion().before(Version.V_5_5_0)) { - /** - * this is needed for BWC with nodes pre 5.5 - */ - in.readNamedWriteable(QueryBuilder.class); - boolean hasChildren = in.readBoolean(); - assert hasChildren == false; - } if (in.getVersion().onOrAfter(Version.V_6_4_0)) { this.innerCollapseBuilder = in.readOptionalWriteable(CollapseBuilder::new); } @@ -206,9 +192,6 @@ public InnerHitBuilder(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { - if (out.getVersion().before(Version.V_5_5_0)) { - throw new IOException("Invalid output version, must >= " + Version.V_5_5_0.toString()); - } out.writeOptionalString(name); out.writeBoolean(ignoreUnmapped); out.writeVInt(from); @@ -252,84 +235,6 @@ public void writeTo(StreamOutput out) throws IOException { } } - /** - * BWC serialization for nested {@link InnerHitBuilder}. - * Should only be used to send nested inner hits to nodes pre 5.5. - */ - protected void writeToNestedBWC(StreamOutput out, QueryBuilder query, String nestedPath) throws IOException { - assert out.getVersion().before(Version.V_5_5_0) : - "invalid output version, must be < " + Version.V_5_5_0.toString(); - writeToBWC(out, query, nestedPath, null); - } - - /** - * BWC serialization for collapsing {@link InnerHitBuilder}. - * Should only be used to send collapsing inner hits to nodes pre 5.5. - */ - public void writeToCollapseBWC(StreamOutput out) throws IOException { - assert out.getVersion().before(Version.V_5_5_0) : - "invalid output version, must be < " + Version.V_5_5_0.toString(); - writeToBWC(out, new MatchAllQueryBuilder(), null, null); - } - - /** - * BWC serialization for parent/child {@link InnerHitBuilder}. - * Should only be used to send hasParent or hasChild inner hits to nodes pre 5.5. - */ - public void writeToParentChildBWC(StreamOutput out, QueryBuilder query, String parentChildPath) throws IOException { - assert(out.getVersion().before(Version.V_5_5_0)) : - "invalid output version, must be < " + Version.V_5_5_0.toString(); - writeToBWC(out, query, null, parentChildPath); - } - - private void writeToBWC(StreamOutput out, - QueryBuilder query, - String nestedPath, - String parentChildPath) throws IOException { - out.writeOptionalString(name); - if (nestedPath != null) { - out.writeOptionalString(nestedPath); - out.writeOptionalString(null); - } else { - out.writeOptionalString(null); - out.writeOptionalString(parentChildPath); - } - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - out.writeBoolean(ignoreUnmapped); - } - out.writeVInt(from); - out.writeVInt(size); - out.writeBoolean(explain); - out.writeBoolean(version); - out.writeBoolean(trackScores); - out.writeOptionalWriteable(storedFieldsContext); - out.writeGenericValue(docValueFields == null - ? null - : docValueFields.stream().map(ff -> ff.field).collect(Collectors.toList())); - boolean hasScriptFields = scriptFields != null; - out.writeBoolean(hasScriptFields); - if (hasScriptFields) { - out.writeVInt(scriptFields.size()); - Iterator iterator = scriptFields.stream() - .sorted(Comparator.comparing(ScriptField::fieldName)).iterator(); - while (iterator.hasNext()) { - iterator.next().writeTo(out); - } - } - out.writeOptionalWriteable(fetchSourceContext); - boolean hasSorts = sorts != null; - out.writeBoolean(hasSorts); - if (hasSorts) { - out.writeVInt(sorts.size()); - for (SortBuilder sort : sorts) { - out.writeNamedWriteable(sort); - } - } - out.writeOptionalWriteable(highlightBuilder); - out.writeNamedWriteable(query); - out.writeBoolean(false); - } - public String getName() { return name; } diff --git a/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java index 0de474f8b990..950c9e052ada 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilder.java @@ -26,7 +26,6 @@ import org.apache.lucene.search.Query; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ExceptionsHelper; -import org.elasticsearch.Version; import org.elasticsearch.action.termvectors.MultiTermVectorsItemResponse; import org.elasticsearch.action.termvectors.MultiTermVectorsRequest; import org.elasticsearch.action.termvectors.MultiTermVectorsResponse; @@ -47,7 +46,6 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.VersionType; @@ -220,11 +218,7 @@ public Item(@Nullable String index, @Nullable String type, XContentBuilder doc) type = in.readOptionalString(); if (in.readBoolean()) { doc = (BytesReference) in.readGenericValue(); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - xContentType = in.readEnum(XContentType.class); - } else { - xContentType = XContentHelper.xContentType(doc); - } + xContentType = in.readEnum(XContentType.class); } else { id = in.readString(); } @@ -242,9 +236,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(doc != null); if (doc != null) { out.writeGenericValue(doc); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeEnum(xContentType); - } + out.writeEnum(xContentType); } else { out.writeString(id); } diff --git a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index 991628578942..8d7c0190eb21 100644 --- a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -32,7 +32,6 @@ import org.apache.lucene.search.join.BitSetProducer; import org.apache.lucene.search.join.ParentChildrenBlockJoinQuery; import org.apache.lucene.search.join.ScoreMode; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; @@ -103,15 +102,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeString(path); out.writeVInt(scoreMode.ordinal()); out.writeNamedWriteable(query); - if (out.getVersion().before(Version.V_5_5_0)) { - final boolean hasInnerHit = innerHitBuilder != null; - out.writeBoolean(hasInnerHit); - if (hasInnerHit) { - innerHitBuilder.writeToNestedBWC(out, query, path); - } - } else { - out.writeOptionalWriteable(innerHitBuilder); - } + out.writeOptionalWriteable(innerHitBuilder); out.writeBoolean(ignoreUnmapped); } diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java index 19687464edca..0289ce6f6ae4 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java @@ -175,9 +175,6 @@ public QueryStringQueryBuilder(StreamInput in) throws IOException { analyzer = in.readOptionalString(); quoteAnalyzer = in.readOptionalString(); quoteFieldSuffix = in.readOptionalString(); - if (in.getVersion().before(Version.V_6_0_0_beta1)) { - in.readBoolean(); // auto_generate_phrase_query - } allowLeadingWildcard = in.readOptionalBoolean(); analyzeWildcard = in.readOptionalBoolean(); enablePositionIncrements = in.readBoolean(); @@ -186,27 +183,15 @@ public QueryStringQueryBuilder(StreamInput in) throws IOException { fuzzyMaxExpansions = in.readVInt(); fuzzyRewrite = in.readOptionalString(); phraseSlop = in.readVInt(); - if (in.getVersion().before(Version.V_6_0_0_beta1)) { - in.readBoolean(); // use_dismax - tieBreaker = in.readFloat(); - type = DEFAULT_TYPE; - } else { - type = MultiMatchQueryBuilder.Type.readFromStream(in); - tieBreaker = in.readOptionalFloat(); - } + type = MultiMatchQueryBuilder.Type.readFromStream(in); + tieBreaker = in.readOptionalFloat(); + rewrite = in.readOptionalString(); minimumShouldMatch = in.readOptionalString(); lenient = in.readOptionalBoolean(); timeZone = in.readOptionalTimeZone(); escape = in.readBoolean(); maxDeterminizedStates = in.readVInt(); - if (in.getVersion().onOrAfter(Version.V_5_1_1) && in.getVersion().before(Version.V_6_0_0_beta1)) { - in.readBoolean(); // split_on_whitespace - Boolean useAllField = in.readOptionalBoolean(); - if (useAllField != null && useAllField) { - defaultField = "*"; - } - } if (in.getVersion().onOrAfter(Version.V_6_1_0)) { autoGenerateSynonymsPhraseQuery = in.readBoolean(); fuzzyTranspositions = in.readBoolean(); @@ -226,9 +211,6 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeOptionalString(this.analyzer); out.writeOptionalString(this.quoteAnalyzer); out.writeOptionalString(this.quoteFieldSuffix); - if (out.getVersion().before(Version.V_6_0_0_beta1)) { - out.writeBoolean(false); // auto_generate_phrase_query - } out.writeOptionalBoolean(this.allowLeadingWildcard); out.writeOptionalBoolean(this.analyzeWildcard); out.writeBoolean(this.enablePositionIncrements); @@ -237,24 +219,14 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeVInt(this.fuzzyMaxExpansions); out.writeOptionalString(this.fuzzyRewrite); out.writeVInt(this.phraseSlop); - if (out.getVersion().before(Version.V_6_0_0_beta1)) { - out.writeBoolean(true); // use_dismax - out.writeFloat(tieBreaker != null ? tieBreaker : 0.0f); - } else { - type.writeTo(out); - out.writeOptionalFloat(tieBreaker); - } + type.writeTo(out); + out.writeOptionalFloat(tieBreaker); out.writeOptionalString(this.rewrite); out.writeOptionalString(this.minimumShouldMatch); out.writeOptionalBoolean(this.lenient); out.writeOptionalTimeZone(timeZone); out.writeBoolean(this.escape); out.writeVInt(this.maxDeterminizedStates); - if (out.getVersion().onOrAfter(Version.V_5_1_1) && out.getVersion().before(Version.V_6_0_0_beta1)) { - out.writeBoolean(false); // split_on_whitespace - Boolean useAllFields = defaultField == null ? null : Regex.isMatchAllPattern(defaultField); - out.writeOptionalBoolean(useAllFields); - } if (out.getVersion().onOrAfter(Version.V_6_1_0)) { out.writeBoolean(autoGenerateSynonymsPhraseQuery); out.writeBoolean(fuzzyTranspositions); diff --git a/server/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java index 6223254874d0..b297036f2f37 100644 --- a/server/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java @@ -23,7 +23,6 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; @@ -108,14 +107,12 @@ public RangeQueryBuilder(StreamInput in) throws IOException { if (formatString != null) { format = Joda.forPattern(formatString); } - if (in.getVersion().onOrAfter(Version.V_5_2_0)) { - String relationString = in.readOptionalString(); - if (relationString != null) { - relation = ShapeRelation.getRelationByName(relationString); - if (relation != null && !isRelationAllowed(relation)) { - throw new IllegalArgumentException( - "[range] query does not support relation [" + relationString + "]"); - } + String relationString = in.readOptionalString(); + if (relationString != null) { + relation = ShapeRelation.getRelationByName(relationString); + if (relation != null && !isRelationAllowed(relation)) { + throw new IllegalArgumentException( + "[range] query does not support relation [" + relationString + "]"); } } } @@ -139,13 +136,11 @@ protected void doWriteTo(StreamOutput out) throws IOException { formatString = this.format.format(); } out.writeOptionalString(formatString); - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - String relationString = null; - if (this.relation != null) { - relationString = this.relation.getRelationName(); - } - out.writeOptionalString(relationString); + String relationString = null; + if (this.relation != null) { + relationString = this.relation.getRelationName(); } + out.writeOptionalString(relationString); } /** diff --git a/server/src/main/java/org/elasticsearch/index/query/SimpleQueryStringBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SimpleQueryStringBuilder.java index 46a958b58fe2..473aa636caab 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SimpleQueryStringBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SimpleQueryStringBuilder.java @@ -168,27 +168,11 @@ public SimpleQueryStringBuilder(StreamInput in) throws IOException { flags = in.readInt(); analyzer = in.readOptionalString(); defaultOperator = Operator.readFromStream(in); - if (in.getVersion().before(Version.V_5_1_1)) { - in.readBoolean(); // lowercase_expanded_terms - } settings.lenient(in.readBoolean()); - if (in.getVersion().onOrAfter(Version.V_5_1_1)) { - this.lenientSet = in.readBoolean(); - } + this.lenientSet = in.readBoolean(); settings.analyzeWildcard(in.readBoolean()); - if (in.getVersion().before(Version.V_5_1_1)) { - in.readString(); // locale - } minimumShouldMatch = in.readOptionalString(); - if (in.getVersion().onOrAfter(Version.V_5_1_1)) { - settings.quoteFieldSuffix(in.readOptionalString()); - if (in.getVersion().before(Version.V_6_0_0_beta2)) { - Boolean useAllFields = in.readOptionalBoolean(); - if (useAllFields != null && useAllFields) { - useAllFields(true); - } - } - } + settings.quoteFieldSuffix(in.readOptionalString()); if (in.getVersion().onOrAfter(Version.V_6_1_0)) { settings.autoGenerateSynonymsPhraseQuery(in.readBoolean()); settings.fuzzyPrefixLength(in.readVInt()); @@ -208,28 +192,11 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeInt(flags); out.writeOptionalString(analyzer); defaultOperator.writeTo(out); - if (out.getVersion().before(Version.V_5_1_1)) { - out.writeBoolean(true); // lowercase_expanded_terms - } out.writeBoolean(settings.lenient()); - if (out.getVersion().onOrAfter(Version.V_5_1_1)) { - out.writeBoolean(lenientSet); - } + out.writeBoolean(lenientSet); out.writeBoolean(settings.analyzeWildcard()); - if (out.getVersion().before(Version.V_5_1_1)) { - out.writeString(Locale.ROOT.toLanguageTag()); // locale - } out.writeOptionalString(minimumShouldMatch); - if (out.getVersion().onOrAfter(Version.V_5_1_1)) { - out.writeOptionalString(settings.quoteFieldSuffix()); - if (out.getVersion().before(Version.V_6_0_0_beta2)) { - if (useAllFields()) { - out.writeOptionalBoolean(true); - } else { - out.writeOptionalBoolean(null); - } - } - } + out.writeOptionalString(settings.quoteFieldSuffix()); if (out.getVersion().onOrAfter(Version.V_6_1_0)) { out.writeBoolean(settings.autoGenerateSynonymsPhraseQuery()); out.writeVInt(settings.fuzzyPrefixLength()); diff --git a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java index 9ff26b13212c..66e83907d499 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java @@ -20,7 +20,6 @@ package org.elasticsearch.index.reindex; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -303,11 +302,7 @@ public Status(List sliceStatuses, @Nullable String reasonCanc } public Status(StreamInput in) throws IOException { - if (in.getVersion().onOrAfter(Version.V_5_1_1)) { - sliceId = in.readOptionalVInt(); - } else { - sliceId = null; - } + sliceId = in.readOptionalVInt(); total = in.readVLong(); updated = in.readVLong(); created = in.readVLong(); @@ -321,18 +316,12 @@ public Status(StreamInput in) throws IOException { requestsPerSecond = in.readFloat(); reasonCancelled = in.readOptionalString(); throttledUntil = in.readTimeValue(); - if (in.getVersion().onOrAfter(Version.V_5_1_1)) { - sliceStatuses = in.readList(stream -> stream.readOptionalWriteable(StatusOrException::new)); - } else { - sliceStatuses = emptyList(); - } + sliceStatuses = in.readList(stream -> stream.readOptionalWriteable(StatusOrException::new)); } @Override public void writeTo(StreamOutput out) throws IOException { - if (out.getVersion().onOrAfter(Version.V_5_1_1)) { - out.writeOptionalVInt(sliceId); - } + out.writeOptionalVInt(sliceId); out.writeVLong(total); out.writeVLong(updated); out.writeVLong(created); @@ -346,11 +335,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeFloat(requestsPerSecond); out.writeOptionalString(reasonCancelled); out.writeTimeValue(throttledUntil); - if (out.getVersion().onOrAfter(Version.V_5_1_1)) { - out.writeVInt(sliceStatuses.size()); - for (StatusOrException sliceStatus : sliceStatuses) { - out.writeOptionalWriteable(sliceStatus); - } + out.writeVInt(sliceStatuses.size()); + for (StatusOrException sliceStatus : sliceStatuses) { + out.writeOptionalWriteable(sliceStatus); } } diff --git a/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java b/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java index 70f79a9def60..3ebd261b5847 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java @@ -92,13 +92,8 @@ public RemoteInfo(StreamInput in) throws IOException { headers.put(in.readString(), in.readString()); } this.headers = unmodifiableMap(headers); - if (in.getVersion().onOrAfter(Version.V_5_2_0)) { - socketTimeout = in.readTimeValue(); - connectTimeout = in.readTimeValue(); - } else { - socketTimeout = DEFAULT_SOCKET_TIMEOUT; - connectTimeout = DEFAULT_CONNECT_TIMEOUT; - } + socketTimeout = in.readTimeValue(); + connectTimeout = in.readTimeValue(); if (in.getVersion().onOrAfter(Version.V_6_4_0)) { pathPrefix = in.readOptionalString(); } else { @@ -119,10 +114,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(header.getKey()); out.writeString(header.getValue()); } - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - out.writeTimeValue(socketTimeout); - out.writeTimeValue(connectTimeout); - } + out.writeTimeValue(socketTimeout); + out.writeTimeValue(connectTimeout); if (out.getVersion().onOrAfter(Version.V_6_4_0)) { out.writeOptionalString(pathPrefix); } diff --git a/server/src/main/java/org/elasticsearch/indices/flush/SyncedFlushService.java b/server/src/main/java/org/elasticsearch/indices/flush/SyncedFlushService.java index f01b4bb31217..fb7885a217e0 100644 --- a/server/src/main/java/org/elasticsearch/indices/flush/SyncedFlushService.java +++ b/server/src/main/java/org/elasticsearch/indices/flush/SyncedFlushService.java @@ -560,9 +560,6 @@ static final class PreSyncedFlushResponse extends TransportResponse { } boolean includeNumDocs(Version version) { - if (version.major == Version.V_5_6_8.major) { - return version.onOrAfter(Version.V_5_6_8); - } return version.onOrAfter(Version.V_6_2_2); } diff --git a/server/src/main/java/org/elasticsearch/ingest/PipelineConfiguration.java b/server/src/main/java/org/elasticsearch/ingest/PipelineConfiguration.java index a2aa8e385e3f..6778f3d1eaa6 100644 --- a/server/src/main/java/org/elasticsearch/ingest/PipelineConfiguration.java +++ b/server/src/main/java/org/elasticsearch/ingest/PipelineConfiguration.java @@ -19,7 +19,6 @@ package org.elasticsearch.ingest; -import org.elasticsearch.Version; import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.Diff; import org.elasticsearch.common.ParseField; @@ -117,13 +116,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } public static PipelineConfiguration readFrom(StreamInput in) throws IOException { - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - return new PipelineConfiguration(in.readString(), in.readBytesReference(), in.readEnum(XContentType.class)); - } else { - final String id = in.readString(); - final BytesReference config = in.readBytesReference(); - return new PipelineConfiguration(id, config, XContentHelper.xContentType(config)); - } + return new PipelineConfiguration(in.readString(), in.readBytesReference(), in.readEnum(XContentType.class)); } public static Diff readDiffFrom(StreamInput in) throws IOException { @@ -134,9 +127,7 @@ public static Diff readDiffFrom(StreamInput in) throws IO public void writeTo(StreamOutput out) throws IOException { out.writeString(id); out.writeBytesReference(config); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeEnum(xContentType); - } + out.writeEnum(xContentType); } @Override diff --git a/server/src/main/java/org/elasticsearch/monitor/os/OsStats.java b/server/src/main/java/org/elasticsearch/monitor/os/OsStats.java index 637f4cf1cbe0..3bdfe95f1e2c 100644 --- a/server/src/main/java/org/elasticsearch/monitor/os/OsStats.java +++ b/server/src/main/java/org/elasticsearch/monitor/os/OsStats.java @@ -52,11 +52,7 @@ public OsStats(StreamInput in) throws IOException { this.cpu = new Cpu(in); this.mem = new Mem(in); this.swap = new Swap(in); - if (in.getVersion().onOrAfter(Version.V_5_1_1)) { - this.cgroup = in.readOptionalWriteable(Cgroup::new); - } else { - this.cgroup = null; - } + this.cgroup = in.readOptionalWriteable(Cgroup::new); } @Override @@ -65,9 +61,7 @@ public void writeTo(StreamOutput out) throws IOException { cpu.writeTo(out); mem.writeTo(out); swap.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_5_1_1)) { - out.writeOptionalWriteable(cgroup); - } + out.writeOptionalWriteable(cgroup); } public long getTimestamp() { diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetaData.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetaData.java index f81b7c770e56..b7a179e41e38 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetaData.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksCustomMetaData.java @@ -188,7 +188,7 @@ public long getNumberOfTasksOnNode(String nodeId, String taskName) { @Override public Version getMinimalSupportedVersion() { - return Version.V_5_4_0; + return Version.CURRENT.minimumCompatibilityVersion(); } @Override diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginInfo.java b/server/src/main/java/org/elasticsearch/plugins/PluginInfo.java index 74a911b0ae4f..d211efef5173 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginInfo.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginInfo.java @@ -107,11 +107,7 @@ public PluginInfo(final StreamInput in) throws IOException { } else { extendedPlugins = Collections.emptyList(); } - if (in.getVersion().onOrAfter(Version.V_5_4_0)) { - hasNativeController = in.readBoolean(); - } else { - hasNativeController = false; - } + hasNativeController = in.readBoolean(); if (in.getVersion().onOrAfter(Version.V_6_0_0_beta2) && in.getVersion().before(Version.V_6_3_0)) { /* * Elasticsearch versions in [6.0.0-beta2, 6.3.0) allowed plugins to specify that they require the keystore and this was @@ -134,9 +130,7 @@ public void writeTo(final StreamOutput out) throws IOException { if (out.getVersion().onOrAfter(Version.V_6_2_0)) { out.writeStringList(extendedPlugins); } - if (out.getVersion().onOrAfter(Version.V_5_4_0)) { - out.writeBoolean(hasNativeController); - } + out.writeBoolean(hasNativeController); if (out.getVersion().onOrAfter(Version.V_6_0_0_beta2) && out.getVersion().before(Version.V_6_3_0)) { /* * Elasticsearch versions in [6.0.0-beta2, 6.3.0) allowed plugins to specify that they require the keystore and this was 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 cc1d27425e13..a4d6518e9af9 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -39,7 +39,6 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.Version; -import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.metadata.RepositoryMetaData; @@ -719,7 +718,7 @@ public boolean isReadOnly() { protected void writeIndexGen(final RepositoryData repositoryData, final long repositoryStateId) throws IOException { assert isReadOnly() == false; // can not write to a read only repository final long currentGen = latestIndexBlobId(); - if (repositoryStateId != SnapshotsInProgress.UNDEFINED_REPOSITORY_STATE_ID && currentGen != repositoryStateId) { + if (currentGen != repositoryStateId) { // the index file was updated by a concurrent operation, so we were operating on stale // repository data throw new RepositoryException(metadata.name(), "concurrent modification of the index-N file, expected current generation [" + diff --git a/server/src/main/java/org/elasticsearch/script/Script.java b/server/src/main/java/org/elasticsearch/script/Script.java index a64a3ecd3764..67ea4f24b83f 100644 --- a/server/src/main/java/org/elasticsearch/script/Script.java +++ b/server/src/main/java/org/elasticsearch/script/Script.java @@ -19,7 +19,6 @@ package org.elasticsearch.script; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -451,133 +450,24 @@ public Script(ScriptType type, String lang, String idOrCode, Map * Creates a {@link Script} read from an input stream. */ public Script(StreamInput in) throws IOException { - // Version 5.3 allows lang to be an optional parameter for stored scripts and expects - // options to be null for stored and file scripts. - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - this.type = ScriptType.readFrom(in); - this.lang = in.readOptionalString(); - this.idOrCode = in.readString(); - @SuppressWarnings("unchecked") - Map options = (Map)(Map)in.readMap(); - this.options = options; - this.params = in.readMap(); - // Version 5.1 to 5.3 (exclusive) requires all Script members to be non-null and supports the potential - // for more options than just XContentType. Reorders the read in contents to be in - // same order as the constructor. - } else if (in.getVersion().onOrAfter(Version.V_5_1_1)) { - this.type = ScriptType.readFrom(in); - String lang = in.readString(); - this.lang = this.type == ScriptType.STORED ? null : lang; - - this.idOrCode = in.readString(); - @SuppressWarnings("unchecked") - Map options = (Map)(Map)in.readMap(); - - if (this.type != ScriptType.INLINE && options.isEmpty()) { - this.options = null; - } else { - this.options = options; - } - - this.params = in.readMap(); - // Prior to version 5.1 the script members are read in certain cases as optional and given - // default values when necessary. Also the only option supported is for XContentType. - } else { - this.idOrCode = in.readString(); - - if (in.readBoolean()) { - this.type = ScriptType.readFrom(in); - } else { - this.type = DEFAULT_SCRIPT_TYPE; - } - - String lang = in.readOptionalString(); - - if (lang == null) { - this.lang = this.type == ScriptType.STORED ? null : DEFAULT_SCRIPT_LANG; - } else { - this.lang = lang; - } - - Map params = in.readMap(); - - if (params == null) { - this.params = new HashMap<>(); - } else { - this.params = params; - } - - if (in.readBoolean()) { - this.options = new HashMap<>(); - XContentType contentType = in.readEnum(XContentType.class); - this.options.put(CONTENT_TYPE_OPTION, contentType.mediaType()); - } else if (type == ScriptType.INLINE) { - options = new HashMap<>(); - } else { - this.options = null; - } - } + this.type = ScriptType.readFrom(in); + this.lang = in.readOptionalString(); + this.idOrCode = in.readString(); + @SuppressWarnings("unchecked") + Map options = (Map)(Map)in.readMap(); + this.options = options; + this.params = in.readMap(); } @Override public void writeTo(StreamOutput out) throws IOException { - // Version 5.3+ allows lang to be an optional parameter for stored scripts and expects - // options to be null for stored and file scripts. - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - type.writeTo(out); - out.writeOptionalString(lang); - out.writeString(idOrCode); - @SuppressWarnings("unchecked") - Map options = (Map)(Map)this.options; - out.writeMap(options); - out.writeMap(params); - // Version 5.1 to 5.3 (exclusive) requires all Script members to be non-null and supports the potential - // for more options than just XContentType. Reorders the written out contents to be in - // same order as the constructor. - } else if (out.getVersion().onOrAfter(Version.V_5_1_1)) { - type.writeTo(out); - - if (lang == null) { - out.writeString(""); - } else { - out.writeString(lang); - } - - out.writeString(idOrCode); - @SuppressWarnings("unchecked") - Map options = (Map)(Map)this.options; - - if (options == null) { - out.writeMap(new HashMap<>()); - } else { - out.writeMap(options); - } - - out.writeMap(params); - // Prior to version 5.1 the Script members were possibly written as optional or null, though there is no case where a null - // value wasn't equivalent to it's default value when actually compiling/executing a script. Meaning, there are no - // backwards compatibility issues, and now there's enforced consistency. Also the only supported compiler - // option was XContentType. - } else { - out.writeString(idOrCode); - out.writeBoolean(true); - type.writeTo(out); - out.writeOptionalString(lang); - - if (params.isEmpty()) { - out.writeMap(null); - } else { - out.writeMap(params); - } - - if (options != null && options.containsKey(CONTENT_TYPE_OPTION)) { - XContentType contentType = XContentType.fromMediaTypeOrFormat(options.get(CONTENT_TYPE_OPTION)); - out.writeBoolean(true); - out.writeEnum(contentType); - } else { - out.writeBoolean(false); - } - } + type.writeTo(out); + out.writeOptionalString(lang); + out.writeString(idOrCode); + @SuppressWarnings("unchecked") + Map options = (Map) (Map) this.options; + out.writeMap(options); + out.writeMap(params); } /** diff --git a/server/src/main/java/org/elasticsearch/script/ScriptMetaData.java b/server/src/main/java/org/elasticsearch/script/ScriptMetaData.java index 59d824eb313e..35a7c2e60d68 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptMetaData.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptMetaData.java @@ -292,25 +292,7 @@ public ScriptMetaData(StreamInput in) throws IOException { for (int i = 0; i < size; i++) { String id = in.readString(); - - // Prior to version 5.3 all scripts were stored using the deprecated namespace. - // Split the id to find the language then use StoredScriptSource to parse the - // expected BytesReference after which a new StoredScriptSource is created - // with the appropriate language and options. - if (in.getVersion().before(Version.V_5_3_0)) { - int split = id.indexOf('#'); - - if (split == -1) { - throw new IllegalArgumentException("illegal stored script id [" + id + "], does not contain lang"); - } else { - source = new StoredScriptSource(in); - source = new StoredScriptSource(id.substring(0, split), source.getSource(), Collections.emptyMap()); - } - // Version 5.3+ can just be parsed normally using StoredScriptSource. - } else { - source = new StoredScriptSource(in); - } - + source = new StoredScriptSource(in); scripts.put(id, source); } @@ -319,34 +301,11 @@ public ScriptMetaData(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { - // Version 5.3+ will output the contents of the scripts' Map using - // StoredScriptSource to stored the language, code, and options. - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeVInt(scripts.size()); - - for (Map.Entry entry : scripts.entrySet()) { - out.writeString(entry.getKey()); - entry.getValue().writeTo(out); - } - // Prior to Version 5.3, stored scripts can only be read using the deprecated - // namespace. Scripts using the deprecated namespace are first isolated in a - // temporary Map, then written out. Since all scripts will be stored using the - // deprecated namespace, no scripts will be lost. - } else { - Map filtered = new HashMap<>(); - - for (Map.Entry entry : scripts.entrySet()) { - if (entry.getKey().contains("#")) { - filtered.put(entry.getKey(), entry.getValue()); - } - } - - out.writeVInt(filtered.size()); + out.writeVInt(scripts.size()); - for (Map.Entry entry : filtered.entrySet()) { - out.writeString(entry.getKey()); - entry.getValue().writeTo(out); - } + for (Map.Entry entry : scripts.entrySet()) { + out.writeString(entry.getKey()); + entry.getValue().writeTo(out); } } diff --git a/server/src/main/java/org/elasticsearch/search/SearchShardTarget.java b/server/src/main/java/org/elasticsearch/search/SearchShardTarget.java index 19c0f8c64d58..4a46c7202d14 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchShardTarget.java +++ b/server/src/main/java/org/elasticsearch/search/SearchShardTarget.java @@ -19,7 +19,6 @@ package org.elasticsearch.search; -import org.elasticsearch.Version; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.io.stream.StreamInput; @@ -52,11 +51,7 @@ public SearchShardTarget(StreamInput in) throws IOException { } shardId = ShardId.readShardId(in); this.originalIndices = null; - if (in.getVersion().onOrAfter(Version.V_5_6_0)) { - clusterAlias = in.readOptionalString(); - } else { - clusterAlias = null; - } + clusterAlias = in.readOptionalString(); } public SearchShardTarget(String nodeId, ShardId shardId, String clusterAlias, OriginalIndices originalIndices) { @@ -121,9 +116,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeText(nodeId); } shardId.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_5_6_0)) { - out.writeOptionalString(clusterAlias); - } + out.writeOptionalString(clusterAlias); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/IncludeExclude.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/IncludeExclude.java index 9e3012c5eb9d..8154108f9f0b 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/IncludeExclude.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/IncludeExclude.java @@ -36,7 +36,6 @@ import org.apache.lucene.util.automaton.Operations; import org.apache.lucene.util.automaton.RegExp; import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -424,13 +423,8 @@ public IncludeExclude(StreamInput in) throws IOException { } else { excludeValues = null; } - if (in.getVersion().onOrAfter(Version.V_5_2_0)) { - incNumPartitions = in.readVInt(); - incZeroBasedPartition = in.readVInt(); - } else { - incNumPartitions = 0; - incZeroBasedPartition = 0; - } + incNumPartitions = in.readVInt(); + incZeroBasedPartition = in.readVInt(); } @Override @@ -457,10 +451,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBytesRef(value); } } - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - out.writeVInt(incNumPartitions); - out.writeVInt(incZeroBasedPartition); - } + out.writeVInt(incNumPartitions); + out.writeVInt(incZeroBasedPartition); } } diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index c42a1a12a187..c7564dc5ea83 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -248,9 +248,7 @@ public SearchSourceBuilder(StreamInput in) throws IOException { profile = in.readBoolean(); searchAfterBuilder = in.readOptionalWriteable(SearchAfterBuilder::new); sliceBuilder = in.readOptionalWriteable(SliceBuilder::new); - if (in.getVersion().onOrAfter(Version.V_5_3_0)) { - collapse = in.readOptionalWriteable(CollapseBuilder::new); - } + collapse = in.readOptionalWriteable(CollapseBuilder::new); if (in.getVersion().onOrAfter(Version.V_6_0_0_beta1)) { trackTotalHits = in.readBoolean(); } else { @@ -313,9 +311,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(profile); out.writeOptionalWriteable(searchAfterBuilder); out.writeOptionalWriteable(sliceBuilder); - if (out.getVersion().onOrAfter(Version.V_5_3_0)) { - out.writeOptionalWriteable(collapse); - } + out.writeOptionalWriteable(collapse); if (out.getVersion().onOrAfter(Version.V_6_0_0_beta1)) { out.writeBoolean(trackTotalHits); } diff --git a/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java b/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java index ccab5e2cb93b..2ebf413b1405 100644 --- a/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/collapse/CollapseBuilder.java @@ -19,7 +19,6 @@ package org.elasticsearch.search.collapse; import org.apache.lucene.index.IndexOptions; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; @@ -94,31 +93,14 @@ public CollapseBuilder(String field) { public CollapseBuilder(StreamInput in) throws IOException { this.field = in.readString(); this.maxConcurrentGroupRequests = in.readVInt(); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - this.innerHits = in.readList(InnerHitBuilder::new); - } else { - InnerHitBuilder innerHitBuilder = in.readOptionalWriteable(InnerHitBuilder::new); - if (innerHitBuilder != null) { - this.innerHits = Collections.singletonList(innerHitBuilder); - } else { - this.innerHits = Collections.emptyList(); - } - } + this.innerHits = in.readList(InnerHitBuilder::new); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(field); out.writeVInt(maxConcurrentGroupRequests); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeList(innerHits); - } else { - boolean hasInnerHit = innerHits.isEmpty() == false; - out.writeBoolean(hasInnerHit); - if (hasInnerHit) { - innerHits.get(0).writeToCollapseBWC(out); - } - } + out.writeList(innerHits); } public static CollapseBuilder fromXContent(XContentParser parser) { diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/AbstractHighlighterBuilder.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/AbstractHighlighterBuilder.java index 7888f6cd5a09..161ca9279f09 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/AbstractHighlighterBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/highlight/AbstractHighlighterBuilder.java @@ -21,7 +21,6 @@ import org.apache.lucene.search.highlight.SimpleFragmenter; import org.apache.lucene.search.highlight.SimpleSpanFragmenter; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; @@ -152,17 +151,13 @@ protected AbstractHighlighterBuilder(StreamInput in) throws IOException { order(in.readOptionalWriteable(Order::readFromStream)); highlightFilter(in.readOptionalBoolean()); forceSource(in.readOptionalBoolean()); - if (in.getVersion().onOrAfter(Version.V_5_4_0)) { - boundaryScannerType(in.readOptionalWriteable(BoundaryScannerType::readFromStream)); - } + boundaryScannerType(in.readOptionalWriteable(BoundaryScannerType::readFromStream)); boundaryMaxScan(in.readOptionalVInt()); if (in.readBoolean()) { boundaryChars(in.readString().toCharArray()); } - if (in.getVersion().onOrAfter(Version.V_5_4_0)) { - if (in.readBoolean()) { - boundaryScannerLocale(in.readString()); - } + if (in.readBoolean()) { + boundaryScannerLocale(in.readString()); } noMatchSize(in.readOptionalVInt()); phraseLimit(in.readOptionalVInt()); @@ -191,21 +186,17 @@ public final void writeTo(StreamOutput out) throws IOException { out.writeOptionalWriteable(order); out.writeOptionalBoolean(highlightFilter); out.writeOptionalBoolean(forceSource); - if (out.getVersion().onOrAfter(Version.V_5_4_0)) { - out.writeOptionalWriteable(boundaryScannerType); - } + out.writeOptionalWriteable(boundaryScannerType); out.writeOptionalVInt(boundaryMaxScan); boolean hasBounaryChars = boundaryChars != null; out.writeBoolean(hasBounaryChars); if (hasBounaryChars) { out.writeString(String.valueOf(boundaryChars)); } - if (out.getVersion().onOrAfter(Version.V_5_4_0)) { - boolean hasBoundaryScannerLocale = boundaryScannerLocale != null; - out.writeBoolean(hasBoundaryScannerLocale); - if (hasBoundaryScannerLocale) { - out.writeString(boundaryScannerLocale.toLanguageTag()); - } + boolean hasBoundaryScannerLocale = boundaryScannerLocale != null; + out.writeBoolean(hasBoundaryScannerLocale); + if (hasBoundaryScannerLocale) { + out.writeString(boundaryScannerLocale.toLanguageTag()); } out.writeOptionalVInt(noMatchSize); out.writeOptionalVInt(phraseLimit); diff --git a/server/src/main/java/org/elasticsearch/search/internal/ShardSearchLocalRequest.java b/server/src/main/java/org/elasticsearch/search/internal/ShardSearchLocalRequest.java index cf656ed3b9cb..72a12b805eb1 100644 --- a/server/src/main/java/org/elasticsearch/search/internal/ShardSearchLocalRequest.java +++ b/server/src/main/java/org/elasticsearch/search/internal/ShardSearchLocalRequest.java @@ -35,7 +35,6 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import java.io.IOException; -import java.util.Optional; /** * Shard level search request that gets created and consumed on the local node. @@ -213,25 +212,10 @@ protected void innerReadFrom(StreamInput in) throws IOException { source = in.readOptionalWriteable(SearchSourceBuilder::new); types = in.readStringArray(); aliasFilter = new AliasFilter(in); - if (in.getVersion().onOrAfter(Version.V_5_2_0)) { - indexBoost = in.readFloat(); - } else { - // Nodes < 5.2.0 doesn't send index boost. Read it from source. - if (source != null) { - Optional boost = source.indexBoosts() - .stream() - .filter(ib -> ib.getIndex().equals(shardId.getIndexName())) - .findFirst(); - indexBoost = boost.isPresent() ? boost.get().getBoost() : 1.0f; - } else { - indexBoost = 1.0f; - } - } + indexBoost = in.readFloat(); nowInMillis = in.readVLong(); requestCache = in.readOptionalBoolean(); - if (in.getVersion().onOrAfter(Version.V_5_6_0)) { - clusterAlias = in.readOptionalString(); - } + clusterAlias = in.readOptionalString(); if (in.getVersion().onOrAfter(Version.V_6_3_0)) { allowPartialSearchResults = in.readOptionalBoolean(); } @@ -254,16 +238,12 @@ protected void innerWriteTo(StreamOutput out, boolean asKey) throws IOException out.writeOptionalWriteable(source); out.writeStringArray(types); aliasFilter.writeTo(out); - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - out.writeFloat(indexBoost); - } + out.writeFloat(indexBoost); if (asKey == false) { out.writeVLong(nowInMillis); } out.writeOptionalBoolean(requestCache); - if (out.getVersion().onOrAfter(Version.V_5_6_0)) { - out.writeOptionalString(clusterAlias); - } + out.writeOptionalString(clusterAlias); if (out.getVersion().onOrAfter(Version.V_6_3_0)) { out.writeOptionalBoolean(allowPartialSearchResults); } diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java index 67ddabc37fa3..fdbe74d8d4dd 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java @@ -76,9 +76,7 @@ public final class SnapshotInfo implements Comparable, ToXContent, private static final String SUCCESSFUL_SHARDS = "successful_shards"; private static final String INCLUDE_GLOBAL_STATE = "include_global_state"; - private static final Version VERSION_INCOMPATIBLE_INTRODUCED = Version.V_5_2_0; private static final Version INCLUDE_GLOBAL_STATE_INTRODUCED = Version.V_6_2_0; - public static final Version VERBOSE_INTRODUCED = Version.V_5_5_0; private static final Comparator COMPARATOR = Comparator.comparing(SnapshotInfo::startTime).thenComparing(SnapshotInfo::snapshotId); @@ -275,11 +273,7 @@ public SnapshotInfo(final StreamInput in) throws IOException { indicesListBuilder.add(in.readString()); } indices = Collections.unmodifiableList(indicesListBuilder); - if (in.getVersion().onOrAfter(VERBOSE_INTRODUCED)) { - state = in.readBoolean() ? SnapshotState.fromValue(in.readByte()) : null; - } else { - state = SnapshotState.fromValue(in.readByte()); - } + state = in.readBoolean() ? SnapshotState.fromValue(in.readByte()) : null; reason = in.readOptionalString(); startTime = in.readVLong(); endTime = in.readVLong(); @@ -295,11 +289,7 @@ public SnapshotInfo(final StreamInput in) throws IOException { } else { shardFailures = Collections.emptyList(); } - if (in.getVersion().before(VERSION_INCOMPATIBLE_INTRODUCED)) { - version = Version.readVersion(in); - } else { - version = in.readBoolean() ? Version.readVersion(in) : null; - } + version = in.readBoolean() ? Version.readVersion(in) : null; if (in.getVersion().onOrAfter(INCLUDE_GLOBAL_STATE_INTRODUCED)) { includeGlobalState = in.readOptionalBoolean(); } @@ -681,19 +671,11 @@ public void writeTo(final StreamOutput out) throws IOException { for (String index : indices) { out.writeString(index); } - if (out.getVersion().onOrAfter(VERBOSE_INTRODUCED)) { - if (state != null) { - out.writeBoolean(true); - out.writeByte(state.value()); - } else { - out.writeBoolean(false); - } + if (state != null) { + out.writeBoolean(true); + out.writeByte(state.value()); } else { - if (out.getVersion().before(VERSION_INCOMPATIBLE_INTRODUCED) && state == SnapshotState.INCOMPATIBLE) { - out.writeByte(SnapshotState.FAILED.value()); - } else { - out.writeByte(state.value()); - } + out.writeBoolean(false); } out.writeOptionalString(reason); out.writeVLong(startTime); @@ -704,19 +686,11 @@ public void writeTo(final StreamOutput out) throws IOException { for (SnapshotShardFailure failure : shardFailures) { failure.writeTo(out); } - if (out.getVersion().before(VERSION_INCOMPATIBLE_INTRODUCED)) { - Version versionToWrite = version; - if (versionToWrite == null) { - versionToWrite = Version.CURRENT; - } - Version.writeVersion(versionToWrite, out); + if (version != null) { + out.writeBoolean(true); + Version.writeVersion(version, out); } else { - if (version != null) { - out.writeBoolean(true); - Version.writeVersion(version, out); - } else { - out.writeBoolean(false); - } + out.writeBoolean(false); } if (out.getVersion().onOrAfter(INCLUDE_GLOBAL_STATE_INTRODUCED)) { out.writeOptionalBoolean(includeGlobalState); diff --git a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java index 1f62eb706a84..5c8c25cbfddf 100644 --- a/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java +++ b/server/src/test/java/org/elasticsearch/ExceptionSerializationTests.java @@ -41,8 +41,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.breaker.CircuitBreakingException; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -104,7 +102,6 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; -import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -116,7 +113,6 @@ import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.instanceOf; public class ExceptionSerializationTests extends ESTestCase { @@ -872,89 +868,12 @@ public void testElasticsearchRemoteException() throws IOException { public void testShardLockObtainFailedException() throws IOException { ShardId shardId = new ShardId("foo", "_na_", 1); ShardLockObtainFailedException orig = new ShardLockObtainFailedException(shardId, "boom"); - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.CURRENT); - if (version.before(Version.V_5_0_2)) { - version = Version.V_5_0_2; - } + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); ShardLockObtainFailedException ex = serialize(orig, version); assertEquals(orig.getMessage(), ex.getMessage()); assertEquals(orig.getShardId(), ex.getShardId()); } - public void testBWCShardLockObtainFailedException() throws IOException { - ShardId shardId = new ShardId("foo", "_na_", 1); - ShardLockObtainFailedException orig = new ShardLockObtainFailedException(shardId, "boom"); - Exception ex = serialize((Exception)orig, randomFrom(Version.V_5_0_0, Version.V_5_0_1)); - assertThat(ex, instanceOf(NotSerializableExceptionWrapper.class)); - assertEquals("shard_lock_obtain_failed_exception: [foo][1]: boom", ex.getMessage()); - } - - public void testBWCHeadersAndMetadata() throws IOException { - //this is a request serialized with headers only, no metadata as they were added in 5.3.0 - BytesReference decoded = new BytesArray(Base64.getDecoder().decode - ("AQ10ZXN0ICBtZXNzYWdlACYtb3JnLmVsYXN0aWNzZWFyY2guRXhjZXB0aW9uU2VyaWFsaXphdGlvblRlc3RzASBFeGNlcHRpb25TZXJpYWxpemF0aW9uVG" + - "VzdHMuamF2YQR0ZXN03wYkc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsAR1OYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuamF2Y" + - "QdpbnZva2Uw/v///w8kc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsAR1OYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuamF2YQZp" + - "bnZva2U+KHN1bi5yZWZsZWN0LkRlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwBIURlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuamF2YQZ" + - "pbnZva2UrGGphdmEubGFuZy5yZWZsZWN0Lk1ldGhvZAELTWV0aG9kLmphdmEGaW52b2tl8QMzY29tLmNhcnJvdHNlYXJjaC5yYW5kb21pemVkdG" + - "VzdGluZy5SYW5kb21pemVkUnVubmVyARVSYW5kb21pemVkUnVubmVyLmphdmEGaW52b2tlsQ01Y29tLmNhcnJvdHNlYXJjaC5yYW5kb21pemVkd" + - "GVzdGluZy5SYW5kb21pemVkUnVubmVyJDgBFVJhbmRvbWl6ZWRSdW5uZXIuamF2YQhldmFsdWF0ZYsHNWNvbS5jYXJyb3RzZWFyY2gucmFuZG9t" + - "aXplZHRlc3RpbmcuUmFuZG9taXplZFJ1bm5lciQ5ARVSYW5kb21pemVkUnVubmVyLmphdmEIZXZhbHVhdGWvBzZjb20uY2Fycm90c2VhcmNoLnJ" + - "hbmRvbWl6ZWR0ZXN0aW5nLlJhbmRvbWl6ZWRSdW5uZXIkMTABFVJhbmRvbWl6ZWRSdW5uZXIuamF2YQhldmFsdWF0Zb0HOWNvbS5jYXJyb3RzZW" + - "FyY2gucmFuZG9taXplZHRlc3RpbmcucnVsZXMuU3RhdGVtZW50QWRhcHRlcgEVU3RhdGVtZW50QWRhcHRlci5qYXZhCGV2YWx1YXRlJDVvcmcuY" + - "XBhY2hlLmx1Y2VuZS51dGlsLlRlc3RSdWxlU2V0dXBUZWFyZG93bkNoYWluZWQkMQEhVGVzdFJ1bGVTZXR1cFRlYXJkb3duQ2hhaW5lZC5qYXZh" + - "CGV2YWx1YXRlMTBvcmcuYXBhY2hlLmx1Y2VuZS51dGlsLkFic3RyYWN0QmVmb3JlQWZ0ZXJSdWxlJDEBHEFic3RyYWN0QmVmb3JlQWZ0ZXJSdWx" + - "lLmphdmEIZXZhbHVhdGUtMm9yZy5hcGFjaGUubHVjZW5lLnV0aWwuVGVzdFJ1bGVUaHJlYWRBbmRUZXN0TmFtZSQxAR5UZXN0UnVsZVRocmVhZE" + - "FuZFRlc3ROYW1lLmphdmEIZXZhbHVhdGUwN29yZy5hcGFjaGUubHVjZW5lLnV0aWwuVGVzdFJ1bGVJZ25vcmVBZnRlck1heEZhaWx1cmVzJDEBI" + - "1Rlc3RSdWxlSWdub3JlQWZ0ZXJNYXhGYWlsdXJlcy5qYXZhCGV2YWx1YXRlQCxvcmcuYXBhY2hlLmx1Y2VuZS51dGlsLlRlc3RSdWxlTWFya0Zh" + - "aWx1cmUkMQEYVGVzdFJ1bGVNYXJrRmFpbHVyZS5qYXZhCGV2YWx1YXRlLzljb20uY2Fycm90c2VhcmNoLnJhbmRvbWl6ZWR0ZXN0aW5nLnJ1bGV" + - "zLlN0YXRlbWVudEFkYXB0ZXIBFVN0YXRlbWVudEFkYXB0ZXIuamF2YQhldmFsdWF0ZSREY29tLmNhcnJvdHNlYXJjaC5yYW5kb21pemVkdGVzdG" + - "luZy5UaHJlYWRMZWFrQ29udHJvbCRTdGF0ZW1lbnRSdW5uZXIBFlRocmVhZExlYWtDb250cm9sLmphdmEDcnVu7wI0Y29tLmNhcnJvdHNlYXJja" + - "C5yYW5kb21pemVkdGVzdGluZy5UaHJlYWRMZWFrQ29udHJvbAEWVGhyZWFkTGVha0NvbnRyb2wuamF2YRJmb3JrVGltZW91dGluZ1Rhc2urBjZj" + - "b20uY2Fycm90c2VhcmNoLnJhbmRvbWl6ZWR0ZXN0aW5nLlRocmVhZExlYWtDb250cm9sJDMBFlRocmVhZExlYWtDb250cm9sLmphdmEIZXZhbHV" + - "hdGXOAzNjb20uY2Fycm90c2VhcmNoLnJhbmRvbWl6ZWR0ZXN0aW5nLlJhbmRvbWl6ZWRSdW5uZXIBFVJhbmRvbWl6ZWRSdW5uZXIuamF2YQ1ydW" + - "5TaW5nbGVUZXN0lAc1Y29tLmNhcnJvdHNlYXJjaC5yYW5kb21pemVkdGVzdGluZy5SYW5kb21pemVkUnVubmVyJDUBFVJhbmRvbWl6ZWRSdW5uZ" + - "XIuamF2YQhldmFsdWF0ZaIGNWNvbS5jYXJyb3RzZWFyY2gucmFuZG9taXplZHRlc3RpbmcuUmFuZG9taXplZFJ1bm5lciQ2ARVSYW5kb21pemVk" + - "UnVubmVyLmphdmEIZXZhbHVhdGXUBjVjb20uY2Fycm90c2VhcmNoLnJhbmRvbWl6ZWR0ZXN0aW5nLlJhbmRvbWl6ZWRSdW5uZXIkNwEVUmFuZG9" + - "taXplZFJ1bm5lci5qYXZhCGV2YWx1YXRl3wYwb3JnLmFwYWNoZS5sdWNlbmUudXRpbC5BYnN0cmFjdEJlZm9yZUFmdGVyUnVsZSQxARxBYnN0cm" + - "FjdEJlZm9yZUFmdGVyUnVsZS5qYXZhCGV2YWx1YXRlLTljb20uY2Fycm90c2VhcmNoLnJhbmRvbWl6ZWR0ZXN0aW5nLnJ1bGVzLlN0YXRlbWVud" + - "EFkYXB0ZXIBFVN0YXRlbWVudEFkYXB0ZXIuamF2YQhldmFsdWF0ZSQvb3JnLmFwYWNoZS5sdWNlbmUudXRpbC5UZXN0UnVsZVN0b3JlQ2xhc3NO" + - "YW1lJDEBG1Rlc3RSdWxlU3RvcmVDbGFzc05hbWUuamF2YQhldmFsdWF0ZSlOY29tLmNhcnJvdHNlYXJjaC5yYW5kb21pemVkdGVzdGluZy5ydWx" + - "lcy5Ob1NoYWRvd2luZ09yT3ZlcnJpZGVzT25NZXRob2RzUnVsZSQxAShOb1NoYWRvd2luZ09yT3ZlcnJpZGVzT25NZXRob2RzUnVsZS5qYXZhCG" + - "V2YWx1YXRlKE5jb20uY2Fycm90c2VhcmNoLnJhbmRvbWl6ZWR0ZXN0aW5nLnJ1bGVzLk5vU2hhZG93aW5nT3JPdmVycmlkZXNPbk1ldGhvZHNSd" + - "WxlJDEBKE5vU2hhZG93aW5nT3JPdmVycmlkZXNPbk1ldGhvZHNSdWxlLmphdmEIZXZhbHVhdGUoOWNvbS5jYXJyb3RzZWFyY2gucmFuZG9taXpl" + - "ZHRlc3RpbmcucnVsZXMuU3RhdGVtZW50QWRhcHRlcgEVU3RhdGVtZW50QWRhcHRlci5qYXZhCGV2YWx1YXRlJDljb20uY2Fycm90c2VhcmNoLnJ" + - "hbmRvbWl6ZWR0ZXN0aW5nLnJ1bGVzLlN0YXRlbWVudEFkYXB0ZXIBFVN0YXRlbWVudEFkYXB0ZXIuamF2YQhldmFsdWF0ZSQ5Y29tLmNhcnJvdH" + - "NlYXJjaC5yYW5kb21pemVkdGVzdGluZy5ydWxlcy5TdGF0ZW1lbnRBZGFwdGVyARVTdGF0ZW1lbnRBZGFwdGVyLmphdmEIZXZhbHVhdGUkM29yZ" + - "y5hcGFjaGUubHVjZW5lLnV0aWwuVGVzdFJ1bGVBc3NlcnRpb25zUmVxdWlyZWQkMQEfVGVzdFJ1bGVBc3NlcnRpb25zUmVxdWlyZWQuamF2YQhl" + - "dmFsdWF0ZTUsb3JnLmFwYWNoZS5sdWNlbmUudXRpbC5UZXN0UnVsZU1hcmtGYWlsdXJlJDEBGFRlc3RSdWxlTWFya0ZhaWx1cmUuamF2YQhldmF" + - "sdWF0ZS83b3JnLmFwYWNoZS5sdWNlbmUudXRpbC5UZXN0UnVsZUlnbm9yZUFmdGVyTWF4RmFpbHVyZXMkMQEjVGVzdFJ1bGVJZ25vcmVBZnRlck" + - "1heEZhaWx1cmVzLmphdmEIZXZhbHVhdGVAMW9yZy5hcGFjaGUubHVjZW5lLnV0aWwuVGVzdFJ1bGVJZ25vcmVUZXN0U3VpdGVzJDEBHVRlc3RSd" + - "WxlSWdub3JlVGVzdFN1aXRlcy5qYXZhCGV2YWx1YXRlNjljb20uY2Fycm90c2VhcmNoLnJhbmRvbWl6ZWR0ZXN0aW5nLnJ1bGVzLlN0YXRlbWVu" + - "dEFkYXB0ZXIBFVN0YXRlbWVudEFkYXB0ZXIuamF2YQhldmFsdWF0ZSREY29tLmNhcnJvdHNlYXJjaC5yYW5kb21pemVkdGVzdGluZy5UaHJlYWR" + - "MZWFrQ29udHJvbCRTdGF0ZW1lbnRSdW5uZXIBFlRocmVhZExlYWtDb250cm9sLmphdmEDcnVu7wIQamF2YS5sYW5nLlRocmVhZAELVGhyZWFkLm" + - "phdmEDcnVu6QUABAdoZWFkZXIyAQZ2YWx1ZTIKZXMuaGVhZGVyMwEGdmFsdWUzB2hlYWRlcjEBBnZhbHVlMQplcy5oZWFkZXI0AQZ2YWx1ZTQAA" + - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + - "AAAAA")); - - try (StreamInput in = decoded.streamInput()) { - //randomize the version across released and unreleased ones - Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_1, Version.V_5_0_2, - Version.V_5_1_1, Version.V_5_1_2, Version.V_5_2_0); - in.setVersion(version); - ElasticsearchException exception = new ElasticsearchException(in); - assertEquals("test message", exception.getMessage()); - //the headers received as part of a single set get split based on their prefix - assertEquals(2, exception.getHeaderKeys().size()); - assertEquals("value1", exception.getHeader("header1").get(0)); - assertEquals("value2", exception.getHeader("header2").get(0)); - assertEquals(2, exception.getMetadataKeys().size()); - assertEquals("value3", exception.getMetadata("es.header3").get(0)); - assertEquals("value4", exception.getMetadata("es.header4").get(0)); - } - } - private static class UnknownException extends Exception { UnknownException(final String message, final Exception cause) { super(message, cause); diff --git a/server/src/test/java/org/elasticsearch/VersionTests.java b/server/src/test/java/org/elasticsearch/VersionTests.java index 74303bfb6d85..4c7dc9eb094b 100644 --- a/server/src/test/java/org/elasticsearch/VersionTests.java +++ b/server/src/test/java/org/elasticsearch/VersionTests.java @@ -36,8 +36,8 @@ import java.util.Map; import java.util.Set; -import static org.elasticsearch.Version.V_5_3_0; -import static org.elasticsearch.Version.V_6_0_0_beta1; +import static org.elasticsearch.Version.V_6_3_0; +import static org.elasticsearch.Version.V_7_0_0_alpha1; import static org.elasticsearch.test.VersionUtils.allVersions; import static org.elasticsearch.test.VersionUtils.randomVersion; import static org.hamcrest.CoreMatchers.equalTo; @@ -50,30 +50,30 @@ public class VersionTests extends ESTestCase { public void testVersionComparison() throws Exception { - assertThat(V_5_3_0.before(V_6_0_0_beta1), is(true)); - assertThat(V_5_3_0.before(V_5_3_0), is(false)); - assertThat(V_6_0_0_beta1.before(V_5_3_0), is(false)); + assertThat(V_6_3_0.before(V_7_0_0_alpha1), is(true)); + assertThat(V_6_3_0.before(V_6_3_0), is(false)); + assertThat(V_7_0_0_alpha1.before(V_6_3_0), is(false)); - assertThat(V_5_3_0.onOrBefore(V_6_0_0_beta1), is(true)); - assertThat(V_5_3_0.onOrBefore(V_5_3_0), is(true)); - assertThat(V_6_0_0_beta1.onOrBefore(V_5_3_0), is(false)); + assertThat(V_6_3_0.onOrBefore(V_7_0_0_alpha1), is(true)); + assertThat(V_6_3_0.onOrBefore(V_6_3_0), is(true)); + assertThat(V_7_0_0_alpha1.onOrBefore(V_6_3_0), is(false)); - assertThat(V_5_3_0.after(V_6_0_0_beta1), is(false)); - assertThat(V_5_3_0.after(V_5_3_0), is(false)); - assertThat(V_6_0_0_beta1.after(V_5_3_0), is(true)); + assertThat(V_6_3_0.after(V_7_0_0_alpha1), is(false)); + assertThat(V_6_3_0.after(V_6_3_0), is(false)); + assertThat(V_7_0_0_alpha1.after(V_6_3_0), is(true)); - assertThat(V_5_3_0.onOrAfter(V_6_0_0_beta1), is(false)); - assertThat(V_5_3_0.onOrAfter(V_5_3_0), is(true)); - assertThat(V_6_0_0_beta1.onOrAfter(V_5_3_0), is(true)); + assertThat(V_6_3_0.onOrAfter(V_7_0_0_alpha1), is(false)); + assertThat(V_6_3_0.onOrAfter(V_6_3_0), is(true)); + assertThat(V_7_0_0_alpha1.onOrAfter(V_6_3_0), is(true)); assertTrue(Version.fromString("5.0.0-alpha2").onOrAfter(Version.fromString("5.0.0-alpha1"))); assertTrue(Version.fromString("5.0.0").onOrAfter(Version.fromString("5.0.0-beta2"))); assertTrue(Version.fromString("5.0.0-rc1").onOrAfter(Version.fromString("5.0.0-beta24"))); assertTrue(Version.fromString("5.0.0-alpha24").before(Version.fromString("5.0.0-beta0"))); - assertThat(V_5_3_0, is(lessThan(V_6_0_0_beta1))); - assertThat(V_5_3_0.compareTo(V_5_3_0), is(0)); - assertThat(V_6_0_0_beta1, is(greaterThan(V_5_3_0))); + assertThat(V_6_3_0, is(lessThan(V_7_0_0_alpha1))); + assertThat(V_6_3_0.compareTo(V_6_3_0), is(0)); + assertThat(V_7_0_0_alpha1, is(greaterThan(V_6_3_0))); } public void testMin() { @@ -101,12 +101,12 @@ public void testMax() { } public void testMinimumIndexCompatibilityVersion() { - assertEquals(Version.V_5_0_0, Version.V_6_0_0_beta1.minimumIndexCompatibilityVersion()); - assertEquals(Version.fromId(2000099), Version.V_5_0_0.minimumIndexCompatibilityVersion()); + assertEquals(Version.fromId(5000099), Version.V_6_0_0_beta1.minimumIndexCompatibilityVersion()); + assertEquals(Version.fromId(2000099), Version.fromId(5000099).minimumIndexCompatibilityVersion()); assertEquals(Version.fromId(2000099), - Version.V_5_1_1.minimumIndexCompatibilityVersion()); + Version.fromId(5010000).minimumIndexCompatibilityVersion()); assertEquals(Version.fromId(2000099), - Version.V_5_0_0_alpha1.minimumIndexCompatibilityVersion()); + Version.fromId(5000001).minimumIndexCompatibilityVersion()); } public void testVersionConstantPresent() { @@ -160,31 +160,38 @@ public void testVersionNoPresentInSettings() { public void testIndexCreatedVersion() { // an actual index has a IndexMetaData.SETTING_INDEX_UUID - final Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_2, - Version.V_5_2_0, Version.V_6_0_0_beta1); + final Version version = Version.V_6_0_0_beta1; assertEquals(version, Version.indexCreated(Settings.builder().put(IndexMetaData.SETTING_INDEX_UUID, "foo").put(IndexMetaData.SETTING_VERSION_CREATED, version).build())); } public void testMinCompatVersion() { - Version prerelease = VersionUtils.getFirstVersion(); - assertThat(prerelease.minimumCompatibilityVersion(), equalTo(prerelease)); Version major = Version.fromString("2.0.0"); assertThat(Version.fromString("2.0.0").minimumCompatibilityVersion(), equalTo(major)); assertThat(Version.fromString("2.2.0").minimumCompatibilityVersion(), equalTo(major)); assertThat(Version.fromString("2.3.0").minimumCompatibilityVersion(), equalTo(major)); - // from 6.0 on we are supporting the latest minor of the previous major... this might fail once we add a new version ie. 5.x is + + Version major5x = Version.fromString("5.0.0"); + assertThat(Version.fromString("5.0.0").minimumCompatibilityVersion(), equalTo(major5x)); + assertThat(Version.fromString("5.2.0").minimumCompatibilityVersion(), equalTo(major5x)); + assertThat(Version.fromString("5.3.0").minimumCompatibilityVersion(), equalTo(major5x)); + + Version major56x = Version.fromString("5.6.0"); + assertThat(Version.V_6_5_0.minimumCompatibilityVersion(), equalTo(major56x)); + assertThat(Version.V_6_3_1.minimumCompatibilityVersion(), equalTo(major56x)); + + // from 7.0 on we are supporting the latest minor of the previous major... this might fail once we add a new version ie. 5.x is // released since we need to bump the supported minor in Version#minimumCompatibilityVersion() - Version lastVersion = Version.V_5_6_0; // TODO: remove this once min compat version is a constant instead of method - assertEquals(lastVersion.major, Version.V_6_0_0_beta1.minimumCompatibilityVersion().major); + Version lastVersion = Version.V_6_5_0; // TODO: remove this once min compat version is a constant instead of method + assertEquals(lastVersion.major, Version.V_7_0_0_alpha1.minimumCompatibilityVersion().major); assertEquals("did you miss to bump the minor in Version#minimumCompatibilityVersion()", - lastVersion.minor, Version.V_6_0_0_beta1.minimumCompatibilityVersion().minor); - assertEquals(0, Version.V_6_0_0_beta1.minimumCompatibilityVersion().revision); + lastVersion.minor, Version.V_7_0_0_alpha1.minimumCompatibilityVersion().minor); + assertEquals(0, Version.V_7_0_0_alpha1.minimumCompatibilityVersion().revision); } public void testToString() { // with 2.0.beta we lowercase assertEquals("2.0.0-beta1", Version.fromString("2.0.0-beta1").toString()); - assertEquals("5.0.0-alpha1", Version.V_5_0_0_alpha1.toString()); + assertEquals("5.0.0-alpha1", Version.fromId(5000001).toString()); assertEquals("2.3.0", Version.fromString("2.3.0").toString()); assertEquals("0.90.0.Beta1", Version.fromString("0.90.0.Beta1").toString()); assertEquals("1.0.0.Beta1", Version.fromString("1.0.0.Beta1").toString()); @@ -334,11 +341,11 @@ public static void assertUnknownVersion(Version version) { public void testIsCompatible() { assertTrue(isCompatible(Version.CURRENT, Version.CURRENT.minimumCompatibilityVersion())); - assertTrue(isCompatible(Version.V_5_6_0, Version.V_6_0_0_alpha2)); - assertFalse(isCompatible(Version.fromId(2000099), Version.V_6_0_0_alpha2)); - assertFalse(isCompatible(Version.fromId(2000099), Version.V_5_0_0)); - assertFalse(isCompatible(Version.fromString("6.0.0"), Version.fromString("7.0.0"))); - assertFalse(isCompatible(Version.fromString("6.0.0-alpha1"), Version.fromString("7.0.0"))); + assertTrue(isCompatible(Version.V_6_5_0, Version.V_7_0_0_alpha1)); + assertFalse(isCompatible(Version.fromId(2000099), Version.V_7_0_0_alpha1)); + assertFalse(isCompatible(Version.fromId(2000099), Version.V_6_5_0)); + assertFalse(isCompatible(Version.fromString("7.0.0"), Version.fromString("8.0.0"))); + assertFalse(isCompatible(Version.fromString("7.0.0-alpha1"), Version.fromString("8.0.0"))); final Version currentMajorVersion = Version.fromId(Version.CURRENT.major * 1000000 + 99); final Version currentOrNextMajorVersion; @@ -373,8 +380,8 @@ public void testIsCompatible() { isCompatible(VersionUtils.getPreviousMinorVersion(), currentOrNextMajorVersion), equalTo(isCompatible)); - assertFalse(isCompatible(Version.V_5_0_0, Version.fromString("6.0.0"))); - assertFalse(isCompatible(Version.V_5_0_0, Version.fromString("7.0.0"))); + assertFalse(isCompatible(Version.fromId(5000099), Version.fromString("6.0.0"))); + assertFalse(isCompatible(Version.fromId(5000099), Version.fromString("7.0.0"))); Version a = randomVersion(random()); Version b = randomVersion(random()); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java index 7bf43b828c05..3384efcf836c 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/stats/NodeStatsTests.java @@ -49,7 +49,6 @@ import java.util.List; import java.util.Map; -import static com.carrotsearch.randomizedtesting.RandomizedTest.randomLongBetween; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequestTests.java index 232259948fb2..5f5fe54321bb 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsRequestTests.java @@ -54,7 +54,7 @@ public void testSerialization() throws Exception { request.routing(routings); } - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.CURRENT); + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); try (BytesStreamOutput out = new BytesStreamOutput()) { out.setVersion(version); request.writeTo(out); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponseTests.java index 90eb7cdcfd46..f685be02141a 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/shards/ClusterSearchShardsResponseTests.java @@ -77,7 +77,7 @@ public void testSerialization() throws Exception { List entries = new ArrayList<>(); entries.addAll(searchModule.getNamedWriteables()); NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(entries); - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.CURRENT); + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); try(BytesStreamOutput out = new BytesStreamOutput()) { out.setVersion(version); clusterSearchShardsResponse.writeTo(out); @@ -93,11 +93,7 @@ public void testSerialization() throws Exception { assertEquals(clusterSearchShardsGroup.getShardId(), deserializedGroup.getShardId()); assertArrayEquals(clusterSearchShardsGroup.getShards(), deserializedGroup.getShards()); } - if (version.onOrAfter(Version.V_5_1_1)) { - assertEquals(clusterSearchShardsResponse.getIndicesAndFilters(), deserialized.getIndicesAndFilters()); - } else { - assertNull(deserialized.getIndicesAndFilters()); - } + assertEquals(clusterSearchShardsResponse.getIndicesAndFilters(), deserialized.getIndicesAndFilters()); } } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponseTests.java index 0cb0063727fe..c0685d5d17d2 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexResponseTests.java @@ -19,10 +19,7 @@ package org.elasticsearch.action.admin.indices.create; -import org.elasticsearch.Version; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.AbstractStreamableXContentTestCase; @@ -67,25 +64,6 @@ protected CreateIndexResponse doParseInstance(XContentParser parser) { return CreateIndexResponse.fromXContent(parser); } - public void testSerializationWithOldVersion() throws IOException { - Version oldVersion = Version.V_5_4_0; - CreateIndexResponse response = new CreateIndexResponse(true, true, "foo"); - - try (BytesStreamOutput output = new BytesStreamOutput()) { - output.setVersion(oldVersion); - response.writeTo(output); - - try (StreamInput in = output.bytes().streamInput()) { - in.setVersion(oldVersion); - CreateIndexResponse serialized = new CreateIndexResponse(); - serialized.readFrom(in); - assertEquals(response.isShardsAcknowledged(), serialized.isShardsAcknowledged()); - assertEquals(response.isAcknowledged(), serialized.isAcknowledged()); - assertNull(serialized.index()); - } - } - } - public void testToXContent() { CreateIndexResponse response = new CreateIndexResponse(true, false, "index_name"); String output = Strings.toString(response); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequestTests.java index 86c2b67be9c5..5243ffd33b39 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequestTests.java @@ -19,20 +19,14 @@ package org.elasticsearch.action.admin.indices.mapping.put; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; -import org.elasticsearch.common.xcontent.yaml.YamlXContent; import org.elasticsearch.index.Index; import org.elasticsearch.index.RandomCreateIndexGenerator; import org.elasticsearch.test.ESTestCase; @@ -87,27 +81,6 @@ public void testBuildFromSimplifiedDef() { assertEquals("mapping source must be pairs of fieldnames and properties definition.", e.getMessage()); } - public void testPutMappingRequestSerialization() throws IOException { - PutMappingRequest request = new PutMappingRequest("foo"); - String mapping = Strings.toString(YamlXContent.contentBuilder().startObject().field("foo", "bar").endObject()); - request.source(mapping, XContentType.YAML); - assertEquals(XContentHelper.convertToJson(new BytesArray(mapping), false, XContentType.YAML), request.source()); - - final Version version = randomFrom(Version.CURRENT, Version.V_5_3_0, Version.V_5_3_1, Version.V_5_3_2, Version.V_5_4_0); - try (BytesStreamOutput bytesStreamOutput = new BytesStreamOutput()) { - bytesStreamOutput.setVersion(version); - request.writeTo(bytesStreamOutput); - try (StreamInput in = StreamInput.wrap(bytesStreamOutput.bytes().toBytesRef().bytes)) { - in.setVersion(version); - PutMappingRequest serialized = new PutMappingRequest(); - serialized.readFrom(in); - - String source = serialized.source(); - assertEquals(XContentHelper.convertToJson(new BytesArray(mapping), false, XContentType.YAML), source); - } - } - } - public void testToXContent() throws IOException { PutMappingRequest request = new PutMappingRequest("foo"); request.type("my_type"); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java index c21e6b3c225f..2d037d7c024d 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequestTests.java @@ -18,25 +18,16 @@ */ package org.elasticsearch.action.admin.indices.template.put; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.admin.indices.alias.Alias; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.common.xcontent.yaml.YamlXContent; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import static org.hamcrest.Matchers.containsString; @@ -46,81 +37,6 @@ import static org.hamcrest.core.Is.is; public class PutIndexTemplateRequestTests extends AbstractXContentTestCase { - - // bwc for #21009 - public void testPutIndexTemplateRequest510() throws IOException { - PutIndexTemplateRequest putRequest = new PutIndexTemplateRequest("test"); - putRequest.patterns(Collections.singletonList("test*")); - putRequest.order(5); - - PutIndexTemplateRequest multiPatternRequest = new PutIndexTemplateRequest("test"); - multiPatternRequest.patterns(Arrays.asList("test*", "*test2", "*test3*")); - multiPatternRequest.order(5); - - // These bytes were retrieved by Base64 encoding the result of the above with 5_0_0 code. - // Note: Instead of a list for the template, in 5_0_0 the element was provided as a string. - String putRequestBytes = "ADwDAAR0ZXN0BXRlc3QqAAAABQAAAAAAAA=="; - BytesArray bytes = new BytesArray(Base64.getDecoder().decode(putRequestBytes)); - - try (StreamInput in = bytes.streamInput()) { - in.setVersion(Version.V_5_0_0); - PutIndexTemplateRequest readRequest = new PutIndexTemplateRequest(); - readRequest.readFrom(in); - assertEquals(putRequest.patterns(), readRequest.patterns()); - assertEquals(putRequest.order(), readRequest.order()); - - BytesStreamOutput output = new BytesStreamOutput(); - output.setVersion(Version.V_5_0_0); - readRequest.writeTo(output); - assertEquals(bytes.toBytesRef(), output.bytes().toBytesRef()); - - // test that multi templates are reverse-compatible. - // for the bwc case, if multiple patterns, use only the first pattern seen. - output.reset(); - multiPatternRequest.writeTo(output); - assertEquals(bytes.toBytesRef(), output.bytes().toBytesRef()); - } - } - - public void testPutIndexTemplateRequestSerializationXContent() throws IOException { - PutIndexTemplateRequest request = new PutIndexTemplateRequest("foo"); - String mapping = Strings.toString(YamlXContent.contentBuilder().startObject().field("foo", "bar").endObject()); - request.patterns(Collections.singletonList("foo")); - request.mapping("bar", mapping, XContentType.YAML); - assertNotEquals(mapping, request.mappings().get("bar")); - assertEquals(XContentHelper.convertToJson(new BytesArray(mapping), false, XContentType.YAML), request.mappings().get("bar")); - - final Version version = randomFrom(Version.CURRENT, Version.V_5_3_0, Version.V_5_3_1, Version.V_5_3_2, Version.V_5_4_0); - try (BytesStreamOutput out = new BytesStreamOutput()) { - out.setVersion(version); - request.writeTo(out); - - try (StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes)) { - in.setVersion(version); - PutIndexTemplateRequest serialized = new PutIndexTemplateRequest(); - serialized.readFrom(in); - assertEquals(XContentHelper.convertToJson(new BytesArray(mapping), false, XContentType.YAML), - serialized.mappings().get("bar")); - } - } - } - - public void testPutIndexTemplateRequestSerializationXContentBwc() throws IOException { - final byte[] data = Base64.getDecoder().decode("ADwDAANmb28IdGVtcGxhdGUAAAAAAAABA2Jhcg8tLS0KZm9vOiAiYmFyIgoAAAAAAAAAAAAAAAA="); - final Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_1, Version.V_5_0_2, - Version.V_5_1_1, Version.V_5_1_2, Version.V_5_2_0); - try (StreamInput in = StreamInput.wrap(data)) { - in.setVersion(version); - PutIndexTemplateRequest request = new PutIndexTemplateRequest(); - request.readFrom(in); - String mapping = Strings.toString(YamlXContent.contentBuilder().startObject().field("foo", "bar").endObject()); - assertNotEquals(mapping, request.mappings().get("bar")); - assertEquals(XContentHelper.convertToJson(new BytesArray(mapping), false, XContentType.YAML), request.mappings().get("bar")); - assertEquals("foo", request.name()); - assertEquals("template", request.patterns().get(0)); - } - } - public void testValidateErrorMessage() throws Exception { PutIndexTemplateRequest request = new PutIndexTemplateRequest(); ActionRequestValidationException withoutNameAndPattern = request.validate(); diff --git a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestTests.java b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestTests.java index 5cd82be8cb04..53c307c43081 100644 --- a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.ingest; -import org.elasticsearch.Version; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; @@ -28,7 +27,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Base64; import static org.hamcrest.CoreMatchers.equalTo; @@ -68,22 +66,4 @@ public void testSerializationWithXContent() throws IOException { assertEquals(XContentType.JSON, serialized.getXContentType()); assertEquals("{}", serialized.getSource().utf8ToString()); } - - public void testSerializationWithXContentBwc() throws IOException { - final byte[] data = Base64.getDecoder().decode("AAAAAnt9AAA="); - final Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_1, Version.V_5_0_2, - Version.V_5_1_1, Version.V_5_1_2, Version.V_5_2_0); - try (StreamInput in = StreamInput.wrap(data)) { - in.setVersion(version); - SimulatePipelineRequest request = new SimulatePipelineRequest(in); - assertEquals(XContentType.JSON, request.getXContentType()); - assertEquals("{}", request.getSource().utf8ToString()); - - try (BytesStreamOutput out = new BytesStreamOutput()) { - out.setVersion(version); - request.writeTo(out); - assertArrayEquals(data, out.bytes().toBytesRef().bytes); - } - } - } } diff --git a/server/src/test/java/org/elasticsearch/action/search/CanMatchPreFilterSearchPhaseTests.java b/server/src/test/java/org/elasticsearch/action/search/CanMatchPreFilterSearchPhaseTests.java index 8b1741967734..50bbad16ab73 100644 --- a/server/src/test/java/org/elasticsearch/action/search/CanMatchPreFilterSearchPhaseTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/CanMatchPreFilterSearchPhaseTests.java @@ -33,7 +33,6 @@ import org.elasticsearch.search.internal.AliasFilter; import org.elasticsearch.search.internal.ShardSearchTransportRequest; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.VersionUtils; import org.elasticsearch.transport.Transport; import java.io.IOException; @@ -110,17 +109,6 @@ public void run() throws IOException { } } - public void testOldNodesTriggerException() { - SearchTransportService searchTransportService = new SearchTransportService( - Settings.builder().put("search.remote.connect", false).build(), null, null); - DiscoveryNode node = new DiscoveryNode("node_1", buildNewFakeTransportAddress(), VersionUtils.randomVersionBetween(random(), - VersionUtils.getFirstVersion(), VersionUtils.getPreviousVersion(Version.V_5_6_0))); - SearchAsyncActionTests.MockConnection mockConnection = new SearchAsyncActionTests.MockConnection(node); - IllegalArgumentException illegalArgumentException = expectThrows(IllegalArgumentException.class, - () -> searchTransportService.sendCanMatch(mockConnection, null, null, null)); - assertEquals("can_match is not supported on pre 5.6 nodes", illegalArgumentException.getMessage()); - } - public void testFilterWithFailure() throws InterruptedException { final TransportSearchAction.SearchTimeProvider timeProvider = new TransportSearchAction.SearchTimeProvider(0, System.nanoTime(), System::nanoTime); diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java index 87e66477a041..feb5ef50795d 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.action.search; -import org.elasticsearch.Version; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -47,13 +46,11 @@ import org.elasticsearch.search.suggest.SuggestTests; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.InternalAggregationTestCase; -import org.elasticsearch.test.VersionUtils; import org.junit.After; import org.junit.Before; import java.io.IOException; import java.util.ArrayList; -import java.util.Base64; import java.util.Collections; import java.util.List; @@ -290,27 +287,4 @@ public void testSerialization() throws IOException { assertEquals(searchResponse.getClusters(), serialized.getClusters()); } } - - public void testSerializationBwc() throws IOException { - final byte[] data = Base64.getDecoder().decode("AAAAAAAAAAAAAgABBQUAAAoAAAAAAAAA"); - final Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_6_5, Version.V_6_0_0); - try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(data), namedWriteableRegistry)) { - in.setVersion(version); - SearchResponse deserialized = new SearchResponse(); - deserialized.readFrom(in); - assertSame(SearchResponse.Clusters.EMPTY, deserialized.getClusters()); - - try (BytesStreamOutput out = new BytesStreamOutput()) { - out.setVersion(version); - deserialized.writeTo(out); - try (StreamInput in2 = new NamedWriteableAwareStreamInput(StreamInput.wrap(out.bytes().toBytesRef().bytes), - namedWriteableRegistry)) { - in2.setVersion(version); - SearchResponse deserialized2 = new SearchResponse(); - deserialized2.readFrom(in2); - assertSame(SearchResponse.Clusters.EMPTY, deserialized2.getClusters()); - } - } - } - } } diff --git a/server/src/test/java/org/elasticsearch/action/termvectors/TermVectorsUnitTests.java b/server/src/test/java/org/elasticsearch/action/termvectors/TermVectorsUnitTests.java index a16a8f628f98..216c1802956e 100644 --- a/server/src/test/java/org/elasticsearch/action/termvectors/TermVectorsUnitTests.java +++ b/server/src/test/java/org/elasticsearch/action/termvectors/TermVectorsUnitTests.java @@ -36,14 +36,11 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; -import org.elasticsearch.Version; import org.elasticsearch.action.termvectors.TermVectorsRequest.Flag; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.InputStreamStreamInput; import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; @@ -60,7 +57,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; -import java.util.Base64; import java.util.EnumSet; import java.util.HashSet; import java.util.Set; @@ -264,34 +260,6 @@ public void testStreamRequest() throws IOException { } } - public void testStreamRequestWithXContentBwc() throws IOException { - final byte[] data = Base64.getDecoder().decode("AAABBWluZGV4BHR5cGUCaWQBAnt9AAABDnNvbWVQcmVmZXJlbmNlFgAAAAEA//////////0AAAA="); - final Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_1, Version.V_5_0_2, - Version.V_5_1_1, Version.V_5_1_2, Version.V_5_2_0); - try (StreamInput in = StreamInput.wrap(data)) { - in.setVersion(version); - TermVectorsRequest request = new TermVectorsRequest(); - request.readFrom(in); - assertEquals("index", request.index()); - assertEquals("type", request.type()); - assertEquals("id", request.id()); - assertTrue(request.offsets()); - assertFalse(request.fieldStatistics()); - assertTrue(request.payloads()); - assertFalse(request.positions()); - assertTrue(request.termStatistics()); - assertEquals("somePreference", request.preference()); - assertEquals("{}", request.doc().utf8ToString()); - assertEquals(XContentType.JSON, request.xContentType()); - - try (BytesStreamOutput out = new BytesStreamOutput()) { - out.setVersion(version); - request.writeTo(out); - assertArrayEquals(data, out.bytes().toBytesRef().bytes); - } - } - } - public void testFieldTypeToTermVectorString() throws Exception { FieldType ft = new FieldType(); ft.setStoreTermVectorOffsets(false); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java index 6d489f5feb31..c98587c4cc63 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java @@ -18,12 +18,9 @@ */ package org.elasticsearch.cluster.metadata; -import org.elasticsearch.Version; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.ImmutableOpenMap; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -35,62 +32,15 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.ESTestCase; -import java.io.IOException; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import static java.util.Collections.singletonMap; -import static org.elasticsearch.cluster.metadata.AliasMetaData.newAliasMetaDataBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.contains; public class IndexTemplateMetaDataTests extends ESTestCase { - // bwc for #21009 - public void testIndexTemplateMetaData510() throws IOException { - IndexTemplateMetaData metaData = IndexTemplateMetaData.builder("foo") - .patterns(Collections.singletonList("bar")) - .order(1) - .settings(Settings.builder() - .put("setting1", "value1") - .put("setting2", "value2")) - .putAlias(newAliasMetaDataBuilder("alias-bar1")).build(); - - IndexTemplateMetaData multiMetaData = IndexTemplateMetaData.builder("foo") - .patterns(Arrays.asList("bar", "foo")) - .order(1) - .settings(Settings.builder() - .put("setting1", "value1") - .put("setting2", "value2")) - .putAlias(newAliasMetaDataBuilder("alias-bar1")).build(); - - // These bytes were retrieved by Base64 encoding the result of the above with 5_0_0 code - String templateBytes = "A2ZvbwAAAAEDYmFyAghzZXR0aW5nMQEGdmFsdWUxCHNldHRpbmcyAQZ2YWx1ZTIAAQphbGlhcy1iYXIxAAAAAAA="; - BytesArray bytes = new BytesArray(Base64.getDecoder().decode(templateBytes)); - - try (StreamInput in = bytes.streamInput()) { - in.setVersion(Version.V_5_0_0); - IndexTemplateMetaData readMetaData = IndexTemplateMetaData.readFrom(in); - assertEquals(0, in.available()); - assertEquals(metaData.getName(), readMetaData.getName()); - assertEquals(metaData.getPatterns(), readMetaData.getPatterns()); - assertTrue(metaData.aliases().containsKey("alias-bar1")); - assertEquals(1, metaData.aliases().size()); - - BytesStreamOutput output = new BytesStreamOutput(); - output.setVersion(Version.V_5_0_0); - readMetaData.writeTo(output); - assertEquals(bytes.toBytesRef(), output.bytes().toBytesRef()); - - // test that multi templates are reverse-compatible. - // for the bwc case, if multiple patterns, use only the first pattern seen. - output.reset(); - multiMetaData.writeTo(output); - assertEquals(bytes.toBytesRef(), output.bytes().toBytesRef()); - } - } - public void testIndexTemplateMetaDataXContentRoundTrip() throws Exception { ToXContent.Params params = new ToXContent.MapParams(singletonMap("reduce_mappings", "true")); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexUpgradeServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexUpgradeServiceTests.java index e329e70134c0..c1e341fd5bc2 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexUpgradeServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexUpgradeServiceTests.java @@ -147,7 +147,7 @@ public static IndexMetaData newIndexMeta(String name, Settings indexSettings) { .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_CREATION_DATE, 1) .put(IndexMetaData.SETTING_INDEX_UUID, "BOOM") - .put(IndexMetaData.SETTING_VERSION_UPGRADED, Version.V_5_0_0_beta1) + .put(IndexMetaData.SETTING_VERSION_UPGRADED, Version.V_6_0_0_alpha1) .put(indexSettings) .build(); return IndexMetaData.builder(name).settings(build).build(); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedNodeRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedNodeRoutingTests.java index 8038d9b5e18d..d4645208071a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedNodeRoutingTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedNodeRoutingTests.java @@ -228,7 +228,7 @@ protected DiscoveryNode createNode(DiscoveryNode.Role... mustHaveRoles) { } final String id = String.format(Locale.ROOT, "node_%03d", nodeIdGenerator.incrementAndGet()); return new DiscoveryNode(id, id, buildNewFakeTransportAddress(), Collections.emptyMap(), roles, - VersionUtils.randomVersionBetween(random(), Version.V_5_6_0, null)); + VersionUtils.randomVersionBetween(random(), Version.V_6_0_0_alpha1, null)); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java index 1fa1ff3a154a..787789d410ff 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/FailedShardsRoutingTests.java @@ -576,7 +576,7 @@ public void testReplicaOnNewestVersionIsPromoted() { // add a single node clusterState = ClusterState.builder(clusterState).nodes( DiscoveryNodes.builder() - .add(newNode("node1-5.x", Version.V_5_6_0))) + .add(newNode("node1-5.x", Version.fromId(5060099)))) .build(); clusterState = ClusterState.builder(clusterState).routingTable(allocation.reroute(clusterState, "reroute").routingTable()).build(); assertThat(clusterState.getRoutingNodes().shardsWithState(INITIALIZING).size(), equalTo(1)); @@ -590,7 +590,7 @@ public void testReplicaOnNewestVersionIsPromoted() { // add another 5.6 node clusterState = ClusterState.builder(clusterState).nodes( DiscoveryNodes.builder(clusterState.nodes()) - .add(newNode("node2-5.x", Version.V_5_6_0))) + .add(newNode("node2-5.x", Version.fromId(5060099)))) .build(); // start the shards, should have 1 primary and 1 replica available diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ResizeAllocationDeciderTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ResizeAllocationDeciderTests.java index 2022ecb945ba..536e3cbb7e08 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ResizeAllocationDeciderTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/ResizeAllocationDeciderTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.cluster.routing.allocation; import org.elasticsearch.Version; -import org.elasticsearch.action.admin.indices.shrink.ResizeAction; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ESAllocationTestCase; @@ -39,7 +38,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.gateway.TestGatewayAllocator; import java.util.Arrays; @@ -243,46 +241,4 @@ public void testSourcePrimaryActive() { routingAllocation).getExplanation()); } } - - public void testAllocateOnOldNode() { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, - VersionUtils.getPreviousVersion(ResizeAction.COMPATIBILITY_VERSION)); - ClusterState clusterState = createInitialClusterState(true, version); - MetaData.Builder metaBuilder = MetaData.builder(clusterState.metaData()); - metaBuilder.put(IndexMetaData.builder("target").settings(settings(Version.CURRENT) - .put(IndexMetaData.INDEX_RESIZE_SOURCE_NAME.getKey(), "source") - .put(IndexMetaData.INDEX_RESIZE_SOURCE_UUID_KEY, IndexMetaData.INDEX_UUID_NA_VALUE)) - .numberOfShards(4).numberOfReplicas(0)); - MetaData metaData = metaBuilder.build(); - RoutingTable.Builder routingTableBuilder = RoutingTable.builder(clusterState.routingTable()); - routingTableBuilder.addAsNew(metaData.index("target")); - - clusterState = ClusterState.builder(clusterState) - .routingTable(routingTableBuilder.build()) - .metaData(metaData).build(); - Index idx = clusterState.metaData().index("target").getIndex(); - - - ResizeAllocationDecider resizeAllocationDecider = new ResizeAllocationDecider(Settings.EMPTY); - RoutingAllocation routingAllocation = new RoutingAllocation(null, clusterState.getRoutingNodes(), clusterState, null, 0); - int shardId = randomIntBetween(0, 3); - int sourceShardId = IndexMetaData.selectSplitShard(shardId, clusterState.metaData().index("source"), 4).id(); - ShardRouting shardRouting = TestShardRouting.newShardRouting(new ShardId(idx, shardId), null, true, RecoverySource - .LocalShardsRecoverySource.INSTANCE, ShardRoutingState.UNASSIGNED); - assertEquals(Decision.YES, resizeAllocationDecider.canAllocate(shardRouting, routingAllocation)); - - assertEquals(Decision.NO, resizeAllocationDecider.canAllocate(shardRouting, clusterState.getRoutingNodes().node("node1"), - routingAllocation)); - assertEquals(Decision.NO, resizeAllocationDecider.canAllocate(shardRouting, clusterState.getRoutingNodes().node("node2"), - routingAllocation)); - - routingAllocation.debugDecision(true); - assertEquals("source primary is active", resizeAllocationDecider.canAllocate(shardRouting, routingAllocation).getExplanation()); - assertEquals("node [node1] is too old to split a shard", - resizeAllocationDecider.canAllocate(shardRouting, clusterState.getRoutingNodes().node("node1"), - routingAllocation).getExplanation()); - assertEquals("node [node2] is too old to split a shard", - resizeAllocationDecider.canAllocate(shardRouting, clusterState.getRoutingNodes().node("node2"), - routingAllocation).getExplanation()); - } } diff --git a/server/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java b/server/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java index e193ea34498c..feaa7c4a0ae5 100644 --- a/server/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java +++ b/server/src/test/java/org/elasticsearch/common/unit/ByteSizeValueTests.java @@ -20,7 +20,6 @@ package org.elasticsearch.common.unit; import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.hamcrest.MatcherAssert; @@ -319,9 +318,4 @@ public void testGetBytesAsInt() { } } } - - public void testOldSerialisation() throws IOException { - ByteSizeValue original = createTestInstance(); - assertSerialization(original, randomFrom(Version.V_5_6_4, Version.V_5_6_5, Version.V_6_0_0, Version.V_6_0_1, Version.V_6_1_0)); - } } diff --git a/server/src/test/java/org/elasticsearch/common/util/IndexFolderUpgraderTests.java b/server/src/test/java/org/elasticsearch/common/util/IndexFolderUpgraderTests.java index 76dd8e343a26..dd2627f4bc20 100644 --- a/server/src/test/java/org/elasticsearch/common/util/IndexFolderUpgraderTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/IndexFolderUpgraderTests.java @@ -63,7 +63,7 @@ public void testUpgradeCustomDataPath() throws IOException { Settings settings = Settings.builder() .put(nodeSettings) .put(IndexMetaData.SETTING_INDEX_UUID, index.getUUID()) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_5_0_0) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_6_0_0) .put(IndexMetaData.SETTING_DATA_PATH, customPath.toAbsolutePath().toString()) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 5)) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) @@ -91,7 +91,7 @@ public void testPartialUpgradeCustomDataPath() throws IOException { Settings settings = Settings.builder() .put(nodeSettings) .put(IndexMetaData.SETTING_INDEX_UUID, index.getUUID()) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_5_0_0) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_6_0_0) .put(IndexMetaData.SETTING_DATA_PATH, customPath.toAbsolutePath().toString()) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 5)) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) @@ -129,7 +129,7 @@ public void testUpgrade() throws IOException { Settings settings = Settings.builder() .put(nodeSettings) .put(IndexMetaData.SETTING_INDEX_UUID, index.getUUID()) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_5_0_0) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_6_0_0) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 5)) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) .build(); @@ -153,7 +153,7 @@ public void testUpgradeIndices() throws IOException { Settings settings = Settings.builder() .put(nodeSettings) .put(IndexMetaData.SETTING_INDEX_UUID, index.getUUID()) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_5_0_0) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_6_0_0) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, randomIntBetween(1, 5)) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) .build(); diff --git a/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java b/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java index 2f4be2fcd539..3c06838593fb 100644 --- a/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java +++ b/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java @@ -80,7 +80,7 @@ public void testPreventJoinClusterWithUnsupportedNodeVersions() { final Version maxNodeVersion = nodes.getMaxNodeVersion(); final Version minNodeVersion = nodes.getMinNodeVersion(); - if (maxNodeVersion.onOrAfter(Version.V_6_0_0_alpha1)) { + if (maxNodeVersion.onOrAfter(Version.V_7_0_0_alpha1)) { final Version tooLow = getPreviousVersion(maxNodeVersion.minimumCompatibilityVersion()); expectThrows(IllegalStateException.class, () -> { if (randomBoolean()) { @@ -91,7 +91,7 @@ public void testPreventJoinClusterWithUnsupportedNodeVersions() { }); } - if (minNodeVersion.before(Version.V_5_5_0)) { + if (minNodeVersion.before(Version.V_6_0_0)) { Version tooHigh = incompatibleFutureVersion(minNodeVersion); expectThrows(IllegalStateException.class, () -> { if (randomBoolean()) { @@ -102,7 +102,7 @@ public void testPreventJoinClusterWithUnsupportedNodeVersions() { }); } - if (minNodeVersion.onOrAfter(Version.V_6_0_0_alpha1)) { + if (minNodeVersion.onOrAfter(Version.V_7_0_0_alpha1)) { Version oldMajor = randomFrom(allVersions().stream().filter(v -> v.major < 6).collect(Collectors.toList())); expectThrows(IllegalStateException.class, () -> MembershipAction.ensureMajorVersionBarrier(oldMajor, minNodeVersion)); } diff --git a/server/src/test/java/org/elasticsearch/get/GetActionIT.java b/server/src/test/java/org/elasticsearch/get/GetActionIT.java index 5ed6b957c78a..829d6ff7c145 100644 --- a/server/src/test/java/org/elasticsearch/get/GetActionIT.java +++ b/server/src/test/java/org/elasticsearch/get/GetActionIT.java @@ -528,7 +528,7 @@ public void testGetFieldsMetaDataWithRouting() throws Exception { assertAcked(prepareCreate("test") .addMapping("_doc", "field1", "type=keyword,store=true") .addAlias(new Alias("alias")) - .setSettings(Settings.builder().put("index.refresh_interval", -1).put("index.version.created", Version.V_5_6_0.id))); + .setSettings(Settings.builder().put("index.refresh_interval", -1).put("index.version.created", Version.V_6_0_0.id))); // multi types in 5.6 client().prepareIndex("test", "_doc", "1") diff --git a/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java b/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java index 78569d927be7..0dcba53df88e 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexSortSettingsTests.java @@ -146,15 +146,4 @@ public void testInvalidMissing() throws IOException { assertThat(exc.getMessage(), containsString("Illegal missing value:[default]," + " must be one of [_last, _first]")); } - - public void testInvalidVersion() throws IOException { - final Settings settings = Settings.builder() - .put("index.sort.field", "field1") - .build(); - IllegalArgumentException exc = - expectThrows(IllegalArgumentException.class, () -> indexSettings(settings, Version.V_5_4_0)); - assertThat(exc.getMessage(), - containsString("unsupported index.version.created:5.4.0, " + - "can't set index.sort on versions prior to 6.0.0-alpha1")); - } } diff --git a/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java b/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java index 26a5b87866c2..04dc98deb7bf 100644 --- a/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java +++ b/server/src/test/java/org/elasticsearch/index/analysis/AnalysisRegistryTests.java @@ -103,7 +103,7 @@ public void testOverrideDefaultAnalyzer() throws IOException { } public void testOverrideDefaultIndexAnalyzerIsUnsupported() { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0_alpha1, Version.CURRENT); + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0_alpha1, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); AnalyzerProvider defaultIndex = new PreBuiltAnalyzerProvider("default_index", AnalyzerScope.INDEX, new EnglishAnalyzer()); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, diff --git a/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java b/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java index 9aba48f7de55..33ec090c61e0 100644 --- a/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java +++ b/server/src/test/java/org/elasticsearch/index/analysis/PreBuiltAnalyzerTests.java @@ -56,21 +56,21 @@ public void testThatDefaultAndStandardAnalyzerAreTheSameInstance() { public void testThatInstancesAreTheSameAlwaysForKeywordAnalyzer() { assertThat(PreBuiltAnalyzers.KEYWORD.getAnalyzer(Version.CURRENT), - is(PreBuiltAnalyzers.KEYWORD.getAnalyzer(Version.V_5_0_0))); + is(PreBuiltAnalyzers.KEYWORD.getAnalyzer(Version.V_6_0_0))); } public void testThatInstancesAreCachedAndReused() { assertSame(PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.CURRENT), PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.CURRENT)); // same es version should be cached - assertSame(PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_5_2_1), - PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_5_2_1)); - assertNotSame(PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_5_0_0), - PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_5_0_1)); + assertSame(PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_6_2_1), + PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_6_2_1)); + assertNotSame(PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_6_0_0), + PreBuiltAnalyzers.STANDARD.getAnalyzer(Version.V_6_0_1)); // Same Lucene version should be cached: - assertSame(PreBuiltAnalyzers.STOP.getAnalyzer(Version.V_5_2_1), - PreBuiltAnalyzers.STOP.getAnalyzer(Version.V_5_2_2)); + assertSame(PreBuiltAnalyzers.STOP.getAnalyzer(Version.V_6_2_1), + PreBuiltAnalyzers.STOP.getAnalyzer(Version.V_6_2_2)); } public void testThatAnalyzersAreUsedInMapping() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplateTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplateTests.java index f48603d30515..a910c2c86bab 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplateTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplateTests.java @@ -40,18 +40,11 @@ public void testParseUnknownParam() throws Exception { templateDef.put("random_param", "random_value"); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, - () -> DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha1)); + () -> DynamicTemplate.parse("my_template", templateDef, Version.V_6_0_0_alpha1)); assertEquals("Illegal dynamic template parameter: [random_param]", e.getMessage()); } public void testParseUnknownMatchType() { - Map templateDef = new HashMap<>(); - templateDef.put("match_mapping_type", "short"); - templateDef.put("mapping", Collections.singletonMap("store", true)); - // if a wrong match type is specified, we ignore the template - assertNull(DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha5)); - assertWarnings("match_mapping_type [short] is invalid and will be ignored: No field type matched on [short], " + - "possible values are [object, string, long, double, boolean, date, binary]"); Map templateDef2 = new HashMap<>(); templateDef2.put("match_mapping_type", "text"); templateDef2.put("mapping", Collections.singletonMap("store", true)); @@ -79,7 +72,7 @@ public void testMatchAllTemplate() { Map templateDef = new HashMap<>(); templateDef.put("match_mapping_type", "*"); templateDef.put("mapping", Collections.singletonMap("store", true)); - DynamicTemplate template = DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha5); + DynamicTemplate template = DynamicTemplate.parse("my_template", templateDef, Version.V_6_0_0_alpha1); assertTrue(template.match("a.b", "b", randomFrom(XContentFieldType.values()))); } @@ -87,7 +80,7 @@ public void testMatchTypeTemplate() { Map templateDef = new HashMap<>(); templateDef.put("match_mapping_type", "string"); templateDef.put("mapping", Collections.singletonMap("store", true)); - DynamicTemplate template = DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha5); + DynamicTemplate template = DynamicTemplate.parse("my_template", templateDef, Version.V_6_0_0_alpha1); assertTrue(template.match("a.b", "b", XContentFieldType.STRING)); assertFalse(template.match("a.b", "b", XContentFieldType.BOOLEAN)); } @@ -97,7 +90,7 @@ public void testSerialization() throws Exception { Map templateDef = new HashMap<>(); templateDef.put("match_mapping_type", "string"); templateDef.put("mapping", Collections.singletonMap("store", true)); - DynamicTemplate template = DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha1); + DynamicTemplate template = DynamicTemplate.parse("my_template", templateDef, Version.V_6_0_0_alpha1); XContentBuilder builder = JsonXContent.contentBuilder(); template.toXContent(builder, ToXContent.EMPTY_PARAMS); assertEquals("{\"match_mapping_type\":\"string\",\"mapping\":{\"store\":true}}", Strings.toString(builder)); @@ -107,7 +100,7 @@ public void testSerialization() throws Exception { templateDef.put("match", "*name"); templateDef.put("unmatch", "first_name"); templateDef.put("mapping", Collections.singletonMap("store", true)); - template = DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha1); + template = DynamicTemplate.parse("my_template", templateDef, Version.V_6_0_0_alpha1); builder = JsonXContent.contentBuilder(); template.toXContent(builder, ToXContent.EMPTY_PARAMS); assertEquals("{\"match\":\"*name\",\"unmatch\":\"first_name\",\"mapping\":{\"store\":true}}", Strings.toString(builder)); @@ -117,7 +110,7 @@ public void testSerialization() throws Exception { templateDef.put("path_match", "*name"); templateDef.put("path_unmatch", "first_name"); templateDef.put("mapping", Collections.singletonMap("store", true)); - template = DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha1); + template = DynamicTemplate.parse("my_template", templateDef, Version.V_6_0_0_alpha1); builder = JsonXContent.contentBuilder(); template.toXContent(builder, ToXContent.EMPTY_PARAMS); assertEquals("{\"path_match\":\"*name\",\"path_unmatch\":\"first_name\",\"mapping\":{\"store\":true}}", @@ -128,7 +121,7 @@ public void testSerialization() throws Exception { templateDef.put("match", "^a$"); templateDef.put("match_pattern", "regex"); templateDef.put("mapping", Collections.singletonMap("store", true)); - template = DynamicTemplate.parse("my_template", templateDef, Version.V_5_0_0_alpha1); + template = DynamicTemplate.parse("my_template", templateDef, Version.V_6_0_0_alpha1); builder = JsonXContent.contentBuilder(); template.toXContent(builder, ToXContent.EMPTY_PARAMS); assertEquals("{\"match\":\"^a$\",\"match_pattern\":\"regex\",\"mapping\":{\"store\":true}}", Strings.toString(builder)); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ExternalFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ExternalFieldMapperTests.java index 8f2a51bbfc2b..5172e7b0b883 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ExternalFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ExternalFieldMapperTests.java @@ -57,7 +57,7 @@ protected Collection> getPlugins() { } public void testExternalValues() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); IndexService indexService = createIndex("test", settings); @@ -107,7 +107,7 @@ public void testExternalValues() throws Exception { } public void testExternalValuesWithMultifield() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); IndexService indexService = createIndex("test", settings); @@ -173,7 +173,7 @@ public void testExternalValuesWithMultifield() throws Exception { } public void testExternalValuesWithMultifieldTwoLevels() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); IndexService indexService = createIndex("test", settings); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldMapperTests.java index 0af663219903..3bec98d33eec 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldMapperTests.java @@ -61,7 +61,7 @@ public void testDocValuesSingleType() throws Exception { public void testDocValues(boolean singleType) throws IOException { Settings indexSettings = singleType ? Settings.EMPTY : Settings.builder() - .put("index.version.created", Version.V_5_6_0) + .put("index.version.created", Version.V_6_0_0) .build(); MapperService mapperService = createIndex("test", indexSettings).mapperService(); DocumentMapper mapper = mapperService.merge("type", new CompressedXContent("{\"type\":{}}"), MergeReason.MAPPING_UPDATE); diff --git a/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java index 0de9cac88550..496d8512d4e2 100644 --- a/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java @@ -30,7 +30,6 @@ import org.apache.lucene.search.PointRangeQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; -import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; @@ -366,9 +365,6 @@ protected void initializeAdditionalMappings(MapperService mapperService) throws public void testMatchPhrasePrefixWithBoost() throws Exception { QueryShardContext context = createShardContext(); - assumeTrue("test runs only when the index version is on or after V_5_0_0_alpha1", - context.indexVersionCreated().onOrAfter(Version.V_5_0_0_alpha1)); - { // field boost is applied on a single term query MatchPhrasePrefixQueryBuilder builder = new MatchPhrasePrefixQueryBuilder("string_boost", "foo"); diff --git a/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java index 6ac97373dfa1..72898dd3911c 100644 --- a/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/MoreLikeThisQueryBuilderTests.java @@ -27,7 +27,6 @@ import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.Version; import org.elasticsearch.action.termvectors.MultiTermVectorsItemResponse; import org.elasticsearch.action.termvectors.MultiTermVectorsRequest; import org.elasticsearch.action.termvectors.MultiTermVectorsResponse; @@ -36,13 +35,11 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.lucene.search.MoreLikeThisQuery; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.query.MoreLikeThisQueryBuilder.Item; @@ -52,7 +49,6 @@ import java.io.IOException; import java.util.Arrays; -import java.util.Base64; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; @@ -333,26 +329,6 @@ public void testItemFromXContent() throws IOException { assertEquals(expectedItem, newItem); } - public void testItemSerializationBwc() throws IOException { - final byte[] data = Base64.getDecoder().decode("AQVpbmRleAEEdHlwZQEODXsiZm9vIjoiYmFyIn0A/wD//////////QAAAAAAAAAA"); - final Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_1, Version.V_5_0_2, - Version.V_5_1_1, Version.V_5_1_2, Version.V_5_2_0); - try (StreamInput in = StreamInput.wrap(data)) { - in.setVersion(version); - Item item = new Item(in); - assertEquals(XContentType.JSON, item.xContentType()); - assertEquals("{\"foo\":\"bar\"}", item.doc().utf8ToString()); - assertEquals("index", item.index()); - assertEquals("type", item.type()); - - try (BytesStreamOutput out = new BytesStreamOutput()) { - out.setVersion(version); - item.writeTo(out); - assertArrayEquals(data, out.bytes().toBytesRef().bytes); - } - } - } - @Override protected boolean isCachable(MoreLikeThisQueryBuilder queryBuilder) { return queryBuilder.likeItems().length == 0; // items are always fetched diff --git a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java index a2e6018d0ef6..76479791283b 100644 --- a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java @@ -124,10 +124,6 @@ protected void doAssertLuceneQuery(NestedQueryBuilder queryBuilder, Query query, public void testSerializationBWC() throws IOException { for (Version version : VersionUtils.allReleasedVersions()) { NestedQueryBuilder testQuery = createTestQueryBuilder(); - if (version.before(Version.V_5_2_0) && testQuery.innerHit() != null) { - // ignore unmapped for inner_hits has been added on 5.2 - testQuery.innerHit().setIgnoreUnmapped(false); - } assertSerialization(testQuery, version); } } diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java index 9e5383a259ad..dff07e0f215e 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java @@ -24,7 +24,6 @@ import org.elasticsearch.Version; import org.elasticsearch.common.Randomness; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; @@ -33,7 +32,6 @@ import java.util.stream.IntStream; import static java.lang.Math.abs; -import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static org.apache.lucene.util.TestUtil.randomSimpleString; import static org.elasticsearch.common.unit.TimeValue.parseTimeValue; @@ -45,15 +43,6 @@ public void testBulkByTaskStatus() throws IOException { status.writeTo(out); BulkByScrollTask.Status tripped = new BulkByScrollTask.Status(out.bytes().streamInput()); assertTaskStatusEquals(out.getVersion(), status, tripped); - - // Also check round tripping pre-5.1 which is the first version to support parallelized scroll - out = new BytesStreamOutput(); - out.setVersion(Version.V_5_0_0_rc1); // This can be V_5_0_0 - status.writeTo(out); - StreamInput in = out.bytes().streamInput(); - in.setVersion(Version.V_5_0_0_rc1); - tripped = new BulkByScrollTask.Status(in); - assertTaskStatusEquals(Version.V_5_0_0_rc1, status, tripped); } /** @@ -74,23 +63,19 @@ public static void assertTaskStatusEquals(Version version, BulkByScrollTask.Stat assertEquals(expected.getRequestsPerSecond(), actual.getRequestsPerSecond(), 0f); assertEquals(expected.getReasonCancelled(), actual.getReasonCancelled()); assertEquals(expected.getThrottledUntil(), actual.getThrottledUntil()); - if (version.onOrAfter(Version.V_5_1_1)) { - assertThat(actual.getSliceStatuses(), Matchers.hasSize(expected.getSliceStatuses().size())); - for (int i = 0; i < expected.getSliceStatuses().size(); i++) { - BulkByScrollTask.StatusOrException sliceStatus = expected.getSliceStatuses().get(i); - if (sliceStatus == null) { - assertNull(actual.getSliceStatuses().get(i)); - } else if (sliceStatus.getException() == null) { - assertNull(actual.getSliceStatuses().get(i).getException()); - assertTaskStatusEquals(version, sliceStatus.getStatus(), actual.getSliceStatuses().get(i).getStatus()); - } else { - assertNull(actual.getSliceStatuses().get(i).getStatus()); - // Just check the message because we're not testing exception serialization in general here. - assertEquals(sliceStatus.getException().getMessage(), actual.getSliceStatuses().get(i).getException().getMessage()); - } + assertThat(actual.getSliceStatuses(), Matchers.hasSize(expected.getSliceStatuses().size())); + for (int i = 0; i < expected.getSliceStatuses().size(); i++) { + BulkByScrollTask.StatusOrException sliceStatus = expected.getSliceStatuses().get(i); + if (sliceStatus == null) { + assertNull(actual.getSliceStatuses().get(i)); + } else if (sliceStatus.getException() == null) { + assertNull(actual.getSliceStatuses().get(i).getException()); + assertTaskStatusEquals(version, sliceStatus.getStatus(), actual.getSliceStatuses().get(i).getStatus()); + } else { + assertNull(actual.getSliceStatuses().get(i).getStatus()); + // Just check the message because we're not testing exception serialization in general here. + assertEquals(sliceStatus.getException().getMessage(), actual.getSliceStatuses().get(i).getException().getMessage()); } - } else { - assertEquals(emptyList(), actual.getSliceStatuses()); } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java b/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java index 95772910747c..04d15d39b58e 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java @@ -77,48 +77,4 @@ public void testGetForUpdate() throws IOException { closeShards(primary); } - - public void testGetForUpdateWithParentField() throws IOException { - Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1) - .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put("index.version.created", Version.V_5_6_0) // for parent field mapper - .build(); - IndexMetaData metaData = IndexMetaData.builder("test") - .putMapping("test", "{ \"properties\": { \"foo\": { \"type\": \"text\"}}}") - .settings(settings) - .primaryTerm(0, 1).build(); - IndexShard primary = newShard(new ShardId(metaData.getIndex(), 0), true, "n1", metaData, null); - recoverShardFromStore(primary); - Engine.IndexResult test = indexDoc(primary, "test", "0", "{\"foo\" : \"bar\"}"); - assertTrue(primary.getEngine().refreshNeeded()); - GetResult testGet = primary.getService().getForUpdate("test", "0", test.getVersion(), VersionType.INTERNAL); - assertFalse(testGet.getFields().containsKey(RoutingFieldMapper.NAME)); - assertEquals(new String(testGet.source(), StandardCharsets.UTF_8), "{\"foo\" : \"bar\"}"); - try (Engine.Searcher searcher = primary.getEngine().acquireSearcher("test", Engine.SearcherScope.INTERNAL)) { - assertEquals(searcher.reader().maxDoc(), 1); // we refreshed - } - - Engine.IndexResult test1 = indexDoc(primary, "test", "1", "{\"foo\" : \"baz\"}", XContentType.JSON, null); - assertTrue(primary.getEngine().refreshNeeded()); - GetResult testGet1 = primary.getService().getForUpdate("test", "1", test1.getVersion(), VersionType.INTERNAL); - assertEquals(new String(testGet1.source(), StandardCharsets.UTF_8), "{\"foo\" : \"baz\"}"); - assertFalse(testGet1.getFields().containsKey(RoutingFieldMapper.NAME)); - try (Engine.Searcher searcher = primary.getEngine().acquireSearcher("test", Engine.SearcherScope.INTERNAL)) { - assertEquals(searcher.reader().maxDoc(), 1); // we read from the translog - } - primary.getEngine().refresh("test"); - try (Engine.Searcher searcher = primary.getEngine().acquireSearcher("test", Engine.SearcherScope.INTERNAL)) { - assertEquals(searcher.reader().maxDoc(), 2); - } - - // now again from the reader - test1 = indexDoc(primary, "test", "1", "{\"foo\" : \"baz\"}", XContentType.JSON, null); - assertTrue(primary.getEngine().refreshNeeded()); - testGet1 = primary.getService().getForUpdate("test", "1", test1.getVersion(), VersionType.INTERNAL); - assertEquals(new String(testGet1.source(), StandardCharsets.UTF_8), "{\"foo\" : \"baz\"}"); - assertFalse(testGet1.getFields().containsKey(RoutingFieldMapper.NAME)); - - closeShards(primary); - } } diff --git a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java index 47f30e10ef91..485fd9209963 100644 --- a/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java +++ b/server/src/test/java/org/elasticsearch/indices/analysis/AnalysisModuleTests.java @@ -136,7 +136,7 @@ public void testAnalyzerAliasNotAllowedPost5x() throws IOException { .put("index.analysis.analyzer.foobar.type", "standard") .put("index.analysis.analyzer.foobar.alias","foobaz") // analyzer aliases were removed in v5.0.0 alpha6 - .put(IndexMetaData.SETTING_VERSION_CREATED, VersionUtils.randomVersionBetween(random(), Version.V_5_0_0_beta1, null)) + .put(IndexMetaData.SETTING_VERSION_CREATED, VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, null)) .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) .build(); AnalysisRegistry registry = getNewRegistry(settings); @@ -149,7 +149,7 @@ public void testVersionedAnalyzers() throws Exception { Settings settings2 = Settings.builder() .loadFromStream(yaml, getClass().getResourceAsStream(yaml), false) .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_5_0_0) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_6_0_0) .build(); AnalysisRegistry newRegistry = getNewRegistry(settings2); IndexAnalyzers indexAnalyzers = getIndexAnalyzers(newRegistry, settings2); @@ -162,9 +162,9 @@ public void testVersionedAnalyzers() throws Exception { // analysis service has the expected version assertThat(indexAnalyzers.get("standard").analyzer(), is(instanceOf(StandardAnalyzer.class))); - assertEquals(Version.V_5_0_0.luceneVersion, + assertEquals(Version.V_6_0_0.luceneVersion, indexAnalyzers.get("standard").analyzer().getVersion()); - assertEquals(Version.V_5_0_0.luceneVersion, + assertEquals(Version.V_6_0_0.luceneVersion, indexAnalyzers.get("stop").analyzer().getVersion()); assertThat(indexAnalyzers.get("custom7").analyzer(), is(instanceOf(StandardAnalyzer.class))); diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index 5ed4b3703078..fa591411bba1 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -725,7 +725,7 @@ public void testMultiIndex() throws Exception { public void testFieldDataFieldsParam() throws Exception { assertAcked(client().admin().indices().prepareCreate("test1") - .setSettings(Settings.builder().put("index.version.created", Version.V_5_6_0.id)) + .setSettings(Settings.builder().put("index.version.created", Version.V_6_0_0.id)) .addMapping("_doc", "bar", "type=text,fielddata=true", "baz", "type=text,fielddata=true").get()); diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java index 5f1d1f612d7a..f6649853eda1 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java @@ -590,10 +590,10 @@ public void testNonExtensibleDep() throws Exception { } public void testIncompatibleElasticsearchVersion() throws Exception { - PluginInfo info = new PluginInfo("my_plugin", "desc", "1.0", Version.V_5_0_0, + PluginInfo info = new PluginInfo("my_plugin", "desc", "1.0", Version.V_6_0_0, "1.8", "FakePlugin", Collections.emptyList(), false); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginsService.verifyCompatibility(info)); - assertThat(e.getMessage(), containsString("was built for Elasticsearch version 5.0.0")); + assertThat(e.getMessage(), containsString("was built for Elasticsearch version 6.0.0")); } public void testIncompatibleJavaVersion() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java index c50fb89f334a..ce45d222dd75 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoDistanceIT.java @@ -67,7 +67,7 @@ protected Collection> nodePlugins() { return Arrays.asList(InternalSettingsPlugin.class); // uses index.version.created } - private Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + private Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); private IndexRequestBuilder indexCity(String idx, String name, String... latLons) throws Exception { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java index fc080dd0f04c..971742aec2d0 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/GeoHashGridIT.java @@ -65,7 +65,7 @@ protected Collection> nodePlugins() { return Arrays.asList(InternalSettingsPlugin.class); // uses index.version.created } - private Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + private Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); static ObjectIntMap expectedDocCountsForGeoHash = null; diff --git a/server/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java b/server/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java index a21893db3920..0a860a636d4a 100644 --- a/server/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java +++ b/server/src/test/java/org/elasticsearch/search/functionscore/DecayFunctionScoreIT.java @@ -613,7 +613,7 @@ public void testDateWithoutOrigin() throws Exception { } public void testManyDocsLin() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.CURRENT); + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = jsonBuilder().startObject().startObject("type").startObject("properties") .startObject("test").field("type", "text").endObject().startObject("date").field("type", "date") diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxIT.java b/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxIT.java index 12a64d80a148..80b40042801b 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxIT.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoBoundingBoxIT.java @@ -51,7 +51,7 @@ protected Collection> nodePlugins() { } public void testSimpleBoundingBoxTest() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") @@ -123,7 +123,7 @@ public void testSimpleBoundingBoxTest() throws Exception { } public void testLimit2BoundingBox() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") @@ -176,7 +176,7 @@ public void testLimit2BoundingBox() throws Exception { } public void testCompleteLonRange() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1") diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoDistanceIT.java b/server/src/test/java/org/elasticsearch/search/geo/GeoDistanceIT.java index 5966ea6a49dc..143fd611c3f5 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoDistanceIT.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoDistanceIT.java @@ -101,7 +101,7 @@ static Double distanceScript(Map vars, Function> nodePlugins() { @Override protected void setupSuiteScopeCluster() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); diff --git a/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceIT.java b/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceIT.java index 965dcb3e8ccf..e134b20c309f 100644 --- a/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceIT.java +++ b/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceIT.java @@ -59,7 +59,7 @@ protected Collection> nodePlugins() { } public void testDistanceSortingMVFields() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1").startObject("properties") @@ -189,7 +189,7 @@ public void testDistanceSortingMVFields() throws Exception { // Regression bug: // https://github.com/elastic/elasticsearch/issues/2851 public void testDistanceSortingWithMissingGeoPoint() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("type1").startObject("properties") @@ -234,7 +234,7 @@ public void testDistanceSortingWithMissingGeoPoint() throws Exception { } public void testDistanceSortingNestedFields() throws Exception { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject().startObject("company").startObject("properties") @@ -383,7 +383,7 @@ public void testDistanceSortingNestedFields() throws Exception { * Issue 3073 */ public void testGeoDistanceFilter() throws IOException { - Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); double lat = 40.720611; diff --git a/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java b/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java index 200043a6668a..cac5fede848a 100644 --- a/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java +++ b/server/src/test/java/org/elasticsearch/search/sort/GeoDistanceSortBuilderIT.java @@ -70,7 +70,7 @@ public void testManyToManyGeoPoints() throws ExecutionException, InterruptedExce * 1 2 3 4 5 6 7 */ Version version = randomBoolean() ? Version.CURRENT - : VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.CURRENT); + : VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); assertAcked(prepareCreate("index").setSettings(settings).addMapping("type", LOCATION_FIELD, "type=geo_point")); XContentBuilder d1Builder = jsonBuilder(); @@ -136,7 +136,7 @@ public void testSingeToManyAvgMedian() throws ExecutionException, InterruptedExc * d2 = (0, 1), (0, 5), (0, 6); so avg. distance is 4, median distance is 5 */ Version version = randomBoolean() ? Version.CURRENT - : VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.CURRENT); + : VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); assertAcked(prepareCreate("index").setSettings(settings).addMapping("type", LOCATION_FIELD, "type=geo_point")); XContentBuilder d1Builder = jsonBuilder(); @@ -197,7 +197,7 @@ public void testManyToManyGeoPointsWithDifferentFormats() throws ExecutionExcept * 1 2 3 4 5 6 */ Version version = randomBoolean() ? Version.CURRENT - : VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, Version.CURRENT); + : VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, version).build(); assertAcked(prepareCreate("index").setSettings(settings).addMapping("type", LOCATION_FIELD, "type=geo_point")); XContentBuilder d1Builder = jsonBuilder(); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java index f1929e72d8b3..84a6ce54d1ed 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java @@ -821,10 +821,5 @@ public void testGetNodePredicatesCombination() { allRoles, Version.CURRENT); assertTrue(nodePredicate.test(node)); } - { - DiscoveryNode node = new DiscoveryNode("id", address, Collections.singletonMap("gateway", "true"), - allRoles, Version.V_5_3_0); - assertFalse(nodePredicate.test(node)); - } } } diff --git a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java index 0b6112eb51c9..0bf12ba82c82 100644 --- a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java +++ b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java @@ -156,19 +156,26 @@ public void testEnsureVersionCompatibility() { TcpTransport.ensureVersionCompatibility(VersionUtils.randomVersionBetween(random(), Version.CURRENT.minimumCompatibilityVersion(), Version.CURRENT), Version.CURRENT, randomBoolean()); - TcpTransport.ensureVersionCompatibility(Version.fromString("5.0.0"), Version.fromString("6.0.0"), true); + TcpTransport.ensureVersionCompatibility(Version.fromString("6.0.0"), Version.fromString("7.0.0"), true); IllegalStateException ise = expectThrows(IllegalStateException.class, () -> - TcpTransport.ensureVersionCompatibility(Version.fromString("5.0.0"), Version.fromString("6.0.0"), false)); - assertEquals("Received message from unsupported version: [5.0.0] minimal compatible version is: [5.6.0]", ise.getMessage()); + TcpTransport.ensureVersionCompatibility(Version.fromString("6.0.0"), Version.fromString("7.0.0"), false)); + assertEquals("Received message from unsupported version: [6.0.0] minimal compatible version is: [6.5.0]", ise.getMessage()); + // For handshake we are compatible with N-2 + TcpTransport.ensureVersionCompatibility(Version.fromString("5.6.0"), Version.fromString("7.0.0"), true); ise = expectThrows(IllegalStateException.class, () -> - TcpTransport.ensureVersionCompatibility(Version.fromString("2.3.0"), Version.fromString("6.0.0"), true)); - assertEquals("Received handshake message from unsupported version: [2.3.0] minimal compatible version is: [5.6.0]", + TcpTransport.ensureVersionCompatibility(Version.fromString("5.6.0"), Version.fromString("7.0.0"), false)); + assertEquals("Received message from unsupported version: [5.6.0] minimal compatible version is: [6.5.0]", ise.getMessage()); ise = expectThrows(IllegalStateException.class, () -> - TcpTransport.ensureVersionCompatibility(Version.fromString("2.3.0"), Version.fromString("6.0.0"), false)); - assertEquals("Received message from unsupported version: [2.3.0] minimal compatible version is: [5.6.0]", + TcpTransport.ensureVersionCompatibility(Version.fromString("2.3.0"), Version.fromString("7.0.0"), true)); + assertEquals("Received handshake message from unsupported version: [2.3.0] minimal compatible version is: [6.5.0]", + ise.getMessage()); + + ise = expectThrows(IllegalStateException.class, () -> + TcpTransport.ensureVersionCompatibility(Version.fromString("2.3.0"), Version.fromString("7.0.0"), false)); + assertEquals("Received message from unsupported version: [2.3.0] minimal compatible version is: [6.5.0]", ise.getMessage()); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/OldIndexUtils.java b/test/framework/src/main/java/org/elasticsearch/test/OldIndexUtils.java index 4c4fe8f76ad8..b9a0e4a9b1ea 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/OldIndexUtils.java +++ b/test/framework/src/main/java/org/elasticsearch/test/OldIndexUtils.java @@ -75,27 +75,20 @@ public static Path getIndexDir( final String indexFile, final Path dataDir) throws IOException { final Version version = Version.fromString(indexName.substring("index-".length())); - if (version.before(Version.V_5_0_0_alpha1)) { - // the bwc scripts packs the indices under this path - Path src = dataDir.resolve("nodes/0/indices/" + indexName); - assertTrue("[" + indexFile + "] missing index dir: " + src.toString(), Files.exists(src)); - return src; - } else { - final List indexFolders = new ArrayList<>(); - try (DirectoryStream stream = Files.newDirectoryStream(dataDir.resolve("0/indices"), - (p) -> p.getFileName().toString().startsWith("extra") == false)) { // extra FS can break this... - for (final Path path : stream) { - indexFolders.add(path); - } + final List indexFolders = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(dataDir.resolve("0/indices"), + (p) -> p.getFileName().toString().startsWith("extra") == false)) { // extra FS can break this... + for (final Path path : stream) { + indexFolders.add(path); } - assertThat(indexFolders.toString(), indexFolders.size(), equalTo(1)); - final IndexMetaData indexMetaData = IndexMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY, - indexFolders.get(0)); - assertNotNull(indexMetaData); - assertThat(indexFolders.get(0).getFileName().toString(), equalTo(indexMetaData.getIndexUUID())); - assertThat(indexMetaData.getCreationVersion(), equalTo(version)); - return indexFolders.get(0); } + assertThat(indexFolders.toString(), indexFolders.size(), equalTo(1)); + final IndexMetaData indexMetaData = IndexMetaData.FORMAT.loadLatestState(logger, NamedXContentRegistry.EMPTY, + indexFolders.get(0)); + assertNotNull(indexMetaData); + assertThat(indexFolders.get(0).getFileName().toString(), equalTo(indexMetaData.getIndexUUID())); + assertThat(indexMetaData.getCreationVersion(), equalTo(version)); + return indexFolders.get(0); } // randomly distribute the files from src over dests paths diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSectionTests.java index 5da8601a9f34..500cff893cb1 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSectionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSectionTests.java @@ -152,7 +152,7 @@ public void testParseTestSectionWithDoSetAndSkipSectionsNoSkip() throws Exceptio parser = createParser(YamlXContent.yamlXContent, "\"First test section\": \n" + " - skip:\n" + - " version: \"5.0.0 - 5.2.0\"\n" + + " version: \"6.0.0 - 6.2.0\"\n" + " reason: \"Update doesn't return metadata fields, waiting for #3259\"\n" + " - do :\n" + " catch: missing\n" + @@ -167,9 +167,9 @@ public void testParseTestSectionWithDoSetAndSkipSectionsNoSkip() throws Exceptio assertThat(testSection, notNullValue()); assertThat(testSection.getName(), equalTo("First test section")); assertThat(testSection.getSkipSection(), notNullValue()); - assertThat(testSection.getSkipSection().getLowerVersion(), equalTo(Version.V_5_0_0)); + assertThat(testSection.getSkipSection().getLowerVersion(), equalTo(Version.V_6_0_0)); assertThat(testSection.getSkipSection().getUpperVersion(), - equalTo(Version.V_5_2_0)); + equalTo(Version.V_6_2_0)); assertThat(testSection.getSkipSection().getReason(), equalTo("Update doesn't return metadata fields, waiting for #3259")); assertThat(testSection.getExecutableSections().size(), equalTo(2)); DoSection doSection = (DoSection)testSection.getExecutableSections().get(0); diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java index 4c97eb453610..71814593ad48 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java @@ -66,10 +66,10 @@ public void testParseTestSetupTeardownAndSections() throws Exception { " - match: {test_index.test_type.properties.text.analyzer: whitespace}\n" + "\n" + "---\n" + - "\"Get type mapping - pre 5.0\":\n" + + "\"Get type mapping - pre 6.0\":\n" + "\n" + " - skip:\n" + - " version: \"5.0.0 - \"\n" + + " version: \"6.0.0 - \"\n" + " reason: \"for newer versions the index name is always returned\"\n" + "\n" + " - do:\n" + @@ -97,7 +97,7 @@ public void testParseTestSetupTeardownAndSections() throws Exception { } else { assertThat(restTestSuite.getSetupSection().isEmpty(), equalTo(true)); } - + assertThat(restTestSuite.getTeardownSection(), notNullValue()); if (includeTeardown) { assertThat(restTestSuite.getTeardownSection().isEmpty(), equalTo(false)); @@ -131,12 +131,12 @@ public void testParseTestSetupTeardownAndSections() throws Exception { assertThat(matchAssertion.getExpectedValue().toString(), equalTo("whitespace")); assertThat(restTestSuite.getTestSections().get(1).getName(), - equalTo("Get type mapping - pre 5.0")); + equalTo("Get type mapping - pre 6.0")); assertThat(restTestSuite.getTestSections().get(1).getSkipSection().isEmpty(), equalTo(false)); assertThat(restTestSuite.getTestSections().get(1).getSkipSection().getReason(), equalTo("for newer versions the index name is always returned")); assertThat(restTestSuite.getTestSections().get(1).getSkipSection().getLowerVersion(), - equalTo(Version.V_5_0_0)); + equalTo(Version.V_6_0_0)); assertThat(restTestSuite.getTestSections().get(1).getSkipSection().getUpperVersion(), equalTo(Version.CURRENT)); assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().size(), equalTo(3)); assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().get(0), instanceOf(DoSection.class)); diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SetupSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SetupSectionTests.java index cb9ab009b259..e883e8e062af 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SetupSectionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SetupSectionTests.java @@ -53,7 +53,7 @@ public void testParseSetupSection() throws Exception { public void testParseSetupAndSkipSectionNoSkip() throws Exception { parser = createParser(YamlXContent.yamlXContent, " - skip:\n" + - " version: \"5.0.0 - 5.3.0\"\n" + + " version: \"6.0.0 - 6.3.0\"\n" + " reason: \"Update doesn't return metadata fields, waiting for #3259\"\n" + " - do:\n" + " index1:\n" + @@ -74,9 +74,9 @@ public void testParseSetupAndSkipSectionNoSkip() throws Exception { assertThat(setupSection, notNullValue()); assertThat(setupSection.getSkipSection().isEmpty(), equalTo(false)); assertThat(setupSection.getSkipSection(), notNullValue()); - assertThat(setupSection.getSkipSection().getLowerVersion(), equalTo(Version.V_5_0_0)); + assertThat(setupSection.getSkipSection().getLowerVersion(), equalTo(Version.V_6_0_0)); assertThat(setupSection.getSkipSection().getUpperVersion(), - equalTo(Version.V_5_3_0)); + equalTo(Version.V_6_3_0)); assertThat(setupSection.getSkipSection().getReason(), equalTo("Update doesn't return metadata fields, waiting for #3259")); assertThat(setupSection.getDoSections().size(), equalTo(2)); assertThat(setupSection.getDoSections().get(0).getApiCallSection().getApi(), equalTo("index1")); diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java index 3ab9583335e7..e5e466a82cc1 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/SkipSectionTests.java @@ -34,17 +34,17 @@ public class SkipSectionTests extends AbstractClientYamlTestFragmentParserTestCase { public void testSkip() { - SkipSection section = new SkipSection("5.0.0 - 5.1.0", + SkipSection section = new SkipSection("6.0.0 - 6.1.0", randomBoolean() ? Collections.emptyList() : Collections.singletonList("warnings"), "foobar"); assertFalse(section.skip(Version.CURRENT)); - assertTrue(section.skip(Version.V_5_0_0)); - section = new SkipSection(randomBoolean() ? null : "5.0.0 - 5.1.0", + assertTrue(section.skip(Version.V_6_0_0)); + section = new SkipSection(randomBoolean() ? null : "6.0.0 - 6.1.0", Collections.singletonList("boom"), "foobar"); assertTrue(section.skip(Version.CURRENT)); } public void testMessage() { - SkipSection section = new SkipSection("5.0.0 - 5.1.0", + SkipSection section = new SkipSection("6.0.0 - 6.1.0", Collections.singletonList("warnings"), "foobar"); assertEquals("[FOOBAR] skipped, reason: [foobar] unsupported features [warnings]", section.getSkipMessage("FOOBAR")); section = new SkipSection(null, Collections.singletonList("warnings"), "foobar"); @@ -55,14 +55,14 @@ public void testMessage() { public void testParseSkipSectionVersionNoFeature() throws Exception { parser = createParser(YamlXContent.yamlXContent, - "version: \" - 5.1.1\"\n" + + "version: \" - 6.1.1\"\n" + "reason: Delete ignores the parent param" ); SkipSection skipSection = SkipSection.parse(parser); assertThat(skipSection, notNullValue()); assertThat(skipSection.getLowerVersion(), equalTo(VersionUtils.getFirstVersion())); - assertThat(skipSection.getUpperVersion(), equalTo(Version.V_5_1_1)); + assertThat(skipSection.getUpperVersion(), equalTo(Version.V_6_1_1)); assertThat(skipSection.getFeatures().size(), equalTo(0)); assertThat(skipSection.getReason(), equalTo("Delete ignores the parent param")); } diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TeardownSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TeardownSectionTests.java index 15ca1ec0096e..07afa9f33b5b 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TeardownSectionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TeardownSectionTests.java @@ -56,7 +56,7 @@ public void testParseTeardownSection() throws Exception { public void testParseWithSkip() throws Exception { parser = createParser(YamlXContent.yamlXContent, " - skip:\n" + - " version: \"5.0.0 - 5.3.0\"\n" + + " version: \"6.0.0 - 6.3.0\"\n" + " reason: \"there is a reason\"\n" + " - do:\n" + " delete:\n" + @@ -75,8 +75,8 @@ public void testParseWithSkip() throws Exception { TeardownSection section = TeardownSection.parse(parser); assertThat(section, notNullValue()); assertThat(section.getSkipSection().isEmpty(), equalTo(false)); - assertThat(section.getSkipSection().getLowerVersion(), equalTo(Version.V_5_0_0)); - assertThat(section.getSkipSection().getUpperVersion(), equalTo(Version.V_5_3_0)); + assertThat(section.getSkipSection().getLowerVersion(), equalTo(Version.V_6_0_0)); + assertThat(section.getSkipSection().getUpperVersion(), equalTo(Version.V_6_3_0)); assertThat(section.getSkipSection().getReason(), equalTo("there is a reason")); assertThat(section.getDoSections().size(), equalTo(2)); assertThat(section.getDoSections().get(0).getApiCallSection().getApi(), equalTo("delete")); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java index 2a7eddcf3539..b51a451a67fa 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.protocol.xpack; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; @@ -399,8 +398,7 @@ public FeatureSet(String name, @Nullable String description, boolean available, } public FeatureSet(StreamInput in) throws IOException { - this(in.readString(), in.readOptionalString(), in.readBoolean(), in.readBoolean(), - in.getVersion().onOrAfter(Version.V_5_4_0) ? in.readMap() : null); + this(in.readString(), in.readOptionalString(), in.readBoolean(), in.readBoolean(), in.readMap()); } @Override @@ -409,9 +407,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(description); out.writeBoolean(available); out.writeBoolean(enabled); - if (out.getVersion().onOrAfter(Version.V_5_4_0)) { - out.writeMap(nativeCodeInfo); - } + out.writeMap(nativeCodeInfo); } public String name() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java index e5b116a3a7a9..16ed33ae9408 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.protocol.xpack.security; -import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; @@ -185,12 +184,7 @@ public static User partialReadFrom(String username, StreamInput input) throws IO boolean hasInnerUser = input.readBoolean(); if (hasInnerUser) { User innerUser = readFrom(input); - if (input.getVersion().onOrBefore(Version.V_5_4_0)) { - // backcompat: runas user was read first, so reverse outer and inner - return new User(innerUser, outerUser); - } else { - return new User(outerUser, innerUser); - } + return new User(outerUser, innerUser); } else { return outerUser; } @@ -207,11 +201,6 @@ public static void writeTo(User user, StreamOutput output) throws IOException { if (user.authenticatedUser == null) { // no backcompat necessary, since there is no inner user writeUser(user, output); - } else if (output.getVersion().onOrBefore(Version.V_5_4_0)) { - // backcompat: write runas user as the "inner" user - writeUser(user.authenticatedUser, output); - output.writeBoolean(true); - writeUser(user, output); } else { writeUser(user, output); output.writeBoolean(true); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java index e0b71abe966d..193695ac6936 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlMetadata.java @@ -115,7 +115,7 @@ public Set expandDatafeedIds(String expression, boolean allowNoDatafeeds @Override public Version getMinimalSupportedVersion() { - return Version.V_5_4_0; + return Version.V_6_0_0_alpha1; } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDatafeedAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDatafeedAction.java index fb3ac55cda02..73cdbeef4425 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDatafeedAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteDatafeedAction.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.action; -import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.AcknowledgedRequest; @@ -72,18 +71,14 @@ public ActionRequestValidationException validate() { public void readFrom(StreamInput in) throws IOException { super.readFrom(in); datafeedId = in.readString(); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - force = in.readBoolean(); - } + force = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeString(datafeedId); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeBoolean(force); - } + out.writeBoolean(force); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteJobAction.java index 933e98b80ff8..56b7ec2b52fc 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/DeleteJobAction.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.action; -import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.AcknowledgedRequest; @@ -79,18 +78,14 @@ public Task createTask(long id, String type, String action, TaskId parentTaskId, public void readFrom(StreamInput in) throws IOException { super.readFrom(in); jobId = in.readString(); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - force = in.readBoolean(); - } + force = in.readBoolean(); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeString(jobId); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeBoolean(force); - } + out.writeBoolean(force); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java index ef086b512622..4b96a4d6b274 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.action; -import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.support.tasks.BaseTasksResponse; @@ -127,9 +126,7 @@ public void readFrom(StreamInput in) throws IOException { start = in.readOptionalString(); end = in.readOptionalString(); advanceTime = in.readOptionalString(); - if (in.getVersion().after(Version.V_5_5_0)) { - skipTime = in.readOptionalString(); - } + skipTime = in.readOptionalString(); } @Override @@ -139,9 +136,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(start); out.writeOptionalString(end); out.writeOptionalString(advanceTime); - if (out.getVersion().after(Version.V_5_5_0)) { - out.writeOptionalString(skipTime); - } + out.writeOptionalString(skipTime); } @Override @@ -222,18 +217,14 @@ public Date getLastFinalizedBucketEnd() { public void readFrom(StreamInput in) throws IOException { super.readFrom(in); flushed = in.readBoolean(); - if (in.getVersion().after(Version.V_5_5_0)) { - lastFinalizedBucketEnd = new Date(in.readVLong()); - } + lastFinalizedBucketEnd = new Date(in.readVLong()); } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeBoolean(flushed); - if (out.getVersion().after(Version.V_5_5_0)) { - out.writeVLong(lastFinalizedBucketEnd.getTime()); - } + out.writeVLong(lastFinalizedBucketEnd.getTime()); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetBucketsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetBucketsAction.java index 29b3d4bb8d55..c6c87ef0e465 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetBucketsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetBucketsAction.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.action; -import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestBuilder; @@ -162,7 +161,7 @@ public PageParams getPageParams() { public void setPageParams(PageParams pageParams) { if (timestamp != null) { - throw new IllegalArgumentException("Param [" + PageParams.FROM.getPreferredName() + throw new IllegalArgumentException("Param [" + PageParams.FROM.getPreferredName() + ", " + PageParams.SIZE.getPreferredName() + "] is incompatible with [" + TIMESTAMP.getPreferredName() + "]."); } this.pageParams = ExceptionsHelper.requireNonNull(pageParams, PageParams.PAGE.getPreferredName()); @@ -212,10 +211,8 @@ public void readFrom(StreamInput in) throws IOException { end = in.readOptionalString(); anomalyScore = in.readOptionalDouble(); pageParams = in.readOptionalWriteable(PageParams::new); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - sort = in.readString(); - descending = in.readBoolean(); - } + sort = in.readString(); + descending = in.readBoolean(); } @Override @@ -229,10 +226,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(end); out.writeOptionalDouble(anomalyScore); out.writeOptionalWriteable(pageParams); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeString(sort); - out.writeBoolean(descending); - } + out.writeString(sort); + out.writeBoolean(descending); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java index c108a983aa17..fc38d974deff 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/OpenJobAction.java @@ -168,10 +168,6 @@ public JobParams(String jobId) { public JobParams(StreamInput in) throws IOException { jobId = in.readString(); - if (in.getVersion().onOrBefore(Version.V_5_5_0)) { - // Read `ignoreDowntime` - in.readBoolean(); - } timeout = TimeValue.timeValueMillis(in.readVLong()); } @@ -199,10 +195,6 @@ public String getWriteableName() { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(jobId); - if (out.getVersion().onOrBefore(Version.V_5_5_0)) { - // Write `ignoreDowntime` - true by default - out.writeBoolean(true); - } out.writeVLong(timeout.millis()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java index 1034b00af0a3..cdf25438cea3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java @@ -189,10 +189,6 @@ public DatafeedConfig(StreamInput in) throws IOException { this.scriptFields = null; } this.scrollSize = in.readOptionalVInt(); - if (in.getVersion().before(Version.V_5_5_0)) { - // read former _source field - in.readBoolean(); - } this.chunkingConfig = in.readOptionalWriteable(ChunkingConfig::new); if (in.getVersion().onOrAfter(Version.V_6_2_0)) { this.headers = Collections.unmodifiableMap(in.readMap(StreamInput::readString, StreamInput::readString)); @@ -290,10 +286,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(false); } out.writeOptionalVInt(scrollSize); - if (out.getVersion().before(Version.V_5_5_0)) { - // write former _source field - out.writeBoolean(false); - } out.writeOptionalWriteable(chunkingConfig); if (out.getVersion().onOrAfter(Version.V_6_2_0)) { out.writeMap(headers, StreamOutput::writeString, StreamOutput::writeString); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedState.java index d894f7b339fe..70102f27a566 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedState.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.datafeed; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -49,14 +48,6 @@ public String getWriteableName() { @Override public void writeTo(StreamOutput out) throws IOException { DatafeedState state = this; - // STARTING & STOPPING states were introduced in v5.5. - if (out.getVersion().before(Version.V_5_5_0)) { - if (this == STARTING) { - state = STOPPED; - } else if (this == STOPPING) { - state = STARTED; - } - } out.writeEnum(state); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java index f3748cefc51b..d5425bdd1f46 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.datafeed; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -122,10 +121,6 @@ public DatafeedUpdate(StreamInput in) throws IOException { this.scriptFields = null; } this.scrollSize = in.readOptionalVInt(); - if (in.getVersion().before(Version.V_5_5_0)) { - // TODO for former _source param - remove in v7.0.0 - in.readOptionalBoolean(); - } this.chunkingConfig = in.readOptionalWriteable(ChunkingConfig::new); } @@ -163,10 +158,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(false); } out.writeOptionalVInt(scrollSize); - if (out.getVersion().before(Version.V_5_5_0)) { - // TODO for former _source param - remove in v7.0.0 - out.writeOptionalBoolean(null); - } out.writeOptionalWriteable(chunkingConfig); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Detector.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Detector.java index 93aa5495c409..b5083aeecb9a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Detector.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Detector.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.core.ml.job.config; import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -248,12 +247,7 @@ public Detector(StreamInput in) throws IOException { useNull = in.readBoolean(); excludeFrequent = in.readBoolean() ? ExcludeFrequent.readFromStream(in) : null; rules = Collections.unmodifiableList(in.readList(DetectionRule::new)); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - detectorIndex = in.readInt(); - } else { - // negative means unknown, and is expected for 5.4 jobs - detectorIndex = -1; - } + detectorIndex = in.readInt(); } @Override @@ -276,9 +270,7 @@ public void writeTo(StreamOutput out) throws IOException { } else { out.writeList(Collections.emptyList()); } - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeInt(detectorIndex); - } + out.writeInt(detectorIndex); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Job.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Job.java index 0005d16a99c9..a978612fd02e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Job.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/Job.java @@ -214,11 +214,7 @@ private Job(String jobId, String jobType, Version jobVersion, List group public Job(StreamInput in) throws IOException { jobId = in.readString(); jobType = in.readString(); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - jobVersion = in.readBoolean() ? Version.readVersion(in) : null; - } else { - jobVersion = null; - } + jobVersion = in.readBoolean() ? Version.readVersion(in) : null; if (in.getVersion().onOrAfter(Version.V_6_1_0)) { groups = Collections.unmodifiableList(in.readList(StreamInput::readString)); } else { @@ -482,13 +478,11 @@ public long earliestValidTimestamp(DataCounts dataCounts) { public void writeTo(StreamOutput out) throws IOException { out.writeString(jobId); out.writeString(jobType); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - if (jobVersion != null) { - out.writeBoolean(true); - Version.writeVersion(jobVersion, out); - } else { - out.writeBoolean(false); - } + if (jobVersion != null) { + out.writeBoolean(true); + Version.writeVersion(jobVersion, out); + } else { + out.writeBoolean(false); } if (out.getVersion().onOrAfter(Version.V_6_1_0)) { out.writeStringList(groups); @@ -666,9 +660,7 @@ private static void checkValueNotLessThan(long minVal, String name, Long value) */ public static Set getCompatibleJobTypes(Version nodeVersion) { Set compatibleTypes = new HashSet<>(); - if (nodeVersion.onOrAfter(Version.V_5_4_0)) { - compatibleTypes.add(ANOMALY_DETECTOR_JOB_TYPE); - } + compatibleTypes.add(ANOMALY_DETECTOR_JOB_TYPE); return compatibleTypes; } @@ -732,9 +724,7 @@ public Builder(Job job) { public Builder(StreamInput in) throws IOException { id = in.readOptionalString(); jobType = in.readString(); - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - jobVersion = in.readBoolean() ? Version.readVersion(in) : null; - } + jobVersion = in.readBoolean() ? Version.readVersion(in) : null; if (in.getVersion().onOrAfter(Version.V_6_1_0)) { groups = in.readList(StreamInput::readString); } else { @@ -921,13 +911,11 @@ public List invalidCreateTimeSettings() { public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(id); out.writeString(jobType); - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - if (jobVersion != null) { - out.writeBoolean(true); - Version.writeVersion(jobVersion, out); - } else { - out.writeBoolean(false); - } + if (jobVersion != null) { + out.writeBoolean(true); + Version.writeVersion(jobVersion, out); + } else { + out.writeBoolean(false); } if (out.getVersion().onOrAfter(Version.V_6_1_0)) { out.writeStringList(groups); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobState.java index e89149a062b6..948284d5e008 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/config/JobState.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.job.config; -import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -34,10 +33,6 @@ public static JobState fromStream(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { JobState state = this; - // Pre v5.5 the OPENING state didn't exist - if (this == OPENING && out.getVersion().before(Version.V_5_5_0)) { - state = CLOSED; - } out.writeEnum(state); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/output/FlushAcknowledgement.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/output/FlushAcknowledgement.java index ad8b24e66c64..2d9afa833c3c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/output/FlushAcknowledgement.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/output/FlushAcknowledgement.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.job.process.autodetect.output; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -58,17 +57,13 @@ public FlushAcknowledgement(String id, Date lastFinalizedBucketEnd) { public FlushAcknowledgement(StreamInput in) throws IOException { id = in.readString(); - if (in.getVersion().after(Version.V_5_5_0)) { - lastFinalizedBucketEnd = new Date(in.readVLong()); - } + lastFinalizedBucketEnd = new Date(in.readVLong()); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(id); - if (out.getVersion().after(Version.V_5_5_0)) { - out.writeVLong(lastFinalizedBucketEnd.getTime()); - } + out.writeVLong(lastFinalizedBucketEnd.getTime()); } public String getId() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelSnapshot.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelSnapshot.java index 03487500d8a8..068b998dc251 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelSnapshot.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelSnapshot.java @@ -143,7 +143,7 @@ public ModelSnapshot(StreamInput in) throws IOException { if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { minVersion = Version.readVersion(in); } else { - minVersion = Version.V_5_5_0; + minVersion = Version.CURRENT.minimumCompatibilityVersion(); } timestamp = in.readBoolean() ? new Date(in.readVLong()) : null; description = in.readOptionalString(); @@ -357,9 +357,8 @@ public static class Builder { private String jobId; // Stored snapshot documents created prior to 6.3.0 will have no - // value for min_version. We default it to 5.5.0 as there were - // no model changes between 5.5.0 and 6.3.0. - private Version minVersion = Version.V_5_5_0; + // value for min_version. + private Version minVersion = Version.V_6_3_0; private Date timestamp; private String description; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java index 360bcfaaeadf..869cdcb437e1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.job.results; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -163,10 +162,6 @@ public AnomalyRecord(String jobId, Date timestamp, long bucketSpan) { @SuppressWarnings("unchecked") public AnomalyRecord(StreamInput in) throws IOException { jobId = in.readString(); - // bwc for removed sequenceNum field - if (in.getVersion().before(Version.V_5_5_0)) { - in.readInt(); - } detectorIndex = in.readInt(); probability = in.readDouble(); byFieldName = in.readOptionalString(); @@ -201,10 +196,6 @@ public AnomalyRecord(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(jobId); - // bwc for removed sequenceNum field - if (out.getVersion().before(Version.V_5_5_0)) { - out.writeInt(0); - } out.writeInt(detectorIndex); out.writeDouble(probability); out.writeOptionalString(byFieldName); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Bucket.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Bucket.java index 8a7fe2395b4e..8280ee9f22ef 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Bucket.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Bucket.java @@ -137,19 +137,11 @@ public Bucket(StreamInput in) throws IOException { anomalyScore = in.readDouble(); bucketSpan = in.readLong(); initialAnomalyScore = in.readDouble(); - // bwc for recordCount - if (in.getVersion().before(Version.V_5_5_0)) { - in.readInt(); - } records = in.readList(AnomalyRecord::new); eventCount = in.readLong(); isInterim = in.readBoolean(); bucketInfluencers = in.readList(BucketInfluencer::new); processingTimeMs = in.readLong(); - // bwc for perPartitionMaxProbability - if (in.getVersion().before(Version.V_5_5_0)) { - in.readGenericValue(); - } // bwc for perPartitionNormalization if (in.getVersion().before(Version.V_6_5_0)) { in.readList(Bucket::readOldPerPartitionNormalization); @@ -171,19 +163,11 @@ public void writeTo(StreamOutput out) throws IOException { out.writeDouble(anomalyScore); out.writeLong(bucketSpan); out.writeDouble(initialAnomalyScore); - // bwc for recordCount - if (out.getVersion().before(Version.V_5_5_0)) { - out.writeInt(0); - } out.writeList(records); out.writeLong(eventCount); out.writeBoolean(isInterim); out.writeList(bucketInfluencers); out.writeLong(processingTimeMs); - // bwc for perPartitionMaxProbability - if (out.getVersion().before(Version.V_5_5_0)) { - out.writeGenericValue(Collections.emptyMap()); - } // bwc for perPartitionNormalization if (out.getVersion().before(Version.V_6_5_0)) { out.writeList(Collections.emptyList()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/BucketInfluencer.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/BucketInfluencer.java index 8b18562ec6d1..38d76789a2ea 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/BucketInfluencer.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/BucketInfluencer.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.job.results; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -100,10 +99,6 @@ public BucketInfluencer(StreamInput in) throws IOException { isInterim = in.readBoolean(); timestamp = new Date(in.readLong()); bucketSpan = in.readLong(); - // bwc for removed sequenceNum field - if (in.getVersion().before(Version.V_5_5_0)) { - in.readInt(); - } } @Override @@ -117,10 +112,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(isInterim); out.writeLong(timestamp.getTime()); out.writeLong(bucketSpan); - // bwc for removed sequenceNum field - if (out.getVersion().before(Version.V_5_5_0)) { - out.writeInt(0); - } } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Influencer.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Influencer.java index 97ed643c44dd..8ee49cb88d05 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Influencer.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/Influencer.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.ml.job.results; -import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -97,10 +96,6 @@ public Influencer(StreamInput in) throws IOException { influencerScore = in.readDouble(); isInterim = in.readBoolean(); bucketSpan = in.readLong(); - // bwc for removed sequenceNum field - if (in.getVersion().before(Version.V_5_5_0)) { - in.readInt(); - } } @Override @@ -114,10 +109,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeDouble(influencerScore); out.writeBoolean(isInterim); out.writeLong(bucketSpan); - // bwc for removed sequenceNum field - if (out.getVersion().before(Version.V_5_5_0)) { - out.writeInt(0); - } } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ModelPlot.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ModelPlot.java index c331d8b04379..9f066b6e98ec 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ModelPlot.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ModelPlot.java @@ -109,20 +109,7 @@ public ModelPlot(String jobId, Date timestamp, long bucketSpan, int detectorInde public ModelPlot(StreamInput in) throws IOException { jobId = in.readString(); - // timestamp isn't optional in v5.5 - if (in.getVersion().before(Version.V_5_5_0)) { - if (in.readBoolean()) { - timestamp = new Date(in.readLong()); - } else { - timestamp = new Date(); - } - } else { - timestamp = new Date(in.readLong()); - } - // bwc for removed id field - if (in.getVersion().before(Version.V_5_5_0)) { - in.readOptionalString(); - } + timestamp = new Date(in.readLong()); partitionFieldName = in.readOptionalString(); partitionFieldValue = in.readOptionalString(); overFieldName = in.readOptionalString(); @@ -138,11 +125,7 @@ public ModelPlot(StreamInput in) throws IOException { } else { actual = in.readOptionalDouble(); } - if (in.getVersion().onOrAfter(Version.V_5_5_0)) { - bucketSpan = in.readLong(); - } else { - bucketSpan = 0; - } + bucketSpan = in.readLong(); if (in.getVersion().onOrAfter(Version.V_6_1_0)) { detectorIndex = in.readInt(); } else { @@ -154,20 +137,7 @@ public ModelPlot(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(jobId); - // timestamp isn't optional in v5.5 - if (out.getVersion().before(Version.V_5_5_0)) { - boolean hasTimestamp = timestamp != null; - out.writeBoolean(hasTimestamp); - if (hasTimestamp) { - out.writeLong(timestamp.getTime()); - } - } else { - out.writeLong(timestamp.getTime()); - } - // bwc for removed id field - if (out.getVersion().before(Version.V_5_5_0)) { - out.writeOptionalString(null); - } + out.writeLong(timestamp.getTime()); out.writeOptionalString(partitionFieldName); out.writeOptionalString(partitionFieldValue); out.writeOptionalString(overFieldName); @@ -189,9 +159,7 @@ public void writeTo(StreamOutput out) throws IOException { } else { out.writeOptionalDouble(actual); } - if (out.getVersion().onOrAfter(Version.V_5_5_0)) { - out.writeLong(bucketSpan); - } + out.writeLong(bucketSpan); if (out.getVersion().onOrAfter(Version.V_6_1_0)) { out.writeInt(detectorIndex); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 38bd84888a88..69712a6f33de 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -243,12 +243,7 @@ public static RoleDescriptor readFrom(StreamInput in) throws IOException { String[] runAs = in.readStringArray(); Map metadata = in.readMap(); - final Map transientMetadata; - if (in.getVersion().onOrAfter(Version.V_5_2_0)) { - transientMetadata = in.readMap(); - } else { - transientMetadata = Collections.emptyMap(); - } + final Map transientMetadata = in.readMap(); final ApplicationResourcePrivileges[] applicationPrivileges; final ConditionalClusterPrivilege[] conditionalClusterPrivileges; @@ -273,9 +268,7 @@ public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws I } out.writeStringArray(descriptor.runAs); out.writeMap(descriptor.metadata); - if (out.getVersion().onOrAfter(Version.V_5_2_0)) { - out.writeMap(descriptor.transientMetadata); - } + out.writeMap(descriptor.transientMetadata); if (out.getVersion().onOrAfter(Version.V_6_4_0)) { out.writeArray(ApplicationResourcePrivileges::write, descriptor.applicationPrivileges); ConditionalClusterPrivileges.writeArray(out, descriptor.getConditionalClusterPrivileges()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java index 047758177fb0..71e43ff5a30f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.security.user; -import org.elasticsearch.Version; import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.support.MetadataUtils; @@ -16,8 +15,6 @@ public class LogstashSystemUser extends User { public static final String NAME = UsernamesField.LOGSTASH_NAME; public static final String ROLE_NAME = UsernamesField.LOGSTASH_ROLE; - public static final Version DEFINED_SINCE = Version.V_5_2_0; - public static final BuiltinUserInfo USER_INFO = new BuiltinUserInfo(NAME, ROLE_NAME, DEFINED_SINCE); public LogstashSystemUser(boolean enabled) { super(NAME, new String[]{ ROLE_NAME }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA, enabled); 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 bb21ddbd1a13..c2cb5af13053 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 @@ -229,7 +229,7 @@ public void testNewTrialDefaultsSecurityOff() { public void testOldTrialDefaultsSecurityOn() { XPackLicenseState licenseState = new XPackLicenseState(Settings.EMPTY); - licenseState.update(TRIAL, true, rarely() ? null : VersionUtils.randomVersionBetween(random(), Version.V_5_6_0, Version.V_6_2_4)); + 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.isAuthAllowed(), is(true)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobTests.java index 88d9b07816d4..7e53478533eb 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/config/JobTests.java @@ -39,7 +39,6 @@ import java.util.Map; import java.util.Set; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -479,19 +478,6 @@ public void testBuilder_givenTimeFieldInAnalysisConfig() { assertThat(e.getMessage(), equalTo(Messages.getMessage(Messages.JOB_CONFIG_TIME_FIELD_NOT_ALLOWED_IN_ANALYSIS_CONFIG))); } - public void testGetCompatibleJobTypes_givenVersionBefore_V_5_4() { - assertThat(Job.getCompatibleJobTypes(Version.V_5_0_0).isEmpty(), is(true)); - assertThat(Job.getCompatibleJobTypes(Version.V_5_3_0).isEmpty(), is(true)); - assertThat(Job.getCompatibleJobTypes(Version.V_5_3_2).isEmpty(), is(true)); - } - - public void testGetCompatibleJobTypes_givenVersionAfter_V_5_4() { - assertThat(Job.getCompatibleJobTypes(Version.V_5_4_0), contains(Job.ANOMALY_DETECTOR_JOB_TYPE)); - assertThat(Job.getCompatibleJobTypes(Version.V_5_4_0).size(), equalTo(1)); - assertThat(Job.getCompatibleJobTypes(Version.V_5_5_0), contains(Job.ANOMALY_DETECTOR_JOB_TYPE)); - assertThat(Job.getCompatibleJobTypes(Version.V_5_5_0).size(), equalTo(1)); - } - public void testInvalidCreateTimeSettings() { Job.Builder builder = new Job.Builder("invalid-settings"); builder.setModelSnapshotId("snapshot-foo"); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java index a2b8d40e44c0..a68a522f0242 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/role/PutRoleRequestTests.java @@ -78,7 +78,7 @@ public void testSerializationV63AndBefore() throws IOException { final PutRoleRequest original = buildRandomRequest(); final BytesStreamOutput out = new BytesStreamOutput(); - final Version version = VersionUtils.randomVersionBetween(random(), Version.V_5_6_0, Version.V_6_3_2); + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.V_6_3_2); out.setVersion(version); original.writeTo(out); diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java index 0f54784a33f4..d496eea2f0d1 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java @@ -7,10 +7,7 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.deprecation.DeprecationInfoAction; @@ -23,153 +20,9 @@ import static org.elasticsearch.xpack.deprecation.DeprecationChecks.INDEX_SETTINGS_CHECKS; public class IndexDeprecationChecksTests extends ESTestCase { - - private static void assertSettingsAndIssue(String key, String value, DeprecationIssue expected) { - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .settings(settings(Version.V_5_6_0) - .put(key, value)) - .numberOfShards(1) - .numberOfReplicas(0) - .build(); - List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); - assertEquals(singletonList(expected), issues); - } - - public void testCoerceBooleanDeprecation() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder(); - mapping.startObject(); { - mapping.startObject("properties"); { - mapping.startObject("my_boolean"); { - mapping.field("type", "boolean"); - } - mapping.endObject(); - mapping.startObject("my_object"); { - mapping.startObject("properties"); { - mapping.startObject("my_inner_boolean"); { - mapping.field("type", "boolean"); - } - mapping.endObject(); - mapping.startObject("my_text"); { - mapping.field("type", "text"); - mapping.startObject("fields"); { - mapping.startObject("raw"); { - mapping.field("type", "boolean"); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .putMapping("testBooleanCoercion", Strings.toString(mapping)) - .settings(settings(Version.V_5_6_0)) - .numberOfShards(1) - .numberOfReplicas(0) - .build(); - - DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.INFO, - "Coercion of boolean fields", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + - "breaking_60_mappings_changes.html#_coercion_of_boolean_fields", - "[[type: testBooleanCoercion, field: my_boolean], [type: testBooleanCoercion, field: my_inner_boolean]," + - " [type: testBooleanCoercion, field: my_text, multifield: raw]]"); - List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); - assertEquals(singletonList(expected), issues); - } - - public void testMatchMappingTypeCheck() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder(); - mapping.startObject(); { - mapping.startArray("dynamic_templates"); - { - mapping.startObject(); - { - mapping.startObject("integers"); - { - mapping.field("match_mapping_type", "UNKNOWN_VALUE"); - mapping.startObject("mapping"); - { - mapping.field("type", "integer"); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endArray(); - } - mapping.endObject(); - - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .putMapping("test", Strings.toString(mapping)) - .settings(settings(Version.V_5_6_0)) - .numberOfShards(1) - .numberOfReplicas(0) - .build(); - - DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "Unrecognized match_mapping_type options not silently ignored", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + - "breaking_60_mappings_changes.html#_unrecognized_literal_match_mapping_type_literal_options_not_silently_ignored", - "[type: test, dynamicFieldDefinitionintegers, unknown match_mapping_type[UNKNOWN_VALUE]]"); - List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); - assertEquals(singletonList(expected), issues); - } - - public void testBaseSimilarityDefinedCheck() { - assertSettingsAndIssue("index.similarity.base.type", "classic", - new DeprecationIssue(DeprecationIssue.Level.WARNING, - "The base similarity is now ignored as coords and query normalization have been removed." + - "If provided, this setting will be ignored and issue a deprecation warning", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + - "breaking_60_settings_changes.html#_similarity_settings", null)); - } - - public void testIndexStoreTypeCheck() { - assertSettingsAndIssue("index.store.type", "niofs", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "The default index.store.type has been removed. If you were using it, " + - "we advise that you simply remove it from your index settings and Elasticsearch" + - "will use the best store implementation for your operating system.", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + - "breaking_60_settings_changes.html#_store_settings", null)); - } - public void testStoreThrottleSettingsCheck() { - assertSettingsAndIssue("index.store.throttle.max_bytes_per_sec", "32", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "index.store.throttle settings are no longer recognized. these settings should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + - "breaking_60_settings_changes.html#_store_throttling_settings", - "present settings: [index.store.throttle.max_bytes_per_sec]")); - assertSettingsAndIssue("index.store.throttle.type", "none", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "index.store.throttle settings are no longer recognized. these settings should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + - "breaking_60_settings_changes.html#_store_throttling_settings", - "present settings: [index.store.throttle.type]")); - } - - public void testSharedFileSystemSettingsCheck() { - assertSettingsAndIssue("index.shared_filesystem", "true", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "[index.shared_filesystem] setting should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_indices_changes.html#_shadow_replicas_have_been_removed", null)); - } - public void testDelimitedPayloadFilterCheck() throws IOException { Settings settings = settings( - VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, VersionUtils.getPreviousVersion(Version.V_7_0_0_alpha1))) + VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, VersionUtils.getPreviousVersion(Version.V_7_0_0_alpha1))) .put("index.analysis.filter.my_delimited_payload_filter.type", "delimited_payload_filter") .put("index.analysis.filter.my_delimited_payload_filter.delimiter", "^") .put("index.analysis.filter.my_delimited_payload_filter.encoding", "identity").build(); @@ -183,4 +36,4 @@ public void testDelimitedPayloadFilterCheck() throws IOException { List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); assertEquals(singletonList(expected), issues); } -} \ No newline at end of file +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java index 3ca3c3154506..252cf97d0c51 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportIsolateDatafeedAction.java @@ -5,8 +5,6 @@ */ package org.elasticsearch.xpack.ml.action; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.TaskOperationFailure; @@ -53,11 +51,6 @@ protected void doExecute(Task task, IsolateDatafeedAction.Request request, Actio String executorNode = datafeedTask.getExecutorNode(); DiscoveryNodes nodes = state.nodes(); - if (nodes.resolveNode(executorNode).getVersion().before(Version.V_5_5_0)) { - listener.onFailure(new ElasticsearchException("Force delete datafeed is not supported because the datafeed task " + - "is running on a node [" + executorNode + "] with a version prior to " + Version.V_5_5_0)); - return; - } request.setNodes(datafeedTask.getExecutorNode()); super.doExecute(task, request, listener); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java index b40f0368a155..a9b43c3bcc47 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportKillProcessAction.java @@ -5,8 +5,6 @@ */ package org.elasticsearch.xpack.ml.action; -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -73,12 +71,6 @@ protected void doExecute(Task task, KillProcessAction.Request request, ActionLis return; } - Version nodeVersion = executorNode.getVersion(); - if (nodeVersion.before(Version.V_5_5_0)) { - listener.onFailure(new ElasticsearchException("Cannot kill the process on node with version " + nodeVersion)); - return; - } - super.doExecute(task, request, listener); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java index 56d03dd1aacc..512d8188abfa 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java @@ -179,14 +179,6 @@ static PersistentTasksCustomMetaData.Assignment selectLeastLoadedMlNode(String j continue; } - if (nodeSupportsJobVersion(node.getVersion()) == false) { - String reason = "Not opening job [" + jobId + "] on node [" + nodeNameAndVersion(node) - + "], because this node does not support jobs of version [" + job.getJobVersion() + "]"; - logger.trace(reason); - reasons.add(reason); - continue; - } - if (nodeSupportsModelSnapshotVersion(node, job) == false) { String reason = "Not opening job [" + jobId + "] on node [" + nodeNameAndVersion(node) + "], because the job's model snapshot requires a node of version [" @@ -385,10 +377,6 @@ static List verifyIndicesPrimaryShardsAreActive(String jobId, ClusterSta return unavailableIndices; } - private static boolean nodeSupportsJobVersion(Version nodeVersion) { - return nodeVersion.onOrAfter(Version.V_5_5_0); - } - private static boolean nodeSupportsModelSnapshotVersion(DiscoveryNode node, Job job) { if (job.getModelSnapshotId() == null || job.getModelSnapshotMinVersion() == null) { // There is no snapshot to restore or the min model snapshot version is 5.5.0 diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportOpenJobActionTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportOpenJobActionTests.java index 02bfb1b326fd..5bf8fb6956bf 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportOpenJobActionTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportOpenJobActionTests.java @@ -423,33 +423,6 @@ public void testSelectLeastLoadedMlNode_noCompatibleJobTypeNodes() { assertNull(result.getExecutorNode()); } - public void testSelectLeastLoadedMlNode_noNodesPriorTo_V_5_5() { - Map nodeAttr = new HashMap<>(); - nodeAttr.put(MachineLearning.ML_ENABLED_NODE_ATTR, "true"); - DiscoveryNodes nodes = DiscoveryNodes.builder() - .add(new DiscoveryNode("_node_name1", "_node_id1", new TransportAddress(InetAddress.getLoopbackAddress(), 9300), - nodeAttr, Collections.emptySet(), Version.V_5_4_0)) - .add(new DiscoveryNode("_node_name2", "_node_id2", new TransportAddress(InetAddress.getLoopbackAddress(), 9301), - nodeAttr, Collections.emptySet(), Version.V_5_4_0)) - .build(); - - PersistentTasksCustomMetaData.Builder tasksBuilder = PersistentTasksCustomMetaData.builder(); - addJobTask("incompatible_type_job", "_node_id1", null, tasksBuilder); - PersistentTasksCustomMetaData tasks = tasksBuilder.build(); - - ClusterState.Builder cs = ClusterState.builder(new ClusterName("_name")); - MetaData.Builder metaData = MetaData.builder(); - RoutingTable.Builder routingTable = RoutingTable.builder(); - addJobAndIndices(metaData, routingTable, "incompatible_type_job"); - cs.nodes(nodes); - metaData.putCustom(PersistentTasksCustomMetaData.TYPE, tasks); - cs.metaData(metaData); - cs.routingTable(routingTable.build()); - Assignment result = TransportOpenJobAction.selectLeastLoadedMlNode("incompatible_type_job", cs.build(), 2, 10, 30, logger); - assertThat(result.getExplanation(), containsString("because this node does not support jobs of version [" + Version.CURRENT + "]")); - assertNull(result.getExecutorNode()); - } - public void testSelectLeastLoadedMlNode_noNodesMatchingModelSnapshotMinVersion() { Map nodeAttr = new HashMap<>(); nodeAttr.put(MachineLearning.ML_ENABLED_NODE_ATTR, "true"); @@ -606,12 +579,6 @@ public void testMappingRequiresUpdateMaliciousMappingVersion() throws IOExceptio assertArrayEquals(indices, TransportOpenJobAction.mappingRequiresUpdate(cs, indices, Version.CURRENT, logger)); } - public void testMappingRequiresUpdateOldMappingVersion() throws IOException { - ClusterState cs = getClusterStateWithMappingsWithMetaData(Collections.singletonMap("version_54", Version.V_5_4_0.toString())); - String[] indices = new String[] { "version_54" }; - assertArrayEquals(indices, TransportOpenJobAction.mappingRequiresUpdate(cs, indices, Version.CURRENT, logger)); - } - public void testMappingRequiresUpdateBogusMappingVersion() throws IOException { ClusterState cs = getClusterStateWithMappingsWithMetaData(Collections.singletonMap("version_bogus", "0.0")); String[] indices = new String[] { "version_bogus" }; @@ -632,21 +599,6 @@ public void testMappingRequiresUpdateNewerMappingVersionMinor() throws IOExcepti TransportOpenJobAction.mappingRequiresUpdate(cs, indices, VersionUtils.getPreviousMinorVersion(), logger)); } - public void testMappingRequiresUpdateSomeVersionMix() throws IOException { - Map versionMix = new HashMap<>(); - versionMix.put("version_54", Version.V_5_4_0); - versionMix.put("version_current", Version.CURRENT); - versionMix.put("version_null", null); - versionMix.put("version_current2", Version.CURRENT); - versionMix.put("version_bogus", "0.0.0"); - versionMix.put("version_current3", Version.CURRENT); - versionMix.put("version_bogus2", "0.0.0"); - - ClusterState cs = getClusterStateWithMappingsWithMetaData(versionMix); - String[] indices = new String[] { "version_54", "version_null", "version_bogus", "version_bogus2" }; - assertArrayEquals(indices, TransportOpenJobAction.mappingRequiresUpdate(cs, indices, Version.CURRENT, logger)); - } - public void testNodeNameAndVersion() { TransportAddress ta = new TransportAddress(InetAddress.getLoopbackAddress(), 9300); Map attributes = new HashMap<>(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedStateTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedStateTests.java index 8b3e68b1e571..32699f60cbdb 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedStateTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedStateTests.java @@ -5,19 +5,8 @@ */ package org.elasticsearch.xpack.ml.datafeed; -import org.elasticsearch.Version; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedState; -import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.io.IOException; - -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class DatafeedStateTests extends ESTestCase { @@ -37,35 +26,4 @@ public void testValidOrdinals() { assertEquals(2, DatafeedState.STARTING.ordinal()); assertEquals(3, DatafeedState.STOPPING.ordinal()); } - - @SuppressWarnings("unchecked") - public void testStreaming_v54BackwardsCompatibility() throws IOException { - StreamOutput out = mock(StreamOutput.class); - when(out.getVersion()).thenReturn(Version.V_5_4_0); - ArgumentCaptor enumCaptor = ArgumentCaptor.forClass(Enum.class); - - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocationOnMock) { - return null; - } - }).when(out).writeEnum(enumCaptor.capture()); - - // STARTING & STOPPING states were introduced in v5.5. - // Pre v5.5 STARTING translated as STOPPED - DatafeedState.STARTING.writeTo(out); - assertEquals(DatafeedState.STOPPED, enumCaptor.getValue()); - - // Pre v5.5 STOPPING means the datafeed is STARTED - DatafeedState.STOPPING.writeTo(out); - assertEquals(DatafeedState.STARTED, enumCaptor.getValue()); - - // POST 5.5 enums a written as is - when(out.getVersion()).thenReturn(Version.V_5_5_0); - - DatafeedState.STARTING.writeTo(out); - assertEquals(DatafeedState.STARTING, enumCaptor.getValue()); - DatafeedState.STOPPING.writeTo(out); - assertEquals(DatafeedState.STOPPING, enumCaptor.getValue()); - } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/config/JobStateTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/config/JobStateTests.java index cd983c6b0302..2e324b6a1c20 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/config/JobStateTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/config/JobStateTests.java @@ -5,19 +5,8 @@ */ package org.elasticsearch.xpack.ml.job.config; -import org.elasticsearch.Version; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.ml.job.config.JobState; -import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.io.IOException; - -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class JobStateTests extends ESTestCase { @@ -60,35 +49,4 @@ public void testIsAnyOf() { assertTrue(JobState.CLOSED.isAnyOf(JobState.CLOSED)); assertTrue(JobState.CLOSING.isAnyOf(JobState.CLOSING)); } - - @SuppressWarnings("unchecked") - public void testStreaming_v54BackwardsCompatibility() throws IOException { - StreamOutput out = mock(StreamOutput.class); - when(out.getVersion()).thenReturn(Version.V_5_4_0); - ArgumentCaptor enumCaptor = ArgumentCaptor.forClass(Enum.class); - - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocationOnMock) { - return null; - } - }).when(out).writeEnum(enumCaptor.capture()); - - // OPENING state was introduced in v5.5. - // Pre v5.5 its translated as CLOSED - JobState.OPENING.writeTo(out); - assertEquals(JobState.CLOSED, enumCaptor.getValue()); - - when(out.getVersion()).thenReturn(Version.V_5_5_0); - - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocationOnMock) { - return null; - } - }).when(out).writeEnum(enumCaptor.capture()); - - JobState.OPENING.writeTo(out); - assertEquals(JobState.OPENING, enumCaptor.getValue()); - } } diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkDocTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkDocTests.java index 57106363bc19..dc294ef53de5 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkDocTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkDocTests.java @@ -5,12 +5,10 @@ */ package org.elasticsearch.xpack.monitoring.action; -import org.elasticsearch.Version; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.EqualsHashCodeTestUtils; @@ -21,7 +19,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import static java.util.Collections.emptyList; @@ -158,23 +155,6 @@ public void testSerialization() throws IOException { } } - public void testSerializationBwc() throws IOException { - final byte[] data = Base64.getDecoder().decode("AQNtSWQBBTUuMS4yAAAAAQEEdHlwZQECaWQNeyJmb28iOiJiYXIifQAAAAAAAAAA"); - final Version version = randomFrom(Version.V_5_0_0, Version.V_5_0_1, Version.V_5_0_2, - Version.V_5_1_1, Version.V_5_1_2, Version.V_5_2_0); - try (StreamInput in = StreamInput.wrap(data)) { - in.setVersion(version); - MonitoringBulkDoc bulkDoc = MonitoringBulkDoc.readFrom(in); - assertEquals(MonitoredSystem.UNKNOWN, bulkDoc.getSystem()); - assertEquals("type", bulkDoc.getType()); - assertEquals("id", bulkDoc.getId()); - assertEquals(0L, bulkDoc.getTimestamp()); - assertEquals(0L, bulkDoc.getIntervalMillis()); - assertEquals("{\"foo\":\"bar\"}", bulkDoc.getSource().utf8ToString()); - assertEquals(XContentType.JSON, bulkDoc.getXContentType()); - } - } - /** * Test that we allow strings to be "" because Logstash 5.2 - 5.3 would submit empty _id values for time-based documents */ diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkRequestTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkRequestTests.java index b336b3c88531..dc5cad7c94fd 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkRequestTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/action/MonitoringBulkRequestTests.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.monitoring.action; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -26,7 +25,6 @@ import java.util.Collection; import java.util.List; -import static org.elasticsearch.test.VersionUtils.randomVersionBetween; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -254,52 +252,6 @@ public void testSerialization() throws IOException { assertArrayEquals(originalBulkDocs, deserializedBulkDocs); } - public void testSerializationBwc() throws IOException { - final MonitoringBulkRequest originalRequest = new MonitoringBulkRequest(); - - final int numDocs = iterations(10, 30); - for (int i = 0; i < numDocs; i++) { - originalRequest.add(randomMonitoringBulkDoc()); - } - - final Version version = randomVersionBetween(random(), Version.V_5_0_0, Version.V_6_0_0_rc1); - - final BytesStreamOutput out = new BytesStreamOutput(); - out.setVersion(version); - originalRequest.writeTo(out); - - final StreamInput in = out.bytes().streamInput(); - in.setVersion(out.getVersion()); - - final MonitoringBulkRequest deserializedRequest = new MonitoringBulkRequest(); - deserializedRequest.readFrom(in); - - assertThat(in.available(), equalTo(0)); - - final MonitoringBulkDoc[] originalBulkDocs = originalRequest.getDocs().toArray(new MonitoringBulkDoc[]{}); - final MonitoringBulkDoc[] deserializedBulkDocs = deserializedRequest.getDocs().toArray(new MonitoringBulkDoc[]{}); - - assertThat(originalBulkDocs.length, equalTo(deserializedBulkDocs.length)); - - for (int i = 0; i < originalBulkDocs.length; i++) { - final MonitoringBulkDoc original = originalBulkDocs[i]; - final MonitoringBulkDoc deserialized = deserializedBulkDocs[i]; - - assertThat(deserialized.getSystem(), equalTo(original.getSystem())); - assertThat(deserialized.getType(), equalTo(original.getType())); - assertThat(deserialized.getId(), equalTo(original.getId())); - assertThat(deserialized.getTimestamp(), equalTo(original.getTimestamp())); - assertThat(deserialized.getSource(), equalTo(original.getSource())); - assertThat(deserialized.getXContentType(), equalTo(original.getXContentType())); - - if (version.onOrAfter(Version.V_6_0_0_rc1)) { - assertThat(deserialized.getIntervalMillis(), equalTo(original.getIntervalMillis())); - } else { - assertThat(deserialized.getIntervalMillis(), equalTo(0L)); - } - } - } - /** * Return a {@link XContentType} supported by the Monitoring Bulk API (JSON or Smile) */ diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/CollectorTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/CollectorTests.java index 79279faa6f40..3d1a0bf9aded 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/CollectorTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/CollectorTests.java @@ -5,39 +5,11 @@ */ package org.elasticsearch.xpack.monitoring.collector; -import org.elasticsearch.Version; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; public class CollectorTests extends ESTestCase { public void testConvertNullNode() { assertEquals(null, Collector.convertNode(randomNonNegativeLong(), null)); } - - public void testConvertNode() { - final String name = randomBoolean() ? randomAlphaOfLength(5) : ""; - final String nodeId = randomAlphaOfLength(5); - final TransportAddress address = buildNewFakeTransportAddress(); - final Version version = randomFrom(Version.V_5_0_1, Version.V_5_3_0, Version.CURRENT); - final long timestamp = randomNonNegativeLong(); - - final Set roles = new HashSet<>(); - if (randomBoolean()) { - roles.addAll(randomSubsetOf(Arrays.asList(DiscoveryNode.Role.values()))); - } - - final MonitoringDoc.Node expectedNode = new MonitoringDoc.Node(nodeId, address.address().getHostString(), address.toString(), - address.getAddress(), name, timestamp); - - DiscoveryNode discoveryNode = new DiscoveryNode(name, nodeId, address, Collections.emptyMap(), roles, version); - assertEquals(expectedNode, Collector.convertNode(timestamp, discoveryNode)); - } } diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/BaseMonitoringDocTestCase.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/BaseMonitoringDocTestCase.java index 513ee3bdbb66..46ba34dcd1a5 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/BaseMonitoringDocTestCase.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/exporter/BaseMonitoringDocTestCase.java @@ -5,12 +5,10 @@ */ package org.elasticsearch.xpack.monitoring.exporter; -import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -31,14 +29,12 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Base64; import java.util.List; import java.util.Map; import static java.util.Collections.emptyList; import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; -import static org.elasticsearch.test.VersionUtils.randomVersionBetween; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; @@ -273,22 +269,4 @@ public void testMonitoringNodeSerialization() throws IOException { assertEquals(deserialized.hashCode(), original.hashCode()); assertNotSame(deserialized, original); } - - public void testMonitoringNodeBwcSerialization() throws IOException { - final Version version = randomVersionBetween(random(), Version.V_5_0_0, Version.V_6_0_0_beta2); - - final byte[] data = Base64.getDecoder() - .decode("AQVFSWJKdgEDdFFOAQV3cGtMagEFa2xqeWEBBVZTamF2AwVrZXkjMgEyBWtleSMxATEFa2V5IzABMAAAAAAAAA=="); - try (StreamInput in = StreamInput.wrap(data)) { - in.setVersion(version); - - final MonitoringDoc.Node node = new MonitoringDoc.Node(in); - assertEquals("EIbJv", node.getUUID()); - assertEquals("VSjav", node.getName()); - assertEquals("tQN", node.getHost()); - assertEquals("wpkLj", node.getTransportAddress()); - assertEquals("kljya", node.getIp()); - assertEquals(0L, node.getTimestamp()); - } - } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 99c138bbb121..0b8dbd023355 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -226,12 +226,10 @@ private boolean userIsDefinedForCurrentSecurityMapping(String username) { private Version getDefinedVersion(String username) { switch (username) { - case LogstashSystemUser.NAME: - return LogstashSystemUser.DEFINED_SINCE; case BeatsSystemUser.NAME: return BeatsSystemUser.DEFINED_SINCE; default: - return Version.V_5_0_0; + return Version.V_6_0_0; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java index 761af81b08ec..b686994a2ee9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java @@ -25,7 +25,6 @@ import org.elasticsearch.transport.nio.NioTcpChannel; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.security.action.SecurityActionMapper; @@ -116,50 +115,28 @@ requests from all the nodes are attached with a user (either a serialize } } - final Version version = transportChannel.getVersion().equals(Version.V_5_4_0) ? Version.CURRENT : transportChannel.getVersion(); + final Version version = transportChannel.getVersion(); authcService.authenticate(securityAction, request, (User)null, ActionListener.wrap((authentication) -> { - if (reservedRealmEnabled && authentication.getVersion().before(Version.V_5_2_0) && - KibanaUser.NAME.equals(authentication.getUser().authenticatedUser().principal())) { - executeAsCurrentVersionKibanaUser(securityAction, request, transportChannel, listener, authentication); - } else if (securityAction.equals(TransportService.HANDSHAKE_ACTION_NAME) && - SystemUser.is(authentication.getUser()) == false) { - securityContext.executeAsUser(SystemUser.INSTANCE, (ctx) -> { - final Authentication replaced = Authentication.getAuthentication(threadContext); - final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = - new AuthorizationUtils.AsyncAuthorizer(replaced, listener, (userRoles, runAsRoles) -> { - authzService.authorize(replaced, securityAction, request, userRoles, runAsRoles); - listener.onResponse(null); - }); - asyncAuthorizer.authorize(authzService); - }, version); - } else { + if (securityAction.equals(TransportService.HANDSHAKE_ACTION_NAME) && + SystemUser.is(authentication.getUser()) == false) { + securityContext.executeAsUser(SystemUser.INSTANCE, (ctx) -> { + final Authentication replaced = Authentication.getAuthentication(threadContext); final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = - new AuthorizationUtils.AsyncAuthorizer(authentication, listener, (userRoles, runAsRoles) -> { - authzService.authorize(authentication, securityAction, request, userRoles, runAsRoles); - listener.onResponse(null); - }); + new AuthorizationUtils.AsyncAuthorizer(replaced, listener, (userRoles, runAsRoles) -> { + authzService.authorize(replaced, securityAction, request, userRoles, runAsRoles); + listener.onResponse(null); + }); asyncAuthorizer.authorize(authzService); - } - }, listener::onFailure)); - } - - private void executeAsCurrentVersionKibanaUser(String securityAction, TransportRequest request, TransportChannel transportChannel, - ActionListener listener, Authentication authentication) { - // the authentication came from an older node - so let's replace the user with our version - final User kibanaUser = new KibanaUser(authentication.getUser().enabled()); - if (kibanaUser.enabled()) { - securityContext.executeAsUser(kibanaUser, (original) -> { - final Authentication replacedUserAuth = securityContext.getAuthentication(); + }, version); + } else { final AuthorizationUtils.AsyncAuthorizer asyncAuthorizer = - new AuthorizationUtils.AsyncAuthorizer(replacedUserAuth, listener, (userRoles, runAsRoles) -> { - authzService.authorize(replacedUserAuth, securityAction, request, userRoles, runAsRoles); + new AuthorizationUtils.AsyncAuthorizer(authentication, listener, (userRoles, runAsRoles) -> { + authzService.authorize(authentication, securityAction, request, userRoles, runAsRoles); listener.onResponse(null); }); asyncAuthorizer.authorize(authzService); - }, transportChannel.getVersion()); - } else { - throw new IllegalStateException("a disabled user should never be sent. " + kibanaUser); - } + } + }, listener::onFailure)); } } 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 1ac5490dc0c6..e4e1e7ca1c01 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 @@ -79,7 +79,7 @@ public void init() throws Exception { ClusterState state = mock(ClusterState.class); DiscoveryNodes nodes = DiscoveryNodes.builder() .add(new DiscoveryNode("id1", buildNewFakeTransportAddress(), Version.CURRENT)) - .add(new DiscoveryNode("id2", buildNewFakeTransportAddress(), Version.V_5_4_0)) + .add(new DiscoveryNode("id2", buildNewFakeTransportAddress(), Version.V_6_0_0)) .build(); when(state.nodes()).thenReturn(nodes); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index 04e0afcf8829..39d518a73f3b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -444,23 +444,15 @@ public static void mockGetAllReservedUserInfo(NativeUsersStore usersStore, Map versionPredicate) { - assertThat(versionPredicate.test(Version.V_5_0_0_rc1), is(false)); switch (principal) { case LogstashSystemUser.NAME: - assertThat(versionPredicate.test(Version.V_5_0_0), is(false)); - assertThat(versionPredicate.test(Version.V_5_1_1), is(false)); - assertThat(versionPredicate.test(Version.V_5_2_0), is(true)); assertThat(versionPredicate.test(Version.V_6_3_0), is(true)); break; case BeatsSystemUser.NAME: - assertThat(versionPredicate.test(Version.V_5_6_9), is(false)); assertThat(versionPredicate.test(Version.V_6_2_3), is(false)); assertThat(versionPredicate.test(Version.V_6_3_0), is(true)); break; default: - assertThat(versionPredicate.test(Version.V_5_0_0), is(true)); - assertThat(versionPredicate.test(Version.V_5_1_1), is(true)); - assertThat(versionPredicate.test(Version.V_5_2_0), is(true)); assertThat(versionPredicate.test(Version.V_6_3_0), is(true)); break; } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java index 825ce4ee44c6..34a0685c2fd2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/accesscontrol/IndicesPermissionTests.java @@ -195,7 +195,7 @@ public void testIndicesPrivilegesStreaming() throws IOException { assertEquals(readIndicesPrivileges, indicesPrivileges.build()); out = new BytesStreamOutput(); - out.setVersion(Version.V_5_0_0); + out.setVersion(Version.V_6_0_0); indicesPrivileges = RoleDescriptor.IndicesPrivileges.builder(); indicesPrivileges.grantedFields(allowed); indicesPrivileges.deniedFields(denied); @@ -205,7 +205,7 @@ public void testIndicesPrivilegesStreaming() throws IOException { indicesPrivileges.build().writeTo(out); out.close(); in = out.bytes().streamInput(); - in.setVersion(Version.V_5_0_0); + in.setVersion(Version.V_6_0_0); RoleDescriptor.IndicesPrivileges readIndicesPrivileges2 = RoleDescriptor.IndicesPrivileges.createFrom(in); assertEquals(readIndicesPrivileges, readIndicesPrivileges2); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerTests.java index 7d10198c6aea..c3a6d7e920d1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/SecurityIndexManagerTests.java @@ -347,10 +347,10 @@ public void testIndexTemplateVersionMatching() throws Exception { assertTrue(SecurityIndexManager.checkTemplateExistsAndVersionMatches( SecurityIndexManager.SECURITY_TEMPLATE_NAME, clusterState, logger, - Version.V_5_0_0::before)); + Version.V_6_0_0::before)); assertFalse(SecurityIndexManager.checkTemplateExistsAndVersionMatches( SecurityIndexManager.SECURITY_TEMPLATE_NAME, clusterState, logger, - Version.V_5_0_0::after)); + Version.V_6_0_0::after)); } public void testUpToDateMappingsAreIdentifiedAsUpToDate() throws IOException { @@ -448,4 +448,4 @@ private static IndexTemplateMetaData.Builder getIndexTemplateMetaData(String tem } return templateBuilder; } -} \ No newline at end of file +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java index 08a991eb3ec2..bf8d8042546f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java @@ -27,7 +27,6 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; -import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.user.XPackUser; @@ -37,12 +36,10 @@ import java.io.IOException; import java.util.Collections; -import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.mock.orig.Mockito.times; import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; import static org.elasticsearch.xpack.core.security.support.Exceptions.authorizationError; -import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -220,47 +217,6 @@ public void testNodeProfileAllowsNodeActions() throws Exception { verifyNoMoreInteractions(authcService, authzService); } - public void testHandlesKibanaUserCompatibility() throws Exception { - TransportRequest request = mock(TransportRequest.class); - User user = new User("kibana", "kibana"); - Authentication authentication = mock(Authentication.class); - final Version version = Version.fromId(randomIntBetween(Version.V_5_0_0_ID, Version.V_5_2_0_ID - 100)); - when(authentication.getVersion()).thenReturn(version); - when(authentication.getUser()).thenReturn(user); - doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[3]; - callback.onResponse(authentication); - return Void.TYPE; - }).when(authcService).authenticate(eq("_action"), eq(request), eq((User)null), any(ActionListener.class)); - AtomicReference rolesRef = new AtomicReference<>(); - final Role empty = Role.EMPTY; - doAnswer((i) -> { - ActionListener callback = - (ActionListener) i.getArguments()[1]; - rolesRef.set(((User) i.getArguments()[0]).roles()); - callback.onResponse(empty); - return Void.TYPE; - }).when(authzService).roles(any(User.class), any(ActionListener.class)); - ServerTransportFilter filter = getClientOrNodeFilter(); - PlainActionFuture future = new PlainActionFuture<>(); - when(channel.getVersion()).thenReturn(version); - filter.inbound("_action", request, channel, future); - assertNotNull(rolesRef.get()); - assertThat(rolesRef.get(), arrayContaining("kibana_system")); - - // test with a version that doesn't need changing - filter = getClientOrNodeFilter(); - rolesRef.set(null); - user = new KibanaUser(true); - when(authentication.getUser()).thenReturn(user); - when(authentication.getVersion()).thenReturn(Version.V_5_2_0); - future = new PlainActionFuture<>(); - filter.inbound("_action", request, channel, future); - assertNotNull(rolesRef.get()); - assertThat(rolesRef.get(), arrayContaining("kibana_system")); - } - private ServerTransportFilter getClientOrNodeFilter() throws IOException { return randomBoolean() ? getNodeFilter(true) : getClientFilter(true); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java index 6bea620982fa..0d5941eaf267 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java @@ -5,9 +5,7 @@ */ package org.elasticsearch.xpack.security.user; -import org.elasticsearch.Version; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper; @@ -60,46 +58,6 @@ public void testWriteToAndReadFromWithRunAs() throws Exception { assertThat(readFromAuthUser.authenticatedUser(), is(authUser)); } - public void testRunAsBackcompatRead() throws Exception { - User user = new User(randomAlphaOfLengthBetween(4, 30), - randomBoolean() ? generateRandomStringArray(20, 30, false) : null); - // store the runAs user as the "authenticationUser" here to mimic old format for writing - User authUser = new User(randomAlphaOfLengthBetween(4, 30), generateRandomStringArray(20, 30, false), user); - - BytesStreamOutput output = new BytesStreamOutput(); - User.writeTo(authUser, output); - StreamInput input = output.bytes().streamInput(); - input.setVersion(randomFrom(Version.V_5_0_0, Version.V_5_4_0)); - User readFrom = User.readFrom(input); - - assertThat(readFrom.principal(), is(user.principal())); - assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); - User readFromAuthUser = readFrom.authenticatedUser(); - assertThat(authUser, is(notNullValue())); - assertThat(readFromAuthUser.principal(), is(authUser.principal())); - assertThat(Arrays.equals(readFromAuthUser.roles(), authUser.roles()), is(true)); - } - - public void testRunAsBackcompatWrite() throws Exception { - User user = new User(randomAlphaOfLengthBetween(4, 30), - randomBoolean() ? generateRandomStringArray(20, 30, false) : null); - // store the runAs user as the "authenticationUser" here to mimic old format for writing - User authUser = new User(randomAlphaOfLengthBetween(4, 30), generateRandomStringArray(20, 30, false), user); - - BytesStreamOutput output = new BytesStreamOutput(); - output.setVersion(randomFrom(Version.V_5_0_0, Version.V_5_4_0)); - User.writeTo(authUser, output); - StreamInput input = output.bytes().streamInput(); - User readFrom = User.readFrom(input); - - assertThat(readFrom.principal(), is(user.principal())); - assertThat(Arrays.equals(readFrom.roles(), user.roles()), is(true)); - User readFromAuthUser = readFrom.authenticatedUser(); - assertThat(authUser, is(notNullValue())); - assertThat(readFromAuthUser.principal(), is(authUser.principal())); - assertThat(Arrays.equals(readFromAuthUser.roles(), authUser.roles()), is(true)); - } - public void testSystemUserReadAndWrite() throws Exception { BytesStreamOutput output = new BytesStreamOutput(); diff --git a/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/IndexUpgradeService.java b/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/IndexUpgradeService.java index 07017e6fc001..ad0ebd6815f2 100644 --- a/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/IndexUpgradeService.java +++ b/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/IndexUpgradeService.java @@ -79,7 +79,7 @@ private UpgradeActionRequired upgradeInfo(IndexMetaData indexMetaData, String in } } // Catch all check for all indices that didn't match the specific checks - if (indexMetaData.getCreationVersion().before(Version.V_5_0_0)) { + if (indexMetaData.getCreationVersion().before(Version.V_6_0_0)) { return UpgradeActionRequired.REINDEX; } else { return null; diff --git a/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/Upgrade.java b/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/Upgrade.java index 568397e37395..e454ac4a0140 100644 --- a/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/Upgrade.java +++ b/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/Upgrade.java @@ -44,7 +44,7 @@ public class Upgrade extends Plugin implements ActionPlugin { - public static final Version UPGRADE_INTRODUCED = Version.V_5_6_0; + public static final Version UPGRADE_INTRODUCED = Version.CURRENT.minimumCompatibilityVersion(); private final Settings settings; private final List> upgradeCheckFactories; diff --git a/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/IndexUpgradeServiceTests.java b/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/IndexUpgradeServiceTests.java index 5939777572b4..f980450c07f7 100644 --- a/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/IndexUpgradeServiceTests.java +++ b/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/IndexUpgradeServiceTests.java @@ -166,7 +166,7 @@ public static IndexMetaData newTestIndexMeta(String name, String alias, Settings .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_CREATION_DATE, 1) .put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()) - .put(IndexMetaData.SETTING_VERSION_UPGRADED, Version.V_5_0_0_beta1) + .put(IndexMetaData.SETTING_VERSION_UPGRADED, Version.V_6_0_0) .put(indexSettings) .build(); IndexMetaData.Builder builder = IndexMetaData.builder(name).settings(build); diff --git a/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexerIT.java b/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexerIT.java index cd83803d1884..71e3348b058b 100644 --- a/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexerIT.java +++ b/x-pack/plugin/upgrade/src/test/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexerIT.java @@ -206,9 +206,9 @@ private ClusterState withRandomOldNode() { DiscoveryNode node = discoveryNodes.get(nodeId); DiscoveryNode newNode = new DiscoveryNode(node.getName(), node.getId(), node.getEphemeralId(), node.getHostName(), node.getHostAddress(), node.getAddress(), node.getAttributes(), node.getRoles(), - randomVersionBetween(random(), Version.V_5_0_0, Version.V_5_4_0)); + randomVersionBetween(random(), Version.V_6_0_0, Version.V_6_4_0)); return ClusterState.builder(clusterState).nodes(DiscoveryNodes.builder(discoveryNodes).remove(node).add(newNode)).build(); } -} \ No newline at end of file +} diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java index 3b9032f09218..1d3e51c11e02 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.protocol.xpack; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; @@ -412,8 +411,7 @@ public FeatureSet(String name, @Nullable String description, boolean available, } public FeatureSet(StreamInput in) throws IOException { - this(in.readString(), in.readOptionalString(), in.readBoolean(), in.readBoolean(), - in.getVersion().onOrAfter(Version.V_5_4_0) ? in.readMap() : null); + this(in.readString(), in.readOptionalString(), in.readBoolean(), in.readBoolean(), in.readMap()); } @Override @@ -422,9 +420,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(description); out.writeBoolean(available); out.writeBoolean(enabled); - if (out.getVersion().onOrAfter(Version.V_5_4_0)) { - out.writeMap(nativeCodeInfo); - } + out.writeMap(nativeCodeInfo); } public String name() { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshot.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshot.java index 2b9957f9bc75..ea5f01699310 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshot.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshot.java @@ -221,10 +221,8 @@ public boolean equals(Object other) { public static class Builder { private String jobId; - // Stored snapshot documents created prior to 6.3.0 will have no - // value for min_version. We default it to 5.5.0 as there were - // no model changes between 5.5.0 and 6.3.0. - private Version minVersion = Version.V_5_5_0; + // Stored snapshot documents created prior to 6.3.0 will have no value for min_version. + private Version minVersion = Version.V_6_3_0; private Date timestamp; private String description; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java index 42e957ecf2d5..e08289e98215 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java @@ -19,7 +19,6 @@ package org.elasticsearch.protocol.xpack.security; -import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; @@ -199,12 +198,7 @@ public static User partialReadFrom(String username, StreamInput input) throws IO boolean hasInnerUser = input.readBoolean(); if (hasInnerUser) { User innerUser = readFrom(input); - if (input.getVersion().onOrBefore(Version.V_5_4_0)) { - // backcompat: runas user was read first, so reverse outer and inner - return new User(innerUser, outerUser); - } else { - return new User(outerUser, innerUser); - } + return new User(outerUser, innerUser); } else { return outerUser; } @@ -221,11 +215,6 @@ public static void writeTo(User user, StreamOutput output) throws IOException { if (user.authenticatedUser == null) { // no backcompat necessary, since there is no inner user writeUser(user, output); - } else if (output.getVersion().onOrBefore(Version.V_5_4_0)) { - // backcompat: write runas user as the "inner" user - writeUser(user.authenticatedUser, output); - output.writeBoolean(true); - writeUser(user, output); } else { writeUser(user, output); output.writeBoolean(true); From c5e5a97a343d970f796b5d5748408ff765127d8f Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Fri, 24 Aug 2018 10:55:23 +0200 Subject: [PATCH 133/283] Update Google Cloud Storage Library for Java (#32940) This commit updated the google-cloud-storage library from version 1.28.0 to version 1.40.0. --- plugins/repository-gcs/build.gradle | 200 ++++----------- .../licenses/api-common-1.5.0.jar.sha1 | 1 - .../licenses/api-common-1.7.0.jar.sha1 | 1 + .../licenses/commons-codec-1.10.jar.sha1 | 1 + .../{old => }/commons-codec-LICENSE.txt | 0 .../{old => }/commons-codec-NOTICE.txt | 0 .../licenses/commons-logging-1.1.3.jar.sha1 | 1 + .../{old => }/commons-logging-LICENSE.txt | 0 .../{old => }/commons-logging-NOTICE.txt | 0 .../licenses/gax-1.25.0.jar.sha1 | 1 - .../licenses/gax-1.30.0.jar.sha1 | 1 + .../licenses/gax-httpjson-0.40.0.jar.sha1 | 1 - .../licenses/gax-httpjson-0.47.0.jar.sha1 | 1 + .../google-api-client-1.23.0.jar.sha1 | 1 - .../google-api-client-1.24.1.jar.sha1 | 1 + ...services-storage-v1-rev115-1.23.0.jar.sha1 | 1 - ...services-storage-v1-rev135-1.24.1.jar.sha1 | 1 + ...e-auth-library-credentials-0.10.0.jar.sha1 | 1 + ...le-auth-library-credentials-0.9.1.jar.sha1 | 1 - ...e-auth-library-oauth2-http-0.10.0.jar.sha1 | 1 + ...le-auth-library-oauth2-http-0.9.1.jar.sha1 | 1 - .../google-cloud-core-1.28.0.jar.sha1 | 1 - .../google-cloud-core-1.40.0.jar.sha1 | 1 + .../google-cloud-core-http-1.28.0.jar.sha1 | 1 - .../google-cloud-core-http-1.40.0.jar.sha1 | 1 + .../google-cloud-storage-1.28.0.jar.sha1 | 1 - .../google-cloud-storage-1.40.0.jar.sha1 | 1 + .../google-http-client-1.23.0.jar.sha1 | 1 - .../google-http-client-1.24.1.jar.sha1 | 1 + ...ogle-http-client-appengine-1.23.0.jar.sha1 | 1 - ...ogle-http-client-appengine-1.24.1.jar.sha1 | 1 + ...google-http-client-jackson-1.23.0.jar.sha1 | 1 - ...google-http-client-jackson-1.24.1.jar.sha1 | 1 + ...oogle-http-client-jackson2-1.23.0.jar.sha1 | 1 - ...oogle-http-client-jackson2-1.24.1.jar.sha1 | 1 + .../google-oauth-client-1.23.0.jar.sha1 | 1 - .../google-oauth-client-1.24.1.jar.sha1 | 1 + .../licenses/grpc-context-1.12.0.jar.sha1 | 1 + .../licenses/grpc-context-1.9.0.jar.sha1 | 1 - .../repository-gcs/licenses/gson-2.7.jar.sha1 | 1 + ...-core-asl-LICENSE.txt => gson-LICENSE.txt} | 0 ...on-core-asl-NOTICE.txt => gson-NOTICE.txt} | 0 .../licenses/httpclient-4.5.2.jar.sha1 | 1 + .../licenses/{old => }/httpclient-LICENSE.txt | 0 .../licenses/{old => }/httpclient-NOTICE.txt | 0 .../licenses/httpcore-4.4.5.jar.sha1 | 1 + .../repository-gcs/licenses/jackson-LICENSE | 8 + .../repository-gcs/licenses/jackson-NOTICE | 20 ++ .../licenses/jackson-core-asl-1.9.11.jar.sha1 | 1 + .../licenses/jackson-core-asl-1.9.13.jar.sha1 | 1 - .../licenses/old/google-LICENSE.txt | 201 --------------- .../licenses/old/google-NOTICE.txt | 1 - .../licenses/old/httpcore-LICENSE.txt | 241 ------------------ .../licenses/old/httpcore-NOTICE.txt | 8 - .../licenses/opencensus-api-0.11.1.jar.sha1 | 1 - .../licenses/opencensus-api-0.15.0.jar.sha1 | 1 + ...encensus-contrib-http-util-0.11.1.jar.sha1 | 1 - ...encensus-contrib-http-util-0.15.0.jar.sha1 | 1 + ...s-LICENSE.txt => proto-google-LICENSE.txt} | 0 ...tos-NOTICE.txt => proto-google-NOTICE.txt} | 0 ...proto-google-common-protos-1.12.0.jar.sha1 | 1 + .../proto-google-common-protos-1.8.0.jar.sha1 | 1 - .../proto-google-iam-v1-0.12.0.jar.sha1 | 1 + .../licenses/protobuf-LICENSE.txt | 32 +++ .../licenses/protobuf-NOTICE.txt | 32 +++ .../licenses/protobuf-java-3.6.0.jar.sha1 | 1 + .../protobuf-java-util-3.6.0.jar.sha1 | 1 + .../licenses/threetenbp-1.3.3.jar.sha1 | 1 + .../licenses/threetenbp-1.3.6.jar.sha1 | 1 - 69 files changed, 167 insertions(+), 626 deletions(-) delete mode 100644 plugins/repository-gcs/licenses/api-common-1.5.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/api-common-1.7.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/commons-codec-1.10.jar.sha1 rename plugins/repository-gcs/licenses/{old => }/commons-codec-LICENSE.txt (100%) rename plugins/repository-gcs/licenses/{old => }/commons-codec-NOTICE.txt (100%) create mode 100644 plugins/repository-gcs/licenses/commons-logging-1.1.3.jar.sha1 rename plugins/repository-gcs/licenses/{old => }/commons-logging-LICENSE.txt (100%) rename plugins/repository-gcs/licenses/{old => }/commons-logging-NOTICE.txt (100%) delete mode 100644 plugins/repository-gcs/licenses/gax-1.25.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/gax-1.30.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/gax-httpjson-0.40.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/gax-httpjson-0.47.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-api-client-1.23.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-api-client-1.24.1.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-api-services-storage-v1-rev115-1.23.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-api-services-storage-v1-rev135-1.24.1.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-auth-library-credentials-0.10.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-auth-library-credentials-0.9.1.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.10.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.9.1.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-cloud-core-1.28.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-cloud-core-1.40.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-cloud-core-http-1.28.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-cloud-core-http-1.40.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-cloud-storage-1.28.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-cloud-storage-1.40.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-http-client-1.23.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-http-client-1.24.1.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-http-client-appengine-1.23.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-http-client-appengine-1.24.1.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-http-client-jackson-1.23.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-http-client-jackson-1.24.1.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-http-client-jackson2-1.23.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-http-client-jackson2-1.24.1.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/google-oauth-client-1.23.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/google-oauth-client-1.24.1.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/grpc-context-1.12.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/grpc-context-1.9.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/gson-2.7.jar.sha1 rename plugins/repository-gcs/licenses/{jackson-core-asl-LICENSE.txt => gson-LICENSE.txt} (100%) rename plugins/repository-gcs/licenses/{jackson-core-asl-NOTICE.txt => gson-NOTICE.txt} (100%) create mode 100644 plugins/repository-gcs/licenses/httpclient-4.5.2.jar.sha1 rename plugins/repository-gcs/licenses/{old => }/httpclient-LICENSE.txt (100%) rename plugins/repository-gcs/licenses/{old => }/httpclient-NOTICE.txt (100%) create mode 100644 plugins/repository-gcs/licenses/httpcore-4.4.5.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/jackson-LICENSE create mode 100644 plugins/repository-gcs/licenses/jackson-NOTICE create mode 100644 plugins/repository-gcs/licenses/jackson-core-asl-1.9.11.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/jackson-core-asl-1.9.13.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/old/google-LICENSE.txt delete mode 100644 plugins/repository-gcs/licenses/old/google-NOTICE.txt delete mode 100644 plugins/repository-gcs/licenses/old/httpcore-LICENSE.txt delete mode 100644 plugins/repository-gcs/licenses/old/httpcore-NOTICE.txt delete mode 100644 plugins/repository-gcs/licenses/opencensus-api-0.11.1.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/opencensus-api-0.15.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.11.1.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.15.0.jar.sha1 rename plugins/repository-gcs/licenses/{proto-google-common-protos-LICENSE.txt => proto-google-LICENSE.txt} (100%) rename plugins/repository-gcs/licenses/{proto-google-common-protos-NOTICE.txt => proto-google-NOTICE.txt} (100%) create mode 100644 plugins/repository-gcs/licenses/proto-google-common-protos-1.12.0.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/proto-google-common-protos-1.8.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/proto-google-iam-v1-0.12.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/protobuf-LICENSE.txt create mode 100644 plugins/repository-gcs/licenses/protobuf-NOTICE.txt create mode 100644 plugins/repository-gcs/licenses/protobuf-java-3.6.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/protobuf-java-util-3.6.0.jar.sha1 create mode 100644 plugins/repository-gcs/licenses/threetenbp-1.3.3.jar.sha1 delete mode 100644 plugins/repository-gcs/licenses/threetenbp-1.3.6.jar.sha1 diff --git a/plugins/repository-gcs/build.gradle b/plugins/repository-gcs/build.gradle index 07ef4b4be5e6..510c101379d2 100644 --- a/plugins/repository-gcs/build.gradle +++ b/plugins/repository-gcs/build.gradle @@ -23,28 +23,38 @@ esplugin { } dependencies { - compile 'com.google.cloud:google-cloud-storage:1.28.0' - compile 'com.google.cloud:google-cloud-core:1.28.0' - compile 'com.google.cloud:google-cloud-core-http:1.28.0' - compile 'com.google.auth:google-auth-library-oauth2-http:0.9.1' - compile 'com.google.auth:google-auth-library-credentials:0.9.1' - compile 'com.google.oauth-client:google-oauth-client:1.23.0' - compile 'com.google.http-client:google-http-client:1.23.0' - compile 'com.google.http-client:google-http-client-jackson:1.23.0' - compile 'com.google.http-client:google-http-client-jackson2:1.23.0' - compile 'com.google.http-client:google-http-client-appengine:1.23.0' - compile 'com.google.api-client:google-api-client:1.23.0' - compile 'com.google.api:gax:1.25.0' - compile 'com.google.api:gax-httpjson:0.40.0' - compile 'com.google.api:api-common:1.5.0' - compile 'com.google.api.grpc:proto-google-common-protos:1.8.0' + compile 'com.google.cloud:google-cloud-storage:1.40.0' + compile 'com.google.cloud:google-cloud-core:1.40.0' compile 'com.google.guava:guava:20.0' - compile 'com.google.apis:google-api-services-storage:v1-rev115-1.23.0' - compile 'org.codehaus.jackson:jackson-core-asl:1.9.13' - compile 'io.grpc:grpc-context:1.9.0' - compile 'io.opencensus:opencensus-api:0.11.1' - compile 'io.opencensus:opencensus-contrib-http-util:0.11.1' - compile 'org.threeten:threetenbp:1.3.6' + compile 'joda-time:joda-time:2.10' + compile 'com.google.http-client:google-http-client:1.24.1' + 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.google.api:api-common:1.7.0' + compile 'com.google.api:gax:1.30.0' + compile 'org.threeten:threetenbp:1.3.3' + compile 'com.google.protobuf:protobuf-java-util:3.6.0' + compile 'com.google.protobuf:protobuf-java:3.6.0' + compile 'com.google.code.gson:gson:2.7' + compile 'com.google.api.grpc:proto-google-common-protos:1.12.0' + compile 'com.google.api.grpc:proto-google-iam-v1:0.12.0' + compile 'com.google.cloud:google-cloud-core-http:1.40.0' + compile 'com.google.auth:google-auth-library-credentials:0.10.0' + compile 'com.google.auth:google-auth-library-oauth2-http:0.10.0' + compile 'com.google.oauth-client:google-oauth-client:1.24.1' + compile 'com.google.api-client:google-api-client:1.24.1' + compile 'com.google.http-client:google-http-client-appengine:1.24.1' + compile 'com.google.http-client:google-http-client-jackson:1.24.1' + compile 'org.codehaus.jackson:jackson-core-asl:1.9.11' + compile 'com.google.http-client:google-http-client-jackson2:1.24.1' + compile "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" + compile 'com.google.api:gax-httpjson:0.47.0' + compile 'io.opencensus:opencensus-api:0.15.0' + compile 'io.grpc:grpc-context:1.12.0' + compile 'io.opencensus:opencensus-contrib-http-util:0.15.0' + compile 'com.google.apis:google-api-services-storage:v1-rev135-1.24.1' } dependencyLicenses { @@ -52,10 +62,18 @@ dependencyLicenses { mapping from: /google-auth-.*/, to: 'google-auth' mapping from: /google-http-.*/, to: 'google-http' mapping from: /opencensus.*/, to: 'opencensus' + mapping from: /jackson-.*/, to: 'jackson' + mapping from: /http.*/, to: 'httpclient' + mapping from: /protobuf.*/, to: 'protobuf' + mapping from: /proto-google.*/, to: 'proto-google' } thirdPartyAudit.excludes = [ // uses internal java api: sun.misc.Unsafe + 'com.google.protobuf.UnsafeUtil', + 'com.google.protobuf.UnsafeUtil$1', + 'com.google.protobuf.UnsafeUtil$JvmMemoryAccessor', + 'com.google.protobuf.UnsafeUtil$MemoryAccessor', 'com.google.common.cache.Striped64', 'com.google.common.cache.Striped64$1', 'com.google.common.cache.Striped64$Cell', @@ -87,139 +105,13 @@ thirdPartyAudit.excludes = [ 'com.google.appengine.api.urlfetch.HTTPResponse', 'com.google.appengine.api.urlfetch.URLFetchService', 'com.google.appengine.api.urlfetch.URLFetchServiceFactory', - 'com.google.gson.Gson', - 'com.google.gson.GsonBuilder', - 'com.google.gson.TypeAdapter', - 'com.google.gson.stream.JsonReader', - 'com.google.gson.stream.JsonWriter', - 'com.google.iam.v1.Binding$Builder', - 'com.google.iam.v1.Binding', - 'com.google.iam.v1.Policy$Builder', - 'com.google.iam.v1.Policy', - 'com.google.protobuf.AbstractMessageLite$Builder', - 'com.google.protobuf.AbstractParser', - 'com.google.protobuf.Any$Builder', - 'com.google.protobuf.Any', - 'com.google.protobuf.AnyOrBuilder', - 'com.google.protobuf.AnyProto', - 'com.google.protobuf.Api$Builder', - 'com.google.protobuf.Api', - 'com.google.protobuf.ApiOrBuilder', - 'com.google.protobuf.ApiProto', - 'com.google.protobuf.ByteString', - 'com.google.protobuf.CodedInputStream', - 'com.google.protobuf.CodedOutputStream', - 'com.google.protobuf.DescriptorProtos', - 'com.google.protobuf.Descriptors$Descriptor', - 'com.google.protobuf.Descriptors$EnumDescriptor', - 'com.google.protobuf.Descriptors$EnumValueDescriptor', - 'com.google.protobuf.Descriptors$FieldDescriptor', - 'com.google.protobuf.Descriptors$FileDescriptor$InternalDescriptorAssigner', - 'com.google.protobuf.Descriptors$FileDescriptor', - 'com.google.protobuf.Descriptors$OneofDescriptor', - 'com.google.protobuf.Duration$Builder', - 'com.google.protobuf.Duration', - 'com.google.protobuf.DurationOrBuilder', - 'com.google.protobuf.DurationProto', - 'com.google.protobuf.EmptyProto', - 'com.google.protobuf.Enum$Builder', - 'com.google.protobuf.Enum', - 'com.google.protobuf.EnumOrBuilder', - 'com.google.protobuf.ExtensionRegistry', - 'com.google.protobuf.ExtensionRegistryLite', - 'com.google.protobuf.FloatValue$Builder', - 'com.google.protobuf.FloatValue', - 'com.google.protobuf.FloatValueOrBuilder', - 'com.google.protobuf.GeneratedMessage$GeneratedExtension', - 'com.google.protobuf.GeneratedMessage', - 'com.google.protobuf.GeneratedMessageV3$Builder', - 'com.google.protobuf.GeneratedMessageV3$BuilderParent', - 'com.google.protobuf.GeneratedMessageV3$FieldAccessorTable', - 'com.google.protobuf.GeneratedMessageV3', - 'com.google.protobuf.Internal$EnumLite', - 'com.google.protobuf.Internal$EnumLiteMap', - 'com.google.protobuf.Internal', - 'com.google.protobuf.InvalidProtocolBufferException', - 'com.google.protobuf.LazyStringArrayList', - 'com.google.protobuf.LazyStringList', - 'com.google.protobuf.MapEntry$Builder', - 'com.google.protobuf.MapEntry', - 'com.google.protobuf.MapField', - 'com.google.protobuf.Message', - 'com.google.protobuf.MessageOrBuilder', - 'com.google.protobuf.Parser', - 'com.google.protobuf.ProtocolMessageEnum', - 'com.google.protobuf.ProtocolStringList', - 'com.google.protobuf.RepeatedFieldBuilderV3', - 'com.google.protobuf.SingleFieldBuilderV3', - 'com.google.protobuf.Struct$Builder', - 'com.google.protobuf.Struct', - 'com.google.protobuf.StructOrBuilder', - 'com.google.protobuf.StructProto', - 'com.google.protobuf.Timestamp$Builder', - 'com.google.protobuf.Timestamp', - 'com.google.protobuf.TimestampProto', - 'com.google.protobuf.Type$Builder', - 'com.google.protobuf.Type', - 'com.google.protobuf.TypeOrBuilder', - 'com.google.protobuf.TypeProto', - 'com.google.protobuf.UInt32Value$Builder', - 'com.google.protobuf.UInt32Value', - 'com.google.protobuf.UInt32ValueOrBuilder', - 'com.google.protobuf.UnknownFieldSet$Builder', - 'com.google.protobuf.UnknownFieldSet', - 'com.google.protobuf.WireFormat$FieldType', - 'com.google.protobuf.WrappersProto', - 'com.google.protobuf.util.Timestamps', - 'org.apache.http.ConnectionReuseStrategy', - 'org.apache.http.Header', - 'org.apache.http.HttpEntity', - 'org.apache.http.HttpEntityEnclosingRequest', - 'org.apache.http.HttpHost', - 'org.apache.http.HttpRequest', - 'org.apache.http.HttpResponse', - 'org.apache.http.HttpVersion', - 'org.apache.http.RequestLine', - 'org.apache.http.StatusLine', - 'org.apache.http.client.AuthenticationHandler', - 'org.apache.http.client.HttpClient', - 'org.apache.http.client.HttpRequestRetryHandler', - 'org.apache.http.client.RedirectHandler', - 'org.apache.http.client.RequestDirector', - 'org.apache.http.client.UserTokenHandler', - 'org.apache.http.client.methods.HttpDelete', - 'org.apache.http.client.methods.HttpEntityEnclosingRequestBase', - 'org.apache.http.client.methods.HttpGet', - 'org.apache.http.client.methods.HttpHead', - 'org.apache.http.client.methods.HttpOptions', - 'org.apache.http.client.methods.HttpPost', - 'org.apache.http.client.methods.HttpPut', - 'org.apache.http.client.methods.HttpRequestBase', - 'org.apache.http.client.methods.HttpTrace', - 'org.apache.http.conn.ClientConnectionManager', - 'org.apache.http.conn.ConnectionKeepAliveStrategy', - 'org.apache.http.conn.params.ConnManagerParams', - 'org.apache.http.conn.params.ConnPerRouteBean', - 'org.apache.http.conn.params.ConnRouteParams', - 'org.apache.http.conn.routing.HttpRoutePlanner', - 'org.apache.http.conn.scheme.PlainSocketFactory', - 'org.apache.http.conn.scheme.Scheme', - 'org.apache.http.conn.scheme.SchemeRegistry', - 'org.apache.http.conn.ssl.SSLSocketFactory', - 'org.apache.http.conn.ssl.X509HostnameVerifier', - 'org.apache.http.entity.AbstractHttpEntity', - 'org.apache.http.impl.client.DefaultHttpClient', - 'org.apache.http.impl.client.DefaultHttpRequestRetryHandler', - 'org.apache.http.impl.conn.ProxySelectorRoutePlanner', - 'org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager', - 'org.apache.http.message.BasicHttpResponse', - 'org.apache.http.params.BasicHttpParams', - 'org.apache.http.params.HttpConnectionParams', - 'org.apache.http.params.HttpParams', - 'org.apache.http.params.HttpProtocolParams', - 'org.apache.http.protocol.HttpContext', - 'org.apache.http.protocol.HttpProcessor', - 'org.apache.http.protocol.HttpRequestExecutor' + // commons-logging optional dependencies + 'org.apache.avalon.framework.logger.Logger', + 'org.apache.log.Hierarchy', + 'org.apache.log.Logger', + // commons-logging provided dependencies + 'javax.servlet.ServletContextEvent', + 'javax.servlet.ServletContextListener' ] check { diff --git a/plugins/repository-gcs/licenses/api-common-1.5.0.jar.sha1 b/plugins/repository-gcs/licenses/api-common-1.5.0.jar.sha1 deleted file mode 100644 index 64435356e5ea..000000000000 --- a/plugins/repository-gcs/licenses/api-common-1.5.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -7e537338d40a57ad469239acb6d828fa544fb52b \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/api-common-1.7.0.jar.sha1 b/plugins/repository-gcs/licenses/api-common-1.7.0.jar.sha1 new file mode 100644 index 000000000000..67291b658e5c --- /dev/null +++ b/plugins/repository-gcs/licenses/api-common-1.7.0.jar.sha1 @@ -0,0 +1 @@ +ea59fb8b2450999345035dec8a6f472543391766 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/commons-codec-1.10.jar.sha1 b/plugins/repository-gcs/licenses/commons-codec-1.10.jar.sha1 new file mode 100644 index 000000000000..3fe8682a1b0f --- /dev/null +++ b/plugins/repository-gcs/licenses/commons-codec-1.10.jar.sha1 @@ -0,0 +1 @@ +4b95f4897fa13f2cd904aee711aeafc0c5295cd8 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/old/commons-codec-LICENSE.txt b/plugins/repository-gcs/licenses/commons-codec-LICENSE.txt similarity index 100% rename from plugins/repository-gcs/licenses/old/commons-codec-LICENSE.txt rename to plugins/repository-gcs/licenses/commons-codec-LICENSE.txt diff --git a/plugins/repository-gcs/licenses/old/commons-codec-NOTICE.txt b/plugins/repository-gcs/licenses/commons-codec-NOTICE.txt similarity index 100% rename from plugins/repository-gcs/licenses/old/commons-codec-NOTICE.txt rename to plugins/repository-gcs/licenses/commons-codec-NOTICE.txt diff --git a/plugins/repository-gcs/licenses/commons-logging-1.1.3.jar.sha1 b/plugins/repository-gcs/licenses/commons-logging-1.1.3.jar.sha1 new file mode 100644 index 000000000000..5b8f029e5829 --- /dev/null +++ b/plugins/repository-gcs/licenses/commons-logging-1.1.3.jar.sha1 @@ -0,0 +1 @@ +f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/old/commons-logging-LICENSE.txt b/plugins/repository-gcs/licenses/commons-logging-LICENSE.txt similarity index 100% rename from plugins/repository-gcs/licenses/old/commons-logging-LICENSE.txt rename to plugins/repository-gcs/licenses/commons-logging-LICENSE.txt diff --git a/plugins/repository-gcs/licenses/old/commons-logging-NOTICE.txt b/plugins/repository-gcs/licenses/commons-logging-NOTICE.txt similarity index 100% rename from plugins/repository-gcs/licenses/old/commons-logging-NOTICE.txt rename to plugins/repository-gcs/licenses/commons-logging-NOTICE.txt diff --git a/plugins/repository-gcs/licenses/gax-1.25.0.jar.sha1 b/plugins/repository-gcs/licenses/gax-1.25.0.jar.sha1 deleted file mode 100644 index 594177047c14..000000000000 --- a/plugins/repository-gcs/licenses/gax-1.25.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -36ab73c0b5d4a67447eb89a3174cc76ced150bd1 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/gax-1.30.0.jar.sha1 b/plugins/repository-gcs/licenses/gax-1.30.0.jar.sha1 new file mode 100644 index 000000000000..d6d2bb20ed84 --- /dev/null +++ b/plugins/repository-gcs/licenses/gax-1.30.0.jar.sha1 @@ -0,0 +1 @@ +58fa2feb11b092be0a6ebe705a28736f12374230 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/gax-httpjson-0.40.0.jar.sha1 b/plugins/repository-gcs/licenses/gax-httpjson-0.40.0.jar.sha1 deleted file mode 100644 index c251ea1dd956..000000000000 --- a/plugins/repository-gcs/licenses/gax-httpjson-0.40.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -cb4bafbfd45b9d24efbb6138a31e37918fac015f \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/gax-httpjson-0.47.0.jar.sha1 b/plugins/repository-gcs/licenses/gax-httpjson-0.47.0.jar.sha1 new file mode 100644 index 000000000000..fdc722d1520d --- /dev/null +++ b/plugins/repository-gcs/licenses/gax-httpjson-0.47.0.jar.sha1 @@ -0,0 +1 @@ +d096f3142eb3adbf877588d1044895d148d9efcb \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-api-client-1.23.0.jar.sha1 b/plugins/repository-gcs/licenses/google-api-client-1.23.0.jar.sha1 deleted file mode 100644 index 0c35d8e08b91..000000000000 --- a/plugins/repository-gcs/licenses/google-api-client-1.23.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -522ea860eb48dee71dfe2c61a1fd09663539f556 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-api-client-1.24.1.jar.sha1 b/plugins/repository-gcs/licenses/google-api-client-1.24.1.jar.sha1 new file mode 100644 index 000000000000..27dafe58a018 --- /dev/null +++ b/plugins/repository-gcs/licenses/google-api-client-1.24.1.jar.sha1 @@ -0,0 +1 @@ +37de23fb9b8b077de4ecec3192d98e752b0e5d72 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-api-services-storage-v1-rev115-1.23.0.jar.sha1 b/plugins/repository-gcs/licenses/google-api-services-storage-v1-rev115-1.23.0.jar.sha1 deleted file mode 100644 index 9f6f77ada3a6..000000000000 --- a/plugins/repository-gcs/licenses/google-api-services-storage-v1-rev115-1.23.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -ba4fb6c5dc8d5ad94dedd9927ceee10a31a59abd \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-api-services-storage-v1-rev135-1.24.1.jar.sha1 b/plugins/repository-gcs/licenses/google-api-services-storage-v1-rev135-1.24.1.jar.sha1 new file mode 100644 index 000000000000..e3042ee6ea07 --- /dev/null +++ b/plugins/repository-gcs/licenses/google-api-services-storage-v1-rev135-1.24.1.jar.sha1 @@ -0,0 +1 @@ +28d3d391dfc7e7e7951760708ad2f48cecacf38f \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-auth-library-credentials-0.10.0.jar.sha1 b/plugins/repository-gcs/licenses/google-auth-library-credentials-0.10.0.jar.sha1 new file mode 100644 index 000000000000..c8258d69326b --- /dev/null +++ b/plugins/repository-gcs/licenses/google-auth-library-credentials-0.10.0.jar.sha1 @@ -0,0 +1 @@ +f981288bd84fe6d140ed70d1d8dbe994a64fa3cc \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-auth-library-credentials-0.9.1.jar.sha1 b/plugins/repository-gcs/licenses/google-auth-library-credentials-0.9.1.jar.sha1 deleted file mode 100644 index 0922a53d2e35..000000000000 --- a/plugins/repository-gcs/licenses/google-auth-library-credentials-0.9.1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -25e0f45f3b3d1b4fccc8944845e51a7a4f359652 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.10.0.jar.sha1 b/plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.10.0.jar.sha1 new file mode 100644 index 000000000000..f55ef7c9c215 --- /dev/null +++ b/plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.10.0.jar.sha1 @@ -0,0 +1 @@ +c079a62086121973a23d90f54e2b8c13050fa39d \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.9.1.jar.sha1 b/plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.9.1.jar.sha1 deleted file mode 100644 index 100a44c18721..000000000000 --- a/plugins/repository-gcs/licenses/google-auth-library-oauth2-http-0.9.1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -c0fe3a39b0f28d59de1986b3c50f018cd7cb9ec2 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-cloud-core-1.28.0.jar.sha1 b/plugins/repository-gcs/licenses/google-cloud-core-1.28.0.jar.sha1 deleted file mode 100644 index 071533f22783..000000000000 --- a/plugins/repository-gcs/licenses/google-cloud-core-1.28.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -c0e88c78ce17c92d76bf46345faf3fa68833b216 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-cloud-core-1.40.0.jar.sha1 b/plugins/repository-gcs/licenses/google-cloud-core-1.40.0.jar.sha1 new file mode 100644 index 000000000000..7562ead12e9f --- /dev/null +++ b/plugins/repository-gcs/licenses/google-cloud-core-1.40.0.jar.sha1 @@ -0,0 +1 @@ +4985701f989030e262cf8f4e38cc954115f5b082 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-cloud-core-http-1.28.0.jar.sha1 b/plugins/repository-gcs/licenses/google-cloud-core-http-1.28.0.jar.sha1 deleted file mode 100644 index fed3fc257c32..000000000000 --- a/plugins/repository-gcs/licenses/google-cloud-core-http-1.28.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -7b4559a9513abd98da50958c56a10f8ae00cb0f7 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-cloud-core-http-1.40.0.jar.sha1 b/plugins/repository-gcs/licenses/google-cloud-core-http-1.40.0.jar.sha1 new file mode 100644 index 000000000000..2761bfdc745c --- /dev/null +++ b/plugins/repository-gcs/licenses/google-cloud-core-http-1.40.0.jar.sha1 @@ -0,0 +1 @@ +67f5806beda32894f1e6c9527925b64199fd2e4f \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-cloud-storage-1.28.0.jar.sha1 b/plugins/repository-gcs/licenses/google-cloud-storage-1.28.0.jar.sha1 deleted file mode 100644 index f49152ea0564..000000000000 --- a/plugins/repository-gcs/licenses/google-cloud-storage-1.28.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -226019ae816b42c59f1b06999aeeb73722b87200 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-cloud-storage-1.40.0.jar.sha1 b/plugins/repository-gcs/licenses/google-cloud-storage-1.40.0.jar.sha1 new file mode 100644 index 000000000000..33e83b73712f --- /dev/null +++ b/plugins/repository-gcs/licenses/google-cloud-storage-1.40.0.jar.sha1 @@ -0,0 +1 @@ +fabefef46f07d1e334123f0de17702708b4dfbd1 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-1.23.0.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-1.23.0.jar.sha1 deleted file mode 100644 index 5526275d5a15..000000000000 --- a/plugins/repository-gcs/licenses/google-http-client-1.23.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8e86c84ff3c98eca6423e97780325b299133d858 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-1.24.1.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-1.24.1.jar.sha1 new file mode 100644 index 000000000000..46b99f23e470 --- /dev/null +++ b/plugins/repository-gcs/licenses/google-http-client-1.24.1.jar.sha1 @@ -0,0 +1 @@ +396eac8d3fb1332675f82b208f48a469d64f3b4a \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-appengine-1.23.0.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-appengine-1.23.0.jar.sha1 deleted file mode 100644 index 823c3a85089a..000000000000 --- a/plugins/repository-gcs/licenses/google-http-client-appengine-1.23.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -0eda0d0f758c1cc525866e52e1226c4eb579d130 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-appengine-1.24.1.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-appengine-1.24.1.jar.sha1 new file mode 100644 index 000000000000..e39f63fe33ae --- /dev/null +++ b/plugins/repository-gcs/licenses/google-http-client-appengine-1.24.1.jar.sha1 @@ -0,0 +1 @@ +8535031ae10bf6a196e68f25e10c0d6382699cb6 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-jackson-1.23.0.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-jackson-1.23.0.jar.sha1 deleted file mode 100644 index 85ba0ab798d0..000000000000 --- a/plugins/repository-gcs/licenses/google-http-client-jackson-1.23.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -a72ea3a197937ef63a893e73df312dac0d813663 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-jackson-1.24.1.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-jackson-1.24.1.jar.sha1 new file mode 100644 index 000000000000..f6b9694abaa6 --- /dev/null +++ b/plugins/repository-gcs/licenses/google-http-client-jackson-1.24.1.jar.sha1 @@ -0,0 +1 @@ +02c88e77c14effdda76f02a0eac968de74e0bd4e \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-jackson2-1.23.0.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-jackson2-1.23.0.jar.sha1 deleted file mode 100644 index 510856a517f0..000000000000 --- a/plugins/repository-gcs/licenses/google-http-client-jackson2-1.23.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -fd6761f4046a8cb0455e6fa5f58e12b061e9826e \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-http-client-jackson2-1.24.1.jar.sha1 b/plugins/repository-gcs/licenses/google-http-client-jackson2-1.24.1.jar.sha1 new file mode 100644 index 000000000000..634b7d9198c8 --- /dev/null +++ b/plugins/repository-gcs/licenses/google-http-client-jackson2-1.24.1.jar.sha1 @@ -0,0 +1 @@ +2ad1dffd8a450055e68d8004fe003033b751d761 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-oauth-client-1.23.0.jar.sha1 b/plugins/repository-gcs/licenses/google-oauth-client-1.23.0.jar.sha1 deleted file mode 100644 index 036812b88b5e..000000000000 --- a/plugins/repository-gcs/licenses/google-oauth-client-1.23.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e57ea1e2220bda5a2bd24ff17860212861f3c5cf \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/google-oauth-client-1.24.1.jar.sha1 b/plugins/repository-gcs/licenses/google-oauth-client-1.24.1.jar.sha1 new file mode 100644 index 000000000000..2d89939674a5 --- /dev/null +++ b/plugins/repository-gcs/licenses/google-oauth-client-1.24.1.jar.sha1 @@ -0,0 +1 @@ +7b0e0218b96808868c23a7d0b40566a713931d9f \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/grpc-context-1.12.0.jar.sha1 b/plugins/repository-gcs/licenses/grpc-context-1.12.0.jar.sha1 new file mode 100644 index 000000000000..57f37a81c960 --- /dev/null +++ b/plugins/repository-gcs/licenses/grpc-context-1.12.0.jar.sha1 @@ -0,0 +1 @@ +5b63a170b786051a42cce08118d5ea3c8f60f749 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/grpc-context-1.9.0.jar.sha1 b/plugins/repository-gcs/licenses/grpc-context-1.9.0.jar.sha1 deleted file mode 100644 index 02bac0e49207..000000000000 --- a/plugins/repository-gcs/licenses/grpc-context-1.9.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -28b0836f48c9705abf73829bbc536dba29a1329a \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/gson-2.7.jar.sha1 b/plugins/repository-gcs/licenses/gson-2.7.jar.sha1 new file mode 100644 index 000000000000..b3433f306eb3 --- /dev/null +++ b/plugins/repository-gcs/licenses/gson-2.7.jar.sha1 @@ -0,0 +1 @@ +751f548c85fa49f330cecbb1875893f971b33c4e \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/jackson-core-asl-LICENSE.txt b/plugins/repository-gcs/licenses/gson-LICENSE.txt similarity index 100% rename from plugins/repository-gcs/licenses/jackson-core-asl-LICENSE.txt rename to plugins/repository-gcs/licenses/gson-LICENSE.txt diff --git a/plugins/repository-gcs/licenses/jackson-core-asl-NOTICE.txt b/plugins/repository-gcs/licenses/gson-NOTICE.txt similarity index 100% rename from plugins/repository-gcs/licenses/jackson-core-asl-NOTICE.txt rename to plugins/repository-gcs/licenses/gson-NOTICE.txt diff --git a/plugins/repository-gcs/licenses/httpclient-4.5.2.jar.sha1 b/plugins/repository-gcs/licenses/httpclient-4.5.2.jar.sha1 new file mode 100644 index 000000000000..6937112a09fb --- /dev/null +++ b/plugins/repository-gcs/licenses/httpclient-4.5.2.jar.sha1 @@ -0,0 +1 @@ +733db77aa8d9b2d68015189df76ab06304406e50 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/old/httpclient-LICENSE.txt b/plugins/repository-gcs/licenses/httpclient-LICENSE.txt similarity index 100% rename from plugins/repository-gcs/licenses/old/httpclient-LICENSE.txt rename to plugins/repository-gcs/licenses/httpclient-LICENSE.txt diff --git a/plugins/repository-gcs/licenses/old/httpclient-NOTICE.txt b/plugins/repository-gcs/licenses/httpclient-NOTICE.txt similarity index 100% rename from plugins/repository-gcs/licenses/old/httpclient-NOTICE.txt rename to plugins/repository-gcs/licenses/httpclient-NOTICE.txt diff --git a/plugins/repository-gcs/licenses/httpcore-4.4.5.jar.sha1 b/plugins/repository-gcs/licenses/httpcore-4.4.5.jar.sha1 new file mode 100644 index 000000000000..581726601745 --- /dev/null +++ b/plugins/repository-gcs/licenses/httpcore-4.4.5.jar.sha1 @@ -0,0 +1 @@ +e7501a1b34325abb00d17dde96150604a0658b54 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/jackson-LICENSE b/plugins/repository-gcs/licenses/jackson-LICENSE new file mode 100644 index 000000000000..f5f45d26a49d --- /dev/null +++ b/plugins/repository-gcs/licenses/jackson-LICENSE @@ -0,0 +1,8 @@ +This copy of Jackson JSON processor streaming parser/generator is licensed under the +Apache (Software) License, version 2.0 ("the License"). +See the License for details about distribution rights, and the +specific rights regarding derivate works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/plugins/repository-gcs/licenses/jackson-NOTICE b/plugins/repository-gcs/licenses/jackson-NOTICE new file mode 100644 index 000000000000..4c976b7b4cc5 --- /dev/null +++ b/plugins/repository-gcs/licenses/jackson-NOTICE @@ -0,0 +1,20 @@ +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers, as well as supported +commercially by FasterXML.com. + +## Licensing + +Jackson core and extension components may licensed under different licenses. +To find the details that apply to this artifact see the accompanying LICENSE file. +For more information, including possible other licensing options, contact +FasterXML.com (http://fasterxml.com). + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. diff --git a/plugins/repository-gcs/licenses/jackson-core-asl-1.9.11.jar.sha1 b/plugins/repository-gcs/licenses/jackson-core-asl-1.9.11.jar.sha1 new file mode 100644 index 000000000000..ed70030899aa --- /dev/null +++ b/plugins/repository-gcs/licenses/jackson-core-asl-1.9.11.jar.sha1 @@ -0,0 +1 @@ +e32303ef8bd18a5c9272780d49b81c95e05ddf43 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/jackson-core-asl-1.9.13.jar.sha1 b/plugins/repository-gcs/licenses/jackson-core-asl-1.9.13.jar.sha1 deleted file mode 100644 index c5016bf828d6..000000000000 --- a/plugins/repository-gcs/licenses/jackson-core-asl-1.9.13.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -3c304d70f42f832e0a86d45bd437f692129299a4 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/old/google-LICENSE.txt b/plugins/repository-gcs/licenses/old/google-LICENSE.txt deleted file mode 100644 index 980a15ac24ee..000000000000 --- a/plugins/repository-gcs/licenses/old/google-LICENSE.txt +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed 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. diff --git a/plugins/repository-gcs/licenses/old/google-NOTICE.txt b/plugins/repository-gcs/licenses/old/google-NOTICE.txt deleted file mode 100644 index 8d1c8b69c3fc..000000000000 --- a/plugins/repository-gcs/licenses/old/google-NOTICE.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/plugins/repository-gcs/licenses/old/httpcore-LICENSE.txt b/plugins/repository-gcs/licenses/old/httpcore-LICENSE.txt deleted file mode 100644 index 72819a9f06f2..000000000000 --- a/plugins/repository-gcs/licenses/old/httpcore-LICENSE.txt +++ /dev/null @@ -1,241 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - -========================================================================= - -This project contains annotations in the package org.apache.http.annotation -which are derived from JCIP-ANNOTATIONS -Copyright (c) 2005 Brian Goetz and Tim Peierls. -See http://www.jcip.net and the Creative Commons Attribution License -(http://creativecommons.org/licenses/by/2.5) -Full text: http://creativecommons.org/licenses/by/2.5/legalcode - -License - -THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. - -BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. - -1. Definitions - - "Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with a number of other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License. - "Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License. - "Licensor" means the individual or entity that offers the Work under the terms of this License. - "Original Author" means the individual or entity who created the Work. - "Work" means the copyrightable work of authorship offered under the terms of this License. - "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. - -2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws. - -3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: - - to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works; - to create and reproduce Derivative Works; - to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works; - to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works. - - For the avoidance of doubt, where the work is a musical composition: - Performance Royalties Under Blanket Licenses. Licensor waives the exclusive right to collect, whether individually or via a performance rights society (e.g. ASCAP, BMI, SESAC), royalties for the public performance or public digital performance (e.g. webcast) of the Work. - Mechanical Rights and Statutory Royalties. Licensor waives the exclusive right to collect, whether individually or via a music rights agency or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions). - Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor waives the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions). - -The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved. - -4. Restrictions.The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: - - You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any credit as required by clause 4(b), as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any credit as required by clause 4(b), as requested. - If you distribute, publicly display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or (ii) if the Original Author and/or Licensor designate another party or parties (e.g. a sponsor institute, publishing entity, journal) for attribution in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). Such credit may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear where any other comparable authorship credit appears and in a manner at least as prominent as such other comparable authorship credit. - -5. Representations, Warranties and Disclaimer - -UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. - -6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. Termination - - This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works or Collective Works from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. - Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. - -8. Miscellaneous - - Each time You distribute or publicly digitally perform the Work or a Collective Work, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. - Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. - If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. - This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. diff --git a/plugins/repository-gcs/licenses/old/httpcore-NOTICE.txt b/plugins/repository-gcs/licenses/old/httpcore-NOTICE.txt deleted file mode 100644 index c0be50a505ec..000000000000 --- a/plugins/repository-gcs/licenses/old/httpcore-NOTICE.txt +++ /dev/null @@ -1,8 +0,0 @@ -Apache HttpComponents Core -Copyright 2005-2014 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - -This project contains annotations derived from JCIP-ANNOTATIONS -Copyright (c) 2005 Brian Goetz and Tim Peierls. See http://www.jcip.net diff --git a/plugins/repository-gcs/licenses/opencensus-api-0.11.1.jar.sha1 b/plugins/repository-gcs/licenses/opencensus-api-0.11.1.jar.sha1 deleted file mode 100644 index 61d8e3b14814..000000000000 --- a/plugins/repository-gcs/licenses/opencensus-api-0.11.1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -54689fbf750a7f26e34fa1f1f96b883c53f51486 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/opencensus-api-0.15.0.jar.sha1 b/plugins/repository-gcs/licenses/opencensus-api-0.15.0.jar.sha1 new file mode 100644 index 000000000000..e200e2e24a7d --- /dev/null +++ b/plugins/repository-gcs/licenses/opencensus-api-0.15.0.jar.sha1 @@ -0,0 +1 @@ +9a098392b287d7924660837f4eba0ce252013683 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.11.1.jar.sha1 b/plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.11.1.jar.sha1 deleted file mode 100644 index c0b04f0f8ccc..000000000000 --- a/plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.11.1.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -82e572b41e81ecf58d0d1e9a3953a05aa8f9c84b \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.15.0.jar.sha1 b/plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.15.0.jar.sha1 new file mode 100644 index 000000000000..b642e1ebebd5 --- /dev/null +++ b/plugins/repository-gcs/licenses/opencensus-contrib-http-util-0.15.0.jar.sha1 @@ -0,0 +1 @@ +d88690591669d9b5ba6d91d9eac7736e58ccf3da \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/proto-google-common-protos-LICENSE.txt b/plugins/repository-gcs/licenses/proto-google-LICENSE.txt similarity index 100% rename from plugins/repository-gcs/licenses/proto-google-common-protos-LICENSE.txt rename to plugins/repository-gcs/licenses/proto-google-LICENSE.txt diff --git a/plugins/repository-gcs/licenses/proto-google-common-protos-NOTICE.txt b/plugins/repository-gcs/licenses/proto-google-NOTICE.txt similarity index 100% rename from plugins/repository-gcs/licenses/proto-google-common-protos-NOTICE.txt rename to plugins/repository-gcs/licenses/proto-google-NOTICE.txt diff --git a/plugins/repository-gcs/licenses/proto-google-common-protos-1.12.0.jar.sha1 b/plugins/repository-gcs/licenses/proto-google-common-protos-1.12.0.jar.sha1 new file mode 100644 index 000000000000..47f3c178a68c --- /dev/null +++ b/plugins/repository-gcs/licenses/proto-google-common-protos-1.12.0.jar.sha1 @@ -0,0 +1 @@ +1140cc74df039deb044ed0e320035e674dc13062 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/proto-google-common-protos-1.8.0.jar.sha1 b/plugins/repository-gcs/licenses/proto-google-common-protos-1.8.0.jar.sha1 deleted file mode 100644 index 0a2dee4447e9..000000000000 --- a/plugins/repository-gcs/licenses/proto-google-common-protos-1.8.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -b3282312ba82536fc9a7778cabfde149a875e877 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/proto-google-iam-v1-0.12.0.jar.sha1 b/plugins/repository-gcs/licenses/proto-google-iam-v1-0.12.0.jar.sha1 new file mode 100644 index 000000000000..2bfae3456d49 --- /dev/null +++ b/plugins/repository-gcs/licenses/proto-google-iam-v1-0.12.0.jar.sha1 @@ -0,0 +1 @@ +ea312c0250a5d0a7cdd1b20bc2c3259938b79855 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/protobuf-LICENSE.txt b/plugins/repository-gcs/licenses/protobuf-LICENSE.txt new file mode 100644 index 000000000000..19b305b00060 --- /dev/null +++ b/plugins/repository-gcs/licenses/protobuf-LICENSE.txt @@ -0,0 +1,32 @@ +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. diff --git a/plugins/repository-gcs/licenses/protobuf-NOTICE.txt b/plugins/repository-gcs/licenses/protobuf-NOTICE.txt new file mode 100644 index 000000000000..19b305b00060 --- /dev/null +++ b/plugins/repository-gcs/licenses/protobuf-NOTICE.txt @@ -0,0 +1,32 @@ +Copyright 2008 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. diff --git a/plugins/repository-gcs/licenses/protobuf-java-3.6.0.jar.sha1 b/plugins/repository-gcs/licenses/protobuf-java-3.6.0.jar.sha1 new file mode 100644 index 000000000000..050ebd44c928 --- /dev/null +++ b/plugins/repository-gcs/licenses/protobuf-java-3.6.0.jar.sha1 @@ -0,0 +1 @@ +5333f7e422744d76840c08a106e28e519fbe3acd \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/protobuf-java-util-3.6.0.jar.sha1 b/plugins/repository-gcs/licenses/protobuf-java-util-3.6.0.jar.sha1 new file mode 100644 index 000000000000..cc85974499a6 --- /dev/null +++ b/plugins/repository-gcs/licenses/protobuf-java-util-3.6.0.jar.sha1 @@ -0,0 +1 @@ +3680d0042d4fe0b95ada844ff24da0698a7f0773 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/threetenbp-1.3.3.jar.sha1 b/plugins/repository-gcs/licenses/threetenbp-1.3.3.jar.sha1 new file mode 100644 index 000000000000..9273043e1452 --- /dev/null +++ b/plugins/repository-gcs/licenses/threetenbp-1.3.3.jar.sha1 @@ -0,0 +1 @@ +3ea31c96676ff12ab56be0b1af6fff61d1a4f1f2 \ No newline at end of file diff --git a/plugins/repository-gcs/licenses/threetenbp-1.3.6.jar.sha1 b/plugins/repository-gcs/licenses/threetenbp-1.3.6.jar.sha1 deleted file mode 100644 index 65c16fed4a07..000000000000 --- a/plugins/repository-gcs/licenses/threetenbp-1.3.6.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -89dcc04a7e028c3c963413a71f950703cf51f057 \ No newline at end of file From 66e458b78b547d68ed74c4ddae4924b4fdb0d0b2 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Fri, 24 Aug 2018 12:36:23 +0300 Subject: [PATCH 134/283] Muted testEmptyAuthorizedIndicesSearchForAllDisallowNoIndices --- .../org/elasticsearch/xpack/security/authz/ReadActionsTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java index 76568d3d48b5..a88dafece325 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java @@ -102,6 +102,7 @@ public void testEmptyAuthorizedIndicesSearchForAll() { assertNoSearchHits(client().prepareSearch().get()); } + @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/33123") public void testEmptyAuthorizedIndicesSearchForAllDisallowNoIndices() { createIndicesWithRandomAliases("index1", "index2"); IndexNotFoundException e = expectThrows(IndexNotFoundException.class, () -> client().prepareSearch() From b0f22d67c46b7882ac4c53e842b3206e9c5a0e86 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 24 Aug 2018 16:56:29 +0700 Subject: [PATCH 135/283] fixed not returning response instance --- .../java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java index eaded2456306..b5d6697fc73c 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrStatsAction.java @@ -40,7 +40,7 @@ private CcrStatsAction() { @Override public TasksResponse newResponse() { - return null; + return new TasksResponse(); } public static class TasksResponse extends BaseTasksResponse implements ToXContentObject { From 879a90b99922f29c8764ce5de7d4349647cd56b7 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Fri, 24 Aug 2018 11:57:46 +0200 Subject: [PATCH 136/283] [Rollup] Move getMetadata() methods out of rollup config objects (#32579) This committ removes the getMetadata() methods from the DateHistoGroupConfig and HistoGroupConfig objects. This way the configuration objects do not rely on RollupField.formatMetaField() anymore and do not expose a getMetadata() method that is tighlty coupled to the rollup indexer. --- .../rollup/job/DateHistogramGroupConfig.java | 4 -- .../core/rollup/job/HistogramGroupConfig.java | 5 +- .../xpack/rollup/job/RollupIndexer.java | 21 ++++++-- .../xpack/rollup/job/RollupIndexerTests.java | 49 +++++++++++++++++++ 4 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java index 77dfa1cbbb1c..281277043c82 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java @@ -211,10 +211,6 @@ public Map toAggCap() { return map; } - public Map getMetadata() { - return Collections.singletonMap(RollupField.formatMetaField(RollupField.INTERVAL), interval.toString()); - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java index 0480050bf52f..1e1f88a7c20e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; @@ -115,8 +116,8 @@ public Map toAggCap() { return map; } - public Map getMetadata() { - return Collections.singletonMap(RollupField.formatMetaField(RollupField.INTERVAL), interval); + public Set getAllFields() { + return Arrays.stream(fields).collect(Collectors.toSet()); } public void validateMappings(Map> fieldCapsResponse, diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java index 87294706b3b7..d1db021361c8 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.core.rollup.RollupField; import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; import org.elasticsearch.xpack.core.rollup.job.GroupConfig; +import org.elasticsearch.xpack.core.rollup.job.HistogramGroupConfig; import org.elasticsearch.xpack.core.rollup.job.IndexerState; import org.elasticsearch.xpack.core.rollup.job.RollupJob; import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; @@ -392,15 +393,12 @@ private SearchRequest buildSearchRequest() { private CompositeAggregationBuilder createCompositeBuilder(RollupJobConfig config) { final GroupConfig groupConfig = config.getGroupConfig(); List> builders = new ArrayList<>(); - Map metadata = new HashMap<>(); // Add all the agg builders to our request in order: date_histo -> histo -> terms if (groupConfig != null) { builders.addAll(groupConfig.getDateHistogram().toBuilders()); - metadata.putAll(groupConfig.getDateHistogram().getMetadata()); if (groupConfig.getHistogram() != null) { builders.addAll(groupConfig.getHistogram().toBuilders()); - metadata.putAll(groupConfig.getHistogram().getMetadata()); } if (groupConfig.getTerms() != null) { builders.addAll(groupConfig.getTerms().toBuilders()); @@ -409,6 +407,8 @@ private CompositeAggregationBuilder createCompositeBuilder(RollupJobConfig confi CompositeAggregationBuilder composite = new CompositeAggregationBuilder(AGGREGATION_NAME, builders); config.getMetricsConfig().forEach(m -> m.toBuilders().forEach(composite::subAggregation)); + + final Map metadata = createMetadata(groupConfig); if (metadata.isEmpty() == false) { composite.setMetaData(metadata); } @@ -441,5 +441,20 @@ private QueryBuilder createBoundaryQuery(Map position) { .format("epoch_millis"); return query; } + + static Map createMetadata(final GroupConfig groupConfig) { + final Map metadata = new HashMap<>(); + if (groupConfig != null) { + // Add all the metadata in order: date_histo -> histo + final DateHistogramGroupConfig dateHistogram = groupConfig.getDateHistogram(); + metadata.put(RollupField.formatMetaField(RollupField.INTERVAL), dateHistogram.getInterval().toString()); + + final HistogramGroupConfig histogram = groupConfig.getHistogram(); + if (histogram != null) { + metadata.put(RollupField.formatMetaField(RollupField.INTERVAL), histogram.getInterval()); + } + } + return metadata; + } } diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerTests.java new file mode 100644 index 000000000000..5ab85e2ffa74 --- /dev/null +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerTests.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.rollup.job; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers; +import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; +import org.elasticsearch.xpack.core.rollup.job.GroupConfig; +import org.elasticsearch.xpack.core.rollup.job.HistogramGroupConfig; + +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; + +public class RollupIndexerTests extends ESTestCase { + + public void testCreateMetadataNoGroupConfig() { + final Map metadata = RollupIndexer.createMetadata(null); + assertNotNull(metadata); + assertTrue(metadata.isEmpty()); + } + + public void testCreateMetadataWithDateHistogramGroupConfigOnly() { + final DateHistogramGroupConfig dateHistogram = ConfigTestHelpers.randomDateHistogramGroupConfig(random()); + final GroupConfig groupConfig = new GroupConfig(dateHistogram); + + final Map metadata = RollupIndexer.createMetadata(groupConfig); + assertEquals(1, metadata.size()); + assertTrue(metadata.containsKey("_rollup.interval")); + Object value = metadata.get("_rollup.interval"); + assertThat(value, equalTo(dateHistogram.getInterval().toString())); + } + + public void testCreateMetadata() { + final DateHistogramGroupConfig dateHistogram = ConfigTestHelpers.randomDateHistogramGroupConfig(random()); + final HistogramGroupConfig histogram = ConfigTestHelpers.randomHistogramGroupConfig(random()); + final GroupConfig groupConfig = new GroupConfig(dateHistogram, histogram, null); + + final Map metadata = RollupIndexer.createMetadata(groupConfig); + assertEquals(1, metadata.size()); + assertTrue(metadata.containsKey("_rollup.interval")); + Object value = metadata.get("_rollup.interval"); + assertThat(value, equalTo(histogram.getInterval())); + } +} + From 1d8745036f19786f62808cc78a5dedbf2cfcda21 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Fri, 24 Aug 2018 13:14:03 +0300 Subject: [PATCH 137/283] Muted testListenersThrowingExceptionsDoNotCauseOtherListenersToBeSkipped --- .../elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java index 869a320fb638..0f98acefe5b7 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java @@ -31,6 +31,7 @@ public class SchedulerEngineTests extends ESTestCase { + @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/33124") public void testListenersThrowingExceptionsDoNotCauseOtherListenersToBeSkipped() throws InterruptedException { final Logger mockLogger = mock(Logger.class); final SchedulerEngine engine = new SchedulerEngine(Settings.EMPTY, Clock.systemUTC(), mockLogger); From 619e0b28b97b8ea7cddf6b29aaa1a2756da9b363 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 24 Aug 2018 06:53:44 -0400 Subject: [PATCH 138/283] Add hook to skip asserting x-content equivalence (#33114) This commit adds a hook to AbstractSerializingTestCase to enable skipping asserting that the x-content of the test instance and an instance parsed from the x-content of the test instance are the same. While we usually expect these to be the same, they will not be the same when exceptions are involved because the x-content there is lossy. --- .../test/AbstractSerializingTestCase.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java index 6ec32f6654ff..5aeb30bfdbd5 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.test; import org.elasticsearch.common.Strings; @@ -34,9 +35,17 @@ public abstract class AbstractSerializingTestCase Date: Fri, 24 Aug 2018 07:45:16 -0400 Subject: [PATCH 139/283] Fix race condition in scheduler engine test This commit addresses a race condition in the scheduler engine test that a listener that throws an exception does not cause other listeners to be skipped. The race here is that we were counting down a latch, and then throwing an exception yet an assertion that expected the exception to have been thrown already could execute after the latch was counted down for the final time but before the exception was thrown and acted upon by the scheduler engine. This commit addresses this by moving the counting down of the latch to definitely be after the exception was acted upon by the scheduler engine. --- .../xpack/core/scheduler/SchedulerEngineTests.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java index 0f98acefe5b7..5ab7b805cc1f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/scheduler/SchedulerEngineTests.java @@ -21,9 +21,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import static org.hamcrest.Matchers.any; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.equalTo; 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.times; import static org.mockito.Mockito.verify; @@ -31,7 +34,6 @@ public class SchedulerEngineTests extends ESTestCase { - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/33124") public void testListenersThrowingExceptionsDoNotCauseOtherListenersToBeSkipped() throws InterruptedException { final Logger mockLogger = mock(Logger.class); final SchedulerEngine engine = new SchedulerEngine(Settings.EMPTY, Clock.systemUTC(), mockLogger); @@ -40,6 +42,7 @@ public void testListenersThrowingExceptionsDoNotCauseOtherListenersToBeSkipped() final int numberOfListeners = randomIntBetween(1, 32); int numberOfFailingListeners = 0; final CountDownLatch latch = new CountDownLatch(numberOfListeners); + for (int i = 0; i < numberOfListeners; i++) { final AtomicBoolean trigger = new AtomicBoolean(); final SchedulerEngine.Listener listener; @@ -55,12 +58,17 @@ public void testListenersThrowingExceptionsDoNotCauseOtherListenersToBeSkipped() numberOfFailingListeners++; listener = event -> { if (trigger.compareAndSet(false, true)) { - latch.countDown(); + // we count down the latch after this exception is caught and mock logged in SchedulerEngine#notifyListeners throw new RuntimeException(getTestName()); } else { fail("listener invoked twice"); } }; + doAnswer(invocationOnMock -> { + // this happens after the listener has been notified, threw an exception, and then mock logged the exception + latch.countDown(); + return null; + }).when(mockLogger).warn(argThat(any(ParameterizedMessage.class)), argThat(any(RuntimeException.class))); } listeners.add(Tuple.tuple(listener, trigger)); } @@ -135,7 +143,7 @@ public void testListenersThrowingExceptionsDoNotCauseNextScheduledTaskToBeSkippe listenersLatch.await(); assertTrue(listeners.stream().map(Tuple::v2).allMatch(count -> count.get() == numberOfSchedules)); latch.await(); - assertFailedListenerLogMessage(mockLogger, numberOfListeners * numberOfSchedules); + assertFailedListenerLogMessage(mockLogger, numberOfSchedules * numberOfListeners); verifyNoMoreInteractions(mockLogger); } finally { engine.stop(); From 7e5efad92918e95676fd047a2897cc626441661f Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Fri, 24 Aug 2018 15:31:41 +0200 Subject: [PATCH 140/283] [Rollup] Move toAggCap() methods out of rollup config objects (#32583) --- .../core/rollup/action/RollupJobCaps.java | 135 +++++++++++------- .../rollup/job/DateHistogramGroupConfig.java | 20 +-- .../core/rollup/job/HistogramGroupConfig.java | 18 +-- .../xpack/core/rollup/job/MetricConfig.java | 8 -- .../core/rollup/job/TermsGroupConfig.java | 10 -- 5 files changed, 89 insertions(+), 102 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupJobCaps.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupJobCaps.java index 1b8eb736084a..054d08df999f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupJobCaps.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/RollupJobCaps.java @@ -11,15 +11,26 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; +import org.elasticsearch.xpack.core.rollup.job.GroupConfig; +import org.elasticsearch.xpack.core.rollup.job.HistogramGroupConfig; +import org.elasticsearch.xpack.core.rollup.job.MetricConfig; import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; +import org.elasticsearch.xpack.core.rollup.job.TermsGroupConfig; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; + +import static java.util.Collections.singletonMap; /** * Represents the Rollup capabilities for a specific job on a single rollup index @@ -42,52 +53,7 @@ public RollupJobCaps(RollupJobConfig job) { jobID = job.getId(); rollupIndex = job.getRollupIndex(); indexPattern = job.getIndexPattern(); - Map dateHistoAggCap = job.getGroupConfig().getDateHistogram().toAggCap(); - String dateField = job.getGroupConfig().getDateHistogram().getField(); - RollupFieldCaps fieldCaps = fieldCapLookup.get(dateField); - if (fieldCaps == null) { - fieldCaps = new RollupFieldCaps(); - } - fieldCaps.addAgg(dateHistoAggCap); - fieldCapLookup.put(dateField, fieldCaps); - - if (job.getGroupConfig().getHistogram() != null) { - Map histoAggCap = job.getGroupConfig().getHistogram().toAggCap(); - Arrays.stream(job.getGroupConfig().getHistogram().getFields()).forEach(field -> { - RollupFieldCaps caps = fieldCapLookup.get(field); - if (caps == null) { - caps = new RollupFieldCaps(); - } - caps.addAgg(histoAggCap); - fieldCapLookup.put(field, caps); - }); - } - - if (job.getGroupConfig().getTerms() != null) { - Map histoAggCap = job.getGroupConfig().getTerms().toAggCap(); - Arrays.stream(job.getGroupConfig().getTerms().getFields()).forEach(field -> { - RollupFieldCaps caps = fieldCapLookup.get(field); - if (caps == null) { - caps = new RollupFieldCaps(); - } - caps.addAgg(histoAggCap); - fieldCapLookup.put(field, caps); - }); - } - - if (job.getMetricsConfig().size() > 0) { - job.getMetricsConfig().forEach(metricConfig -> { - List> metrics = metricConfig.toAggCap(); - metrics.forEach(m -> { - RollupFieldCaps caps = fieldCapLookup.get(metricConfig.getField()); - if (caps == null) { - caps = new RollupFieldCaps(); - } - caps.addAgg(m); - fieldCapLookup.put(metricConfig.getField(), caps); - }); - }); - } + fieldCapLookup = createRollupFieldCaps(job); } public RollupJobCaps(StreamInput in) throws IOException { @@ -149,8 +115,8 @@ public boolean equals(Object other) { RollupJobCaps that = (RollupJobCaps) other; return Objects.equals(this.jobID, that.jobID) - && Objects.equals(this.rollupIndex, that.rollupIndex) - && Objects.equals(this.fieldCapLookup, that.fieldCapLookup); + && Objects.equals(this.rollupIndex, that.rollupIndex) + && Objects.equals(this.fieldCapLookup, that.fieldCapLookup); } @Override @@ -158,6 +124,77 @@ public int hashCode() { return Objects.hash(jobID, rollupIndex, fieldCapLookup); } + static Map createRollupFieldCaps(final RollupJobConfig rollupJobConfig) { + final Map fieldCapLookup = new HashMap<>(); + + final GroupConfig groupConfig = rollupJobConfig.getGroupConfig(); + if (groupConfig != null) { + // Create RollupFieldCaps for the date histogram + final DateHistogramGroupConfig dateHistogram = groupConfig.getDateHistogram(); + final Map dateHistogramAggCap = new HashMap<>(); + dateHistogramAggCap.put("agg", DateHistogramAggregationBuilder.NAME); + dateHistogramAggCap.put(DateHistogramGroupConfig.INTERVAL, dateHistogram.getInterval().toString()); + if (dateHistogram.getDelay() != null) { + dateHistogramAggCap.put(DateHistogramGroupConfig.DELAY, dateHistogram.getDelay().toString()); + } + dateHistogramAggCap.put(DateHistogramGroupConfig.TIME_ZONE, dateHistogram.getTimeZone()); + + final RollupFieldCaps dateHistogramFieldCaps = new RollupFieldCaps(); + dateHistogramFieldCaps.addAgg(dateHistogramAggCap); + fieldCapLookup.put(dateHistogram.getField(), dateHistogramFieldCaps); + + // Create RollupFieldCaps for the histogram + final HistogramGroupConfig histogram = groupConfig.getHistogram(); + if (histogram != null) { + final Map histogramAggCap = new HashMap<>(); + histogramAggCap.put("agg", HistogramAggregationBuilder.NAME); + histogramAggCap.put(HistogramGroupConfig.INTERVAL, histogram.getInterval()); + for (String field : histogram.getFields()) { + RollupFieldCaps caps = fieldCapLookup.get(field); + if (caps == null) { + caps = new RollupFieldCaps(); + } + caps.addAgg(histogramAggCap); + fieldCapLookup.put(field, caps); + } + } + + // Create RollupFieldCaps for the term + final TermsGroupConfig terms = groupConfig.getTerms(); + if (terms != null) { + final Map termsAggCap = singletonMap("agg", TermsAggregationBuilder.NAME); + for (String field : terms.getFields()) { + RollupFieldCaps caps = fieldCapLookup.get(field); + if (caps == null) { + caps = new RollupFieldCaps(); + } + caps.addAgg(termsAggCap); + fieldCapLookup.put(field, caps); + } + } + } + + // Create RollupFieldCaps for the metrics + final List metricsConfig = rollupJobConfig.getMetricsConfig(); + if (metricsConfig.size() > 0) { + metricsConfig.forEach(metricConfig -> { + final List> metrics = metricConfig.getMetrics().stream() + .map(metric -> singletonMap("agg", (Object) metric)) + .collect(Collectors.toList()); + + metrics.forEach(m -> { + RollupFieldCaps caps = fieldCapLookup.get(metricConfig.getField()); + if (caps == null) { + caps = new RollupFieldCaps(); + } + caps.addAgg(m); + fieldCapLookup.put(metricConfig.getField(), caps); + }); + }); + } + return Collections.unmodifiableMap(fieldCapLookup); + } + public static class RollupFieldCaps implements Writeable, ToXContentObject { private List> aggs = new ArrayList<>(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java index 281277043c82..a9cc95bb07c9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -55,10 +54,10 @@ public class DateHistogramGroupConfig implements Writeable, ToXContentObject { static final String NAME = "date_histogram"; - private static final String INTERVAL = "interval"; + public static final String INTERVAL = "interval"; private static final String FIELD = "field"; public static final String TIME_ZONE = "time_zone"; - private static final String DELAY = "delay"; + public static final String DELAY = "delay"; private static final String DEFAULT_TIMEZONE = "UTC"; private static final ConstructingObjectParser PARSER; static { @@ -196,21 +195,6 @@ public List> toBuilders() { return Collections.singletonList(vsBuilder); } - /** - * @return A map representing this config object as a RollupCaps aggregation object - */ - public Map toAggCap() { - Map map = new HashMap<>(3); - map.put("agg", DateHistogramAggregationBuilder.NAME); - map.put(INTERVAL, interval.toString()); - if (delay != null) { - map.put(DELAY, delay.toString()); - } - map.put(TIME_ZONE, timeZone); - - return map; - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java index 1e1f88a7c20e..d1bc50566faf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java @@ -24,11 +24,9 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; @@ -48,7 +46,7 @@ public class HistogramGroupConfig implements Writeable, ToXContentObject { static final String NAME = "histogram"; - private static final String INTERVAL = "interval"; + public static final String INTERVAL = "interval"; private static final String FIELDS = "fields"; private static final ConstructingObjectParser PARSER; static { @@ -106,20 +104,6 @@ public List> toBuilders() { }).collect(Collectors.toList()); } - /** - * @return A map representing this config object as a RollupCaps aggregation object - */ - public Map toAggCap() { - Map map = new HashMap<>(2); - map.put("agg", HistogramAggregationBuilder.NAME); - map.put(INTERVAL, interval); - return map; - } - - public Set getAllFields() { - return Arrays.stream(fields).collect(Collectors.toSet()); - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java index cc673c4ed0d3..b4e022f55004 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java @@ -31,7 +31,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; @@ -152,13 +151,6 @@ public List toBuilders() { return aggs; } - /** - * @return A map representing this config object as a RollupCaps aggregation object - */ - public List> toAggCap() { - return metrics.stream().map(metric -> Collections.singletonMap("agg", (Object)metric)).collect(Collectors.toList()); - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java index 32507d57f32b..abd6825e9f7b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -94,15 +93,6 @@ public List> toBuilders() { }).collect(Collectors.toList()); } - /** - * @return A map representing this config object as a RollupCaps aggregation object - */ - public Map toAggCap() { - Map map = new HashMap<>(1); - map.put("agg", TermsAggregationBuilder.NAME); - return map; - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { From 7fa8a728c4c7441a738e2e493912fbec80db5a56 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 24 Aug 2018 09:48:54 -0400 Subject: [PATCH 141/283] Make CCR QA tests build again (#33113) Welp, I broke this. I merged a change to auto-discover the CCR QA tests by making :x-pack:plugin:ccr:check auto-discover the check tasks in the qa sub-project. Yet, the check tasks for these sub-projects did not depend on the necessary test tasks (as we were previously doing this directly from the ccr build file. This commit fixes this! --- .../ccr/qa/multi-cluster-with-incompatible-license/build.gradle | 1 + x-pack/plugin/ccr/qa/multi-cluster-with-security/build.gradle | 1 + x-pack/plugin/ccr/qa/multi-cluster/build.gradle | 1 + 3 files changed, 3 insertions(+) diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle index 18f1eba3b6d4..97d4008eb8c1 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-incompatible-license/build.gradle @@ -36,4 +36,5 @@ followClusterTestRunner { finalizedBy 'leaderClusterTestCluster#stop' } +check.dependsOn followClusterTest test.enabled = false 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 970c400e7327..897aed0110e1 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 @@ -71,4 +71,5 @@ followClusterTestRunner { finalizedBy 'leaderClusterTestCluster#stop' } +check.dependsOn followClusterTest test.enabled = false // no unit tests for multi-cluster-search, only the rest integration test diff --git a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle index 9d59a18ab520..cc726e1a6525 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/build.gradle +++ b/x-pack/plugin/ccr/qa/multi-cluster/build.gradle @@ -37,4 +37,5 @@ followClusterTestRunner { finalizedBy 'leaderClusterTestCluster#stop' } +check.dependsOn followClusterTest test.enabled = false // no unit tests for multi-cluster-search, only the rest integration test From 6f1ee76443c4d04095bce2c34a8cb474c5854e4a Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Fri, 24 Aug 2018 10:12:16 -0400 Subject: [PATCH 142/283] Revert "Do NOT allow termvectors on nested fields (#32728)" This reverts commit fdff8f3db0093fa15cfa161f7dec80b715a48a43. --- docs/reference/docs/termvectors.asciidoc | 4 -- .../test/termvectors/50_nested.yml | 49 ------------------- .../index/termvectors/TermVectorsService.java | 17 ++----- 3 files changed, 3 insertions(+), 67 deletions(-) delete mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml diff --git a/docs/reference/docs/termvectors.asciidoc b/docs/reference/docs/termvectors.asciidoc index 0e6078ad7b23..3cd21b21df4d 100644 --- a/docs/reference/docs/termvectors.asciidoc +++ b/docs/reference/docs/termvectors.asciidoc @@ -30,10 +30,6 @@ in similar way to the <> [WARNING] Note that the usage of `/_termvector` is deprecated in 2.0, and replaced by `/_termvectors`. -[WARNING] -Term Vectors API doesn't work on nested fields. `/_termvectors` on a nested -field and any sub-fields of a nested field returns empty results. - [float] === Return values diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml deleted file mode 100644 index a10fc7b504bf..000000000000 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/termvectors/50_nested.yml +++ /dev/null @@ -1,49 +0,0 @@ -setup: - - do: - indices.create: - index: testidx - body: - mappings: - _doc: - properties: - nested1: - type : nested - properties: - nested1-text: - type: text - object1: - properties: - object1-text: - type: text - object1-nested1: - type: nested - properties: - object1-nested1-text: - type: text - - do: - index: - index: testidx - type: _doc - id: 1 - body: - "nested1" : [{ "nested1-text": "text1" }] - "object1" : [{ "object1-text": "text2" }, "object1-nested1" : [{"object1-nested1-text" : "text3"}]] - - - do: - indices.refresh: {} - ---- -"Termvectors on nested fields should return empty results": - - - do: - termvectors: - index: testidx - type: _doc - id: 1 - fields: ["nested1", "nested1.nested1-text", "object1.object1-nested1", "object1.object1-nested1.object1-nested1-text", "object1.object1-text"] - - - is_false: term_vectors.nested1 - - is_false: term_vectors.nested1\.nested1-text # escaping as the field name contains dot - - is_false: term_vectors.object1\.object1-nested1 - - is_false: term_vectors.object1\.object1-nested1\.object1-nested1-text - - is_true: term_vectors.object1\.object1-text diff --git a/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java b/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java index 43f1a278f54c..bc77626b9427 100644 --- a/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java +++ b/server/src/main/java/org/elasticsearch/index/termvectors/TermVectorsService.java @@ -45,7 +45,6 @@ import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.mapper.ObjectMapper; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SourceFieldMapper; @@ -161,7 +160,7 @@ private static void handleFieldWildcards(IndexShard indexShard, TermVectorsReque request.selectedFields(fieldNames.toArray(Strings.EMPTY_ARRAY)); } - private static boolean isValidField(MappedFieldType fieldType, IndexShard indexShard) { + private static boolean isValidField(MappedFieldType fieldType) { // must be a string if (fieldType instanceof StringFieldType == false) { return false; @@ -170,16 +169,6 @@ private static boolean isValidField(MappedFieldType fieldType, IndexShard indexS if (fieldType.indexOptions() == IndexOptions.NONE) { return false; } - // and must not be under nested field - int dotIndex = fieldType.name().indexOf('.'); - while (dotIndex > -1) { - String parentField = fieldType.name().substring(0, dotIndex); - ObjectMapper mapper = indexShard.mapperService().getObjectMapper(parentField); - if (mapper != null && mapper.nested().isNested()) { - return false; - } - dotIndex = fieldType.name().indexOf('.', dotIndex + 1); - } return true; } @@ -188,7 +177,7 @@ private static Fields addGeneratedTermVectors(IndexShard indexShard, Engine.GetR Set validFields = new HashSet<>(); for (String field : selectedFields) { MappedFieldType fieldType = indexShard.mapperService().fullName(field); - if (isValidField(fieldType, indexShard) == false) { + if (!isValidField(fieldType)) { continue; } // already retrieved, only if the analyzer hasn't been overridden at the field @@ -295,7 +284,7 @@ private static Fields generateTermVectorsFromDoc(IndexShard indexShard, TermVect Collection documentFields = new HashSet<>(); for (IndexableField field : doc.getFields()) { MappedFieldType fieldType = indexShard.mapperService().fullName(field.name()); - if (isValidField(fieldType, indexShard) == false) { + if (!isValidField(fieldType)) { continue; } if (request.selectedFields() != null && !request.selectedFields().contains(field.name())) { From 70030c18f1eb51c7b31db8657e162a5523462ac8 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 24 Aug 2018 18:40:04 +0200 Subject: [PATCH 143/283] [Test] Fix sporadic failure in MembershipActionTests Rewrite test that require Version.V_5 constants. --- .../org/elasticsearch/discovery/zen/MembershipActionTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java b/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java index 3c06838593fb..a64551268119 100644 --- a/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java +++ b/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java @@ -103,7 +103,7 @@ public void testPreventJoinClusterWithUnsupportedNodeVersions() { } if (minNodeVersion.onOrAfter(Version.V_7_0_0_alpha1)) { - Version oldMajor = randomFrom(allVersions().stream().filter(v -> v.major < 6).collect(Collectors.toList())); + Version oldMajor = Version.V_6_4_0.minimumCompatibilityVersion(); expectThrows(IllegalStateException.class, () -> MembershipAction.ensureMajorVersionBarrier(oldMajor, minNodeVersion)); } From a023e64801547ce014d8ff672b24e21b1f441d49 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 24 Aug 2018 14:13:13 -0400 Subject: [PATCH 144/283] Checkstyle! Catching your unused imports since 2001. --- .../org/elasticsearch/discovery/zen/MembershipActionTests.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java b/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java index a64551268119..8ebb543da6e4 100644 --- a/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java +++ b/server/src/test/java/org/elasticsearch/discovery/zen/MembershipActionTests.java @@ -28,9 +28,6 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; -import java.util.stream.Collectors; - -import static org.elasticsearch.test.VersionUtils.allVersions; import static org.elasticsearch.test.VersionUtils.getPreviousVersion; import static org.elasticsearch.test.VersionUtils.incompatibleFutureVersion; import static org.elasticsearch.test.VersionUtils.maxCompatibleVersion; From 52cf57ee2dbdeb2d6135ac7645b0bc8e956cea19 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 24 Aug 2018 13:18:50 -0500 Subject: [PATCH 145/283] HLRC: request/response homogeneity and JavaDoc improvements (#33133) --- .../protocol/xpack/ml/CloseJobRequest.java | 34 +++++++++---------- .../protocol/xpack/ml/CloseJobResponse.java | 22 ++++++------ .../protocol/xpack/ml/DeleteJobRequest.java | 13 +++++++ .../protocol/xpack/ml/DeleteJobResponse.java | 3 ++ .../protocol/xpack/ml/GetJobRequest.java | 11 ++---- .../protocol/xpack/ml/OpenJobRequest.java | 18 ++++++++++ .../protocol/xpack/ml/OpenJobResponse.java | 24 +++++++------ .../protocol/xpack/ml/PutJobRequest.java | 8 +++++ .../protocol/xpack/ml/PutJobResponse.java | 5 ++- 9 files changed, 89 insertions(+), 49 deletions(-) diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java index 3d54bfb9488a..38f924163061 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java @@ -35,6 +35,9 @@ import java.util.List; import java.util.Objects; +/** + * Request to close Machine Learning Jobs + */ public class CloseJobRequest extends ActionRequest implements ToXContentObject { public static final ParseField JOB_ID = new ParseField("job_id"); @@ -98,49 +101,44 @@ public List getJobIds() { return jobIds; } - /** - * How long to wait for the close request to complete before timing out. - * - * Default: 30 minutes - */ public TimeValue getTimeout() { return timeout; } /** - * {@link CloseJobRequest#getTimeout()} + * How long to wait for the close request to complete before timing out. + * + * @param timeout Default value: 30 minutes */ public void setTimeout(TimeValue timeout) { this.timeout = timeout; } - /** - * Should the closing be forced. - * - * Use to close a failed job, or to forcefully close a job which has not responded to its initial close request. - */ public Boolean isForce() { return force; } /** - * {@link CloseJobRequest#isForce()} + * Should the closing be forced. + * + * Use to close a failed job, or to forcefully close a job which has not responded to its initial close request. + * + * @param force When {@code true} forcefully close the job. Defaults to {@code false} */ public void setForce(boolean force) { this.force = force; } - /** - * Whether to ignore if a wildcard expression matches no jobs. - * - * This includes `_all` string or when no jobs have been specified - */ public Boolean isAllowNoJobs() { return this.allowNoJobs; } /** - * {@link CloseJobRequest#isAllowNoJobs()} + * Whether to ignore if a wildcard expression matches no jobs. + * + * This includes `_all` string or when no jobs have been specified + * + * @param allowNoJobs When {@code true} ignore if wildcard or `_all` matches no jobs. Defaults to {@code true} */ public void setAllowNoJobs(boolean allowNoJobs) { this.allowNoJobs = allowNoJobs; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java index 9e1f38ef6bab..1b8ff3ca7d4d 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java @@ -20,7 +20,7 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -28,22 +28,22 @@ import java.io.IOException; import java.util.Objects; +/** + * Response indicating if the Job(s) closed or not + */ public class CloseJobResponse extends ActionResponse implements ToXContentObject { private static final ParseField CLOSED = new ParseField("closed"); - public static final ObjectParser PARSER = - new ObjectParser<>("close_job_response", true, CloseJobResponse::new); + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("close_job_response", true, (a) -> new CloseJobResponse((Boolean)a[0])); static { - PARSER.declareBoolean(CloseJobResponse::setClosed, CLOSED); + PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), CLOSED); } private boolean closed; - CloseJobResponse() { - } - public CloseJobResponse(boolean closed) { this.closed = closed; } @@ -52,14 +52,14 @@ public static CloseJobResponse fromXContent(XContentParser parser) throws IOExce return PARSER.parse(parser, null); } + /** + * Has the job closed or not + * @return boolean value indicating the job closed status + */ public boolean isClosed() { return closed; } - public void setClosed(boolean closed) { - this.closed = closed; - } - @Override public boolean equals(Object other) { if (this == other) { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java index 1b7450de0929..9f265fd20a8c 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java @@ -23,6 +23,9 @@ import java.util.Objects; +/** + * Request to delete a Machine Learning Job via its ID + */ public class DeleteJobRequest extends ActionRequest { private String jobId; @@ -36,6 +39,10 @@ public String getJobId() { return jobId; } + /** + * The jobId which to delete + * @param jobId unique jobId to delete, must not be null + */ public void setJobId(String jobId) { this.jobId = Objects.requireNonNull(jobId, "[job_id] must not be null"); } @@ -44,6 +51,12 @@ public boolean isForce() { return force; } + /** + * Used to forcefully delete an opened job. + * This method is quicker than closing and deleting the job. + * + * @param force When {@code true} forcefully delete an opened job. Defaults to {@code false} + */ public void setForce(boolean force) { this.force = force; } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java index 0b4faa38f545..795eb784aaff 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java @@ -24,6 +24,9 @@ import java.io.IOException; import java.util.Objects; +/** + * Response acknowledging the Machine Learning Job request + */ public class DeleteJobResponse extends AcknowledgedResponse { public DeleteJobResponse(boolean acknowledged) { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java index b0377c86fdc7..c3c14726505c 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java @@ -87,20 +87,15 @@ public List getJobIds() { return jobIds; } - /** - * See {@link GetJobRequest#isAllowNoJobs()} - * @param allowNoJobs + * Whether to ignore if a wildcard expression matches no jobs. + * + * @param allowNoJobs If this is {@code false}, then an error is returned when a wildcard (or `_all`) does not match any jobs */ public void setAllowNoJobs(boolean allowNoJobs) { this.allowNoJobs = allowNoJobs; } - /** - * Whether to ignore if a wildcard expression matches no jobs. - * - * If this is `false`, then an error is returned when a wildcard (or `_all`) does not match any jobs - */ public Boolean isAllowNoJobs() { return allowNoJobs; } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequest.java index a18a18bb55a1..658c7d38503e 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequest.java @@ -33,6 +33,9 @@ import java.io.IOException; import java.util.Objects; +/** + * Request to open a Machine Learning Job + */ public class OpenJobRequest extends ActionRequest implements ToXContentObject { public static final ParseField TIMEOUT = new ParseField("timeout"); @@ -51,6 +54,11 @@ public static OpenJobRequest fromXContent(XContentParser parser) throws IOExcept private String jobId; private TimeValue timeout; + /** + * Create a new request with the desired jobId + * + * @param jobId unique jobId, must not be null + */ public OpenJobRequest(String jobId) { this.jobId = Objects.requireNonNull(jobId, "[job_id] must not be null"); } @@ -59,6 +67,11 @@ public String getJobId() { return jobId; } + /** + * The jobId to open + * + * @param jobId unique jobId, must not be null + */ public void setJobId(String jobId) { this.jobId = Objects.requireNonNull(jobId, "[job_id] must not be null"); } @@ -67,6 +80,11 @@ public TimeValue getTimeout() { return timeout; } + /** + * How long to wait for job to open before timing out the request + * + * @param timeout default value of 30 minutes + */ public void setTimeout(TimeValue timeout) { this.timeout = timeout; } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponse.java index d8850ddbbe3a..3a1e47798043 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponse.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponse.java @@ -20,7 +20,7 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; @@ -28,22 +28,23 @@ import java.io.IOException; import java.util.Objects; +/** + * Response indicating if the Machine Learning Job is now opened or not + */ public class OpenJobResponse extends ActionResponse implements ToXContentObject { private static final ParseField OPENED = new ParseField("opened"); - public static final ObjectParser PARSER = new ObjectParser<>("open_job_response", true, OpenJobResponse::new); + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("open_job_response", true, (a) -> new OpenJobResponse((Boolean)a[0])); static { - PARSER.declareBoolean(OpenJobResponse::setOpened, OPENED); + PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), OPENED); } private boolean opened; - OpenJobResponse() { - } - - public OpenJobResponse(boolean opened) { + OpenJobResponse(boolean opened) { this.opened = opened; } @@ -51,14 +52,15 @@ public static OpenJobResponse fromXContent(XContentParser parser) throws IOExcep return PARSER.parse(parser, null); } + /** + * Has the job opened or not + * + * @return boolean value indicating the job opened status + */ public boolean isOpened() { return opened; } - public void setOpened(boolean opened) { - this.opened = opened; - } - @Override public boolean equals(Object other) { if (this == other) { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobRequest.java index 2cdf1993fccd..bc3fd778c1bd 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobRequest.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobRequest.java @@ -28,10 +28,18 @@ import java.io.IOException; import java.util.Objects; +/** + * Request to create a new Machine Learning Job given a {@link Job} configuration + */ public class PutJobRequest extends ActionRequest implements ToXContentObject { private final Job job; + /** + * Construct a new PutJobRequest + * + * @param job a {@link Job} configuration to create + */ public PutJobRequest(Job job) { this.job = job; } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java index 1bd9e87f6544..3fa1b30dd3eb 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java +++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java @@ -27,6 +27,9 @@ import java.io.IOException; import java.util.Objects; +/** + * Response containing the newly created {@link Job} + */ public class PutJobResponse implements ToXContentObject { private Job job; @@ -35,7 +38,7 @@ public static PutJobResponse fromXContent(XContentParser parser) throws IOExcept return new PutJobResponse(Job.PARSER.parse(parser, null).build()); } - public PutJobResponse(Job job) { + PutJobResponse(Job job) { this.job = job; } From ef9607ea0cbc2f928fd233e51ba67b392d7e81bd Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 24 Aug 2018 14:21:23 -0400 Subject: [PATCH 146/283] Track fetch exceptions for shard follow tasks (#33047) This commit adds tracking and reporting for fetch exceptions. We track fetch exceptions per fetch, keeping track of up to the maximum number of concurrent fetches. With each failing fetch, we associate the from sequence number with the exception that caused the fetch. We report these in the CCR stats endpoint, and add some testing for this tracking. --- .../java/org/elasticsearch/xpack/ccr/Ccr.java | 9 +- .../xpack/ccr/action/ShardChangesAction.java | 11 ++ .../xpack/ccr/action/ShardFollowNodeTask.java | 124 ++++++++++++++---- .../ShardFollowNodeTaskRandomTests.java | 6 + .../ShardFollowNodeTaskStatusTests.java | 62 ++++++++- .../ccr/action/ShardFollowNodeTaskTests.java | 105 ++++++++++++++- .../rest-api-spec/test/ccr/stats.yml | 1 + 7 files changed, 283 insertions(+), 35 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 d76af9f3c535..b00883f5c2af 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 @@ -154,7 +154,7 @@ public List getNamedWriteables() { ShardFollowTask::new), // Task statuses - new NamedWriteableRegistry.Entry(Task.Status.class, ShardFollowNodeTask.Status.NAME, + new NamedWriteableRegistry.Entry(Task.Status.class, ShardFollowNodeTask.Status.STATUS_PARSER_NAME, ShardFollowNodeTask.Status::new) ); } @@ -166,9 +166,10 @@ public List getNamedXContent() { ShardFollowTask::fromXContent), // Task statuses - new NamedXContentRegistry.Entry(ShardFollowNodeTask.Status.class, new ParseField(ShardFollowNodeTask.Status.NAME), - ShardFollowNodeTask.Status::fromXContent) - ); + new NamedXContentRegistry.Entry( + ShardFollowNodeTask.Status.class, + new ParseField(ShardFollowNodeTask.Status.STATUS_PARSER_NAME), + ShardFollowNodeTask.Status::fromXContent)); } /** 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 bc63ba5944e9..4eaf71f9c689 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 @@ -146,6 +146,17 @@ public boolean equals(final Object o) { public int hashCode() { return Objects.hash(fromSeqNo, maxOperationCount, shardId, maxOperationSizeInBytes); } + + @Override + public String toString() { + return "Request{" + + "fromSeqNo=" + fromSeqNo + + ", maxOperationCount=" + maxOperationCount + + ", shardId=" + shardId + + ", maxOperationSizeInBytes=" + maxOperationSizeInBytes + + '}'; + } + } public static final class Response extends ActionResponse { 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 cfc8e0fc4e76..f2b5b7b3772d 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 @@ -30,20 +30,25 @@ import org.elasticsearch.xpack.ccr.action.bulk.BulkShardOperationsResponse; 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; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; 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 @@ -86,6 +91,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { private long numberOfFailedBulkOperations = 0; private long numberOfOperationsIndexed = 0; private final Queue buffer = new PriorityQueue<>(Comparator.comparing(Translog.Operation::seqNo)); + private final LinkedHashMap fetchExceptions; ShardFollowNodeTask(long id, String type, String action, String description, TaskId parentTask, Map headers, ShardFollowTask params, BiConsumer scheduler, final LongSupplier relativeTimeProvider) { @@ -95,6 +101,17 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { this.relativeTimeProvider = relativeTimeProvider; this.retryTimeout = params.getRetryTimeout(); 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 + * concurrent fetches. For each failed fetch, we track the from sequence number associated with the request, and we clear the entry + * when the fetch task associated with that from sequence number succeeds. + */ + this.fetchExceptions = new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > params.getMaxConcurrentReadBatches(); + } + }; } void start( @@ -224,6 +241,7 @@ private void sendShardChangesRequest(long from, int maxOperationCount, long maxR synchronized (ShardFollowNodeTask.this) { totalFetchTimeMillis += TimeUnit.NANOSECONDS.toMillis(relativeTimeProvider.getAsLong() - startTime); numberOfSuccessfulFetches++; + fetchExceptions.remove(from); operationsReceived += response.getOperations().length; totalTransferredBytes += Arrays.stream(response.getOperations()).mapToLong(Translog.Operation::estimateSize).sum(); } @@ -233,6 +251,7 @@ private void sendShardChangesRequest(long from, int maxOperationCount, long maxR synchronized (ShardFollowNodeTask.this) { totalFetchTimeMillis += TimeUnit.NANOSECONDS.toMillis(relativeTimeProvider.getAsLong() - startTime); numberOfFailedFetches++; + fetchExceptions.put(from, new ElasticsearchException(e)); } handleFailure(e, retryCounter, () -> sendShardChangesRequest(from, maxOperationCount, maxRequiredSeqNo, retryCounter)); }); @@ -412,12 +431,13 @@ public synchronized Status getStatus() { totalIndexTimeMillis, numberOfSuccessfulBulkOperations, numberOfFailedBulkOperations, - numberOfOperationsIndexed); + numberOfOperationsIndexed, + new TreeMap<>(fetchExceptions)); } public static class Status implements Task.Status { - public static final String NAME = "shard-follow-node-task-status"; + public static final String STATUS_PARSER_NAME = "shard-follow-node-task-status"; static final ParseField SHARD_ID = new ParseField("shard_id"); static final ParseField LEADER_GLOBAL_CHECKPOINT_FIELD = new ParseField("leader_global_checkpoint"); @@ -438,8 +458,10 @@ public static class Status implements Task.Status { 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 ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, + @SuppressWarnings("unchecked") + static final ConstructingObjectParser STATUS_PARSER = new ConstructingObjectParser<>(STATUS_PARSER_NAME, args -> new Status( (int) args[0], (long) args[1], @@ -459,28 +481,51 @@ public static class Status implements Task.Status { (long) args[15], (long) args[16], (long) args[17], - (long) args[18])); + (long) args[18], + new TreeMap<>( + ((List>) args[19]) + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))))); + + 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.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(), INDEX_METADATA_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); + } + + 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 { - PARSER.declareInt(ConstructingObjectParser.constructorArg(), SHARD_ID); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_GLOBAL_CHECKPOINT_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), LEADER_MAX_SEQ_NO_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_GLOBAL_CHECKPOINT_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), FOLLOWER_MAX_SEQ_NO_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), LAST_REQUESTED_SEQ_NO_FIELD); - PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_CONCURRENT_READS_FIELD); - PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_CONCURRENT_WRITES_FIELD); - PARSER.declareInt(ConstructingObjectParser.constructorArg(), NUMBER_OF_QUEUED_WRITES_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), INDEX_METADATA_VERSION_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_FETCH_TIME_MILLIS_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_SUCCESSFUL_FETCHES_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_FETCHES_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), OPERATIONS_RECEIVED_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_TRANSFERRED_BYTES); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL_INDEX_TIME_MILLIS_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_SUCCESSFUL_BULK_OPERATIONS_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_FAILED_BULK_OPERATIONS_FIELD); - PARSER.declareLong(ConstructingObjectParser.constructorArg(), NUMBER_OF_OPERATIONS_INDEXED_FIELD); + 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 int shardId; @@ -597,6 +642,12 @@ public long numberOfOperationsIndexed() { return numberOfOperationsIndexed; } + private final NavigableMap fetchExceptions; + + public NavigableMap fetchExceptions() { + return fetchExceptions; + } + Status( final int shardId, final long leaderGlobalCheckpoint, @@ -616,7 +667,8 @@ public long numberOfOperationsIndexed() { final long totalIndexTimeMillis, final long numberOfSuccessfulBulkOperations, final long numberOfFailedBulkOperations, - final long numberOfOperationsIndexed) { + final long numberOfOperationsIndexed, + final NavigableMap fetchExceptions) { this.shardId = shardId; this.leaderGlobalCheckpoint = leaderGlobalCheckpoint; this.leaderMaxSeqNo = leaderMaxSeqNo; @@ -636,6 +688,7 @@ public long numberOfOperationsIndexed() { this.numberOfSuccessfulBulkOperations = numberOfSuccessfulBulkOperations; this.numberOfFailedBulkOperations = numberOfFailedBulkOperations; this.numberOfOperationsIndexed = numberOfOperationsIndexed; + this.fetchExceptions = fetchExceptions; } public Status(final StreamInput in) throws IOException { @@ -658,11 +711,12 @@ public Status(final StreamInput in) throws IOException { this.numberOfSuccessfulBulkOperations = in.readVLong(); this.numberOfFailedBulkOperations = in.readVLong(); this.numberOfOperationsIndexed = in.readVLong(); + this.fetchExceptions = new TreeMap<>(in.readMap(StreamInput::readVLong, StreamInput::readException)); } @Override public String getWriteableName() { - return NAME; + return STATUS_PARSER_NAME; } @Override @@ -686,6 +740,7 @@ public void writeTo(final StreamOutput out) throws IOException { out.writeVLong(numberOfSuccessfulBulkOperations); out.writeVLong(numberOfFailedBulkOperations); out.writeVLong(numberOfOperationsIndexed); + out.writeMap(fetchExceptions, StreamOutput::writeVLong, StreamOutput::writeException); } @Override @@ -720,13 +775,30 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa 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.endObject(); return builder; } public static Status fromXContent(final XContentParser parser) { - return PARSER.apply(parser, null); + return STATUS_PARSER.apply(parser, null); } @Override 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 f4cd7a680f4e..b96d5b47ec26 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 @@ -32,6 +32,7 @@ import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; public class ShardFollowNodeTaskRandomTests extends ESTestCase { @@ -54,6 +55,11 @@ private void startAndAssertAndStopTask(ShardFollowNodeTask task, TestRun testRun ShardFollowNodeTask.Status status = task.getStatus(); assertThat(status.leaderGlobalCheckpoint(), equalTo(testRun.finalExpectedGlobalCheckpoint)); assertThat(status.followerGlobalCheckpoint(), equalTo(testRun.finalExpectedGlobalCheckpoint)); + final long numberOfFailedFetches = + testRun.responses.values().stream().flatMap(List::stream).filter(f -> f.exception != null).count(); + assertThat(status.numberOfFailedFetches(), equalTo(numberOfFailedFetches)); + // the failures were able to be retried so fetch failures should have cleared + assertThat(status.fetchExceptions().entrySet(), hasSize(0)); assertThat(status.indexMetadataVersion(), equalTo(testRun.finalIndexMetaDataVerion)); }); 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 6138ba96d543..4eb428309195 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 @@ -6,11 +6,20 @@ package org.elasticsearch.xpack.ccr.action; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractSerializingTestCase; import java.io.IOException; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase { @@ -21,6 +30,7 @@ protected ShardFollowNodeTask.Status doParseInstance(XContentParser parser) thro @Override protected ShardFollowNodeTask.Status createTestInstance() { + // if you change this constructor, reflect the changes in the hand-written assertions below return new ShardFollowNodeTask.Status( randomInt(), randomNonNegativeLong(), @@ -40,7 +50,57 @@ protected ShardFollowNodeTask.Status createTestInstance() { randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong(), - randomNonNegativeLong()); + randomNonNegativeLong(), + randomReadExceptions()); + } + + @Override + protected void assertEqualInstances(final ShardFollowNodeTask.Status expectedInstance, final ShardFollowNodeTask.Status newInstance) { + assertNotSame(expectedInstance, newInstance); + assertThat(newInstance.getShardId(), equalTo(expectedInstance.getShardId())); + assertThat(newInstance.leaderGlobalCheckpoint(), equalTo(expectedInstance.leaderGlobalCheckpoint())); + assertThat(newInstance.leaderMaxSeqNo(), equalTo(expectedInstance.leaderMaxSeqNo())); + assertThat(newInstance.followerGlobalCheckpoint(), equalTo(expectedInstance.followerGlobalCheckpoint())); + assertThat(newInstance.lastRequestedSeqNo(), equalTo(expectedInstance.lastRequestedSeqNo())); + assertThat(newInstance.numberOfConcurrentReads(), equalTo(expectedInstance.numberOfConcurrentReads())); + assertThat(newInstance.numberOfConcurrentWrites(), equalTo(expectedInstance.numberOfConcurrentWrites())); + assertThat(newInstance.numberOfQueuedWrites(), equalTo(expectedInstance.numberOfQueuedWrites())); + assertThat(newInstance.indexMetadataVersion(), equalTo(expectedInstance.indexMetadataVersion())); + assertThat(newInstance.totalFetchTimeMillis(), equalTo(expectedInstance.totalFetchTimeMillis())); + assertThat(newInstance.numberOfSuccessfulFetches(), equalTo(expectedInstance.numberOfSuccessfulFetches())); + assertThat(newInstance.numberOfFailedFetches(), equalTo(expectedInstance.numberOfFailedFetches())); + assertThat(newInstance.operationsReceived(), equalTo(expectedInstance.operationsReceived())); + assertThat(newInstance.totalTransferredBytes(), equalTo(expectedInstance.totalTransferredBytes())); + assertThat(newInstance.totalIndexTimeMillis(), equalTo(expectedInstance.totalIndexTimeMillis())); + assertThat(newInstance.numberOfSuccessfulBulkOperations(), equalTo(expectedInstance.numberOfSuccessfulBulkOperations())); + assertThat(newInstance.numberOfFailedBulkOperations(), equalTo(expectedInstance.numberOfFailedBulkOperations())); + assertThat(newInstance.numberOfOperationsIndexed(), equalTo(expectedInstance.numberOfOperationsIndexed())); + assertThat(newInstance.fetchExceptions().size(), equalTo(expectedInstance.fetchExceptions().size())); + assertThat(newInstance.fetchExceptions().keySet(), equalTo(expectedInstance.fetchExceptions().keySet())); + for (final Map.Entry entry : newInstance.fetchExceptions().entrySet()) { + // x-content loses the exception + final ElasticsearchException expected = expectedInstance.fetchExceptions().get(entry.getKey()); + assertThat(entry.getValue().getMessage(), containsString(expected.getMessage())); + assertNotNull(entry.getValue().getCause()); + assertThat( + entry.getValue().getCause(), + anyOf(instanceOf(ElasticsearchException.class), instanceOf(IllegalStateException.class))); + assertThat(entry.getValue().getCause().getMessage(), containsString(expected.getCause().getMessage())); + } + } + + @Override + protected boolean assertToXContentEquivalence() { + return false; + } + + private NavigableMap randomReadExceptions() { + final int count = randomIntBetween(0, 16); + final NavigableMap readExceptions = new TreeMap<>(); + for (int i = 0; i < count; i++) { + readExceptions.put(randomNonNegativeLong(), new ElasticsearchException(new IllegalStateException("index [" + i + "]"))); + } + return readExceptions; } @Override 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 9eda637dc9df..54aef6bd3d11 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 @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ccr.action; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.shard.ShardId; @@ -20,8 +21,10 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.LongConsumer; @@ -29,6 +32,8 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; @@ -39,11 +44,17 @@ public class ShardFollowNodeTaskTests extends ESTestCase { private List> bulkShardOperationRequests; private BiConsumer scheduler = (delay, task) -> task.run(); + private Consumer beforeSendShardChangesRequest = status -> {}; + + private AtomicBoolean simulateResponse = new AtomicBoolean(); + private Queue readFailures; private Queue writeFailures; private Queue mappingUpdateFailures; private Queue imdVersions; + private Queue leaderGlobalCheckpoints; private Queue followerGlobalCheckpoints; + private Queue maxSeqNos; public void testCoordinateReads() { ShardFollowNodeTask task = createShardFollowTask(8, between(8, 20), between(1, 20), Integer.MAX_VALUE, Long.MAX_VALUE); @@ -169,6 +180,27 @@ public void testReceiveRetryableError() { for (int i = 0; i < max; i++) { readFailures.add(new ShardNotFoundException(new ShardId("leader_index", "", 0))); } + imdVersions.add(1L); + leaderGlobalCheckpoints.add(63L); + maxSeqNos.add(63L); + simulateResponse.set(true); + final AtomicLong retryCounter = new AtomicLong(); + // before each retry, we assert the fetch failures; after the last retry, the fetch failure should clear + beforeSendShardChangesRequest = status -> { + 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(); // NUmber of requests is equal to initial request + retried attempts @@ -178,10 +210,14 @@ public void testReceiveRetryableError() { assertThat(shardChangesRequest[1], equalTo(64L)); } - assertThat(task.isStopped(), equalTo(false)); + assertFalse("task is not stopped", task.isStopped()); ShardFollowNodeTask.Status status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); + assertThat(status.numberOfFailedFetches(), equalTo((long)max)); + assertThat(status.numberOfSuccessfulFetches(), equalTo(1L)); + // the fetch failure has cleared + assertThat(status.fetchExceptions().entrySet(), hasSize(0)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); assertThat(status.leaderGlobalCheckpoint(), equalTo(63L)); } @@ -194,6 +230,23 @@ public void testReceiveRetryableErrorRetriedTooManyTimes() { for (int i = 0; i < max; i++) { readFailures.add(new ShardNotFoundException(new ShardId("leader_index", "", 0))); } + final AtomicLong retryCounter = new AtomicLong(); + // before each retry, we assert the fetch failures; after the last retry, the fetch failure should persist + beforeSendShardChangesRequest = status -> { + 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)); @@ -202,12 +255,22 @@ public void testReceiveRetryableErrorRetriedTooManyTimes() { assertThat(shardChangesRequest[1], equalTo(64L)); } - assertThat(task.isStopped(), equalTo(true)); + assertTrue("task is stopped", task.isStopped()); assertThat(fatalError, notNullValue()); assertThat(fatalError.getMessage(), containsString("retrying failed [")); ShardFollowNodeTask.Status 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)); } @@ -216,19 +279,38 @@ public void testReceiveNonRetryableError() { ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); startTask(task, 63, -1); - Exception failure = new RuntimeException(); + Exception failure = new RuntimeException("replication failed"); readFailures.add(failure); + final AtomicBoolean invoked = new AtomicBoolean(); + // since there will be only one failure, this should only be invoked once and there should not be a fetch failure + beforeSendShardChangesRequest = status -> { + if (invoked.compareAndSet(false, true)) { + assertThat(status.numberOfFailedFetches(), equalTo(0L)); + assertThat(status.fetchExceptions().entrySet(), hasSize(0)); + } else { + fail("invoked twice"); + } + }; task.coordinateReads(); assertThat(shardChangesRequests.size(), equalTo(1)); assertThat(shardChangesRequests.get(0)[0], equalTo(0L)); assertThat(shardChangesRequests.get(0)[1], equalTo(64L)); - assertThat(task.isStopped(), equalTo(true)); + assertTrue("task is stopped", task.isStopped()); assertThat(fatalError, sameInstance(failure)); ShardFollowNodeTask.Status status = task.getStatus(); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); + assertThat(status.numberOfFailedFetches(), equalTo(1L)); + 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(RuntimeException.class)); + final RuntimeException cause = (RuntimeException) entry.getValue().getCause(); + assertThat(cause.getMessage(), equalTo("replication failed")); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); assertThat(status.leaderGlobalCheckpoint(), equalTo(63L)); } @@ -642,7 +724,9 @@ ShardFollowNodeTask createShardFollowTask(int maxBatchOperationCount, int maxCon writeFailures = new LinkedList<>(); mappingUpdateFailures = new LinkedList<>(); imdVersions = new LinkedList<>(); + leaderGlobalCheckpoints = new LinkedList<>(); followerGlobalCheckpoints = new LinkedList<>(); + maxSeqNos = new LinkedList<>(); return new ShardFollowNodeTask( 1L, "type", ShardFollowTask.NAME, "description", null, Collections.emptyMap(), params, scheduler, System::nanoTime) { @@ -683,10 +767,23 @@ protected void innerSendBulkShardOperationsRequest( @Override protected void innerSendShardChangesRequest(long from, int requestBatchSize, Consumer handler, Consumer errorHandler) { + beforeSendShardChangesRequest.accept(getStatus()); shardChangesRequests.add(new long[]{from, requestBatchSize}); Exception readFailure = ShardFollowNodeTaskTests.this.readFailures.poll(); if (readFailure != null) { errorHandler.accept(readFailure); + } else if (simulateResponse.get()) { + final Translog.Operation[] operations = new Translog.Operation[requestBatchSize]; + for (int i = 0; i < requestBatchSize; i++) { + operations[i] = new Translog.NoOp(from + i, 0, "test"); + } + final ShardChangesAction.Response response = + new ShardChangesAction.Response( + imdVersions.poll(), + leaderGlobalCheckpoints.poll(), + maxSeqNos.poll(), + operations); + handler.accept(response); } } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml index 94af4c345fda..a38698a45be4 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml @@ -43,6 +43,7 @@ - match: { bar.0.number_of_successful_bulk_operations: 0 } - match: { bar.0.number_of_failed_bulk_operations: 0 } - match: { bar.0.number_of_operations_indexed: 0 } + - length: { bar.0.fetch_exceptions: 0 } - do: ccr.unfollow_index: From b86dad22ce2701a403d0a6073889141ab4578552 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Fri, 24 Aug 2018 12:51:22 -0600 Subject: [PATCH 147/283] Security index expands to a single replica (#33131) This change removes the use of 0-all for auto expand replicas for the security index. The use of 0-all causes some unexpected behavior with certain allocation settings. This change allows us to avoid these with a default install. If necessary, the number of replicas can be tuned by the user. Closes #29933 Closes #29712 --- .../plugin/core/src/main/resources/security-index-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/resources/security-index-template.json b/x-pack/plugin/core/src/main/resources/security-index-template.json index dd17baf04740..bac5930c0d5c 100644 --- a/x-pack/plugin/core/src/main/resources/security-index-template.json +++ b/x-pack/plugin/core/src/main/resources/security-index-template.json @@ -4,7 +4,7 @@ "settings" : { "number_of_shards" : 1, "number_of_replicas" : 0, - "auto_expand_replicas" : "0-all", + "auto_expand_replicas" : "0-1", "index.priority": 1000, "index.format": 6, "analysis" : { From a9a66a09dc3d3ac373cf1dbd11f6befe6bdb42a8 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 24 Aug 2018 16:11:07 -0400 Subject: [PATCH 148/283] Build: Line up IDE detection logic The IDE detection logic in build.gradle and settings.gradle has to match or else Eclipse users will have a bad time. But in this case I think it mostly just hits folks who use the Eclipse compiler server like me. --- settings.gradle | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index bdae1c396fda..dedf3520bbbc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -78,7 +78,12 @@ addSubProjects('', new File(rootProject.projectDir, 'plugins')) addSubProjects('', new File(rootProject.projectDir, 'qa')) addSubProjects('', new File(rootProject.projectDir, 'x-pack')) -boolean isEclipse = System.getProperty("eclipse.launcher") != null || gradle.startParameter.taskNames.contains('eclipse') || gradle.startParameter.taskNames.contains('cleanEclipse') +List startTasks = gradle.startParameter.taskNames +boolean isEclipse = + System.getProperty("eclipse.launcher") != null || // Detects gradle launched from the Eclipse IDE + System.getProperty("eclipse.application") != null || // Detects gradle launched from the Eclipse compiler server + startTasks.contains("eclipse") || // Detects gradle launched from the command line to do Eclipse stuff + startTasks.contains("cleanEclipse"); if (isEclipse) { // eclipse cannot handle an intermediate dependency between main and test, so we must create separate projects // for server-src and server-tests From 8bee6b3a922f5f8b86bdae125aab9171dd88a242 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 24 Aug 2018 16:36:40 -0400 Subject: [PATCH 149/283] Switch remaining ml tests to new style Requests (#33107) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. This changes all calls in the `x-pack/plugin/ml/qa/native-multi-node-tests`, `x-pack/plugin/ml/qa/single-node-tests` projects to use the new versions. --- .../ml/integration/DatafeedJobsRestIT.java | 601 ++++++++++-------- .../xpack/ml/integration/MlJobIT.java | 470 ++++++-------- .../ml/transforms/PainlessDomainSplitIT.java | 105 ++- 3 files changed, 585 insertions(+), 591 deletions(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java index 54d8090a7a42..7a93ecdd9e1c 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java @@ -5,9 +5,9 @@ */ package org.elasticsearch.xpack.ml.integration; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.message.BasicHeader; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; import org.elasticsearch.client.RestClient; @@ -22,10 +22,7 @@ import org.junit.After; import org.junit.Before; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -36,6 +33,7 @@ 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.not; public class DatafeedJobsRestIT extends ESRestTestCase { @@ -57,26 +55,24 @@ protected boolean preserveTemplatesUponCompletion() { } private void setupDataAccessRole(String index) throws IOException { - String json = "{" + Request request = new Request("PUT", "/_xpack/security/role/test_data_access"); + request.setJsonEntity("{" + " \"indices\" : [" + " { \"names\": [\"" + index + "\"], \"privileges\": [\"read\"] }" + " ]" - + "}"; - - client().performRequest("put", "_xpack/security/role/test_data_access", Collections.emptyMap(), - new StringEntity(json, ContentType.APPLICATION_JSON)); + + "}"); + client().performRequest(request); } private void setupUser(String user, List roles) throws IOException { String password = new String(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING.getChars()); - String json = "{" + Request request = new Request("PUT", "/_xpack/security/user/" + user); + request.setJsonEntity("{" + " \"password\" : \"" + password + "\"," + " \"roles\" : [ " + roles.stream().map(unquoted -> "\"" + unquoted + "\"").collect(Collectors.joining(", ")) + " ]" - + "}"; - - client().performRequest("put", "_xpack/security/user/" + user, Collections.emptyMap(), - new StringEntity(json, ContentType.APPLICATION_JSON)); + + "}"); + client().performRequest(request); } @Before @@ -92,7 +88,10 @@ public void setUpData() throws Exception { } private void addAirlineData() throws IOException { - String mappings = "{" + StringBuilder bulk = new StringBuilder(); + + Request createEmptyAirlineDataRequest = new Request("PUT", "/airline-data-empty"); + createEmptyAirlineDataRequest.setJsonEntity("{" + " \"mappings\": {" + " \"response\": {" + " \"properties\": {" @@ -102,12 +101,12 @@ private void addAirlineData() throws IOException { + " }" + " }" + " }" - + "}"; - client().performRequest("put", "airline-data-empty", Collections.emptyMap(), - new StringEntity(mappings, ContentType.APPLICATION_JSON)); + + "}"); + client().performRequest(createEmptyAirlineDataRequest); // Create index with source = enabled, doc_values = enabled, stored = false + multi-field - mappings = "{" + Request createAirlineDataRequest = new Request("PUT", "/airline-data"); + createAirlineDataRequest.setJsonEntity("{" + " \"mappings\": {" + " \"response\": {" + " \"properties\": {" @@ -123,18 +122,17 @@ private void addAirlineData() throws IOException { + " }" + " }" + " }" - + "}"; - client().performRequest("put", "airline-data", Collections.emptyMap(), new StringEntity(mappings, ContentType.APPLICATION_JSON)); + + "}"); + client().performRequest(createAirlineDataRequest); - client().performRequest("put", "airline-data/response/1", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data/response/2", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}", - ContentType.APPLICATION_JSON)); + bulk.append("{\"index\": {\"_index\": \"airline-data\", \"_type\": \"response\", \"_id\": 1}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data\", \"_type\": \"response\", \"_id\": 2}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}\n"); // Create index with source = enabled, doc_values = disabled (except time), stored = false - mappings = "{" + Request createAirlineDataDisabledDocValues = new Request("PUT", "/airline-data-disabled-doc-values"); + createAirlineDataDisabledDocValues.setJsonEntity("{" + " \"mappings\": {" + " \"response\": {" + " \"properties\": {" @@ -144,19 +142,17 @@ private void addAirlineData() throws IOException { + " }" + " }" + " }" - + "}"; - client().performRequest("put", "airline-data-disabled-doc-values", Collections.emptyMap(), - new StringEntity(mappings, ContentType.APPLICATION_JSON)); + + "}"); + client().performRequest(createAirlineDataDisabledDocValues); - client().performRequest("put", "airline-data-disabled-doc-values/response/1", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-disabled-doc-values/response/2", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}", - ContentType.APPLICATION_JSON)); + bulk.append("{\"index\": {\"_index\": \"airline-data-disabled-doc-values\", \"_type\": \"response\", \"_id\": 1}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-disabled-doc-values\", \"_type\": \"response\", \"_id\": 2}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}\n"); // Create index with source = disabled, doc_values = enabled (except time), stored = true - mappings = "{" + Request createAirlineDataDisabledSource = new Request("PUT", "/airline-data-disabled-source"); + createAirlineDataDisabledSource.setJsonEntity("{" + " \"mappings\": {" + " \"response\": {" + " \"_source\":{\"enabled\":false}," @@ -167,19 +163,16 @@ private void addAirlineData() throws IOException { + " }" + " }" + " }" - + "}"; - client().performRequest("put", "airline-data-disabled-source", Collections.emptyMap(), - new StringEntity(mappings, ContentType.APPLICATION_JSON)); + + "}"); - client().performRequest("put", "airline-data-disabled-source/response/1", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-disabled-source/response/2", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}", - ContentType.APPLICATION_JSON)); + bulk.append("{\"index\": {\"_index\": \"airline-data-disabled-source\", \"_type\": \"response\", \"_id\": 1}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":135.22}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-disabled-source\", \"_type\": \"response\", \"_id\": 2}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T01:59:00Z\",\"airline\":\"AAA\",\"responsetime\":541.76}\n"); // Create index with nested documents - mappings = "{" + Request createAirlineDataNested = new Request("PUT", "/nested-data"); + createAirlineDataNested.setJsonEntity("{" + " \"mappings\": {" + " \"response\": {" + " \"properties\": {" @@ -187,18 +180,17 @@ private void addAirlineData() throws IOException { + " }" + " }" + " }" - + "}"; - client().performRequest("put", "nested-data", Collections.emptyMap(), new StringEntity(mappings, ContentType.APPLICATION_JSON)); + + "}"); + client().performRequest(createAirlineDataNested); - client().performRequest("put", "nested-data/response/1", Collections.emptyMap(), - new StringEntity("{\"time\":\"2016-06-01T00:00:00Z\", \"responsetime\":{\"millis\":135.22}}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "nested-data/response/2", Collections.emptyMap(), - new StringEntity("{\"time\":\"2016-06-01T01:59:00Z\",\"responsetime\":{\"millis\":222.0}}", - ContentType.APPLICATION_JSON)); + bulk.append("{\"index\": {\"_index\": \"nested-data\", \"_type\": \"response\", \"_id\": 1}}\n"); + bulk.append("{\"time\":\"2016-06-01T00:00:00Z\", \"responsetime\":{\"millis\":135.22}}\n"); + bulk.append("{\"index\": {\"_index\": \"nested-data\", \"_type\": \"response\", \"_id\": 2}}\n"); + bulk.append("{\"time\":\"2016-06-01T01:59:00Z\",\"responsetime\":{\"millis\":222.0}}\n"); // Create index with multiple docs per time interval for aggregation testing - mappings = "{" + Request createAirlineDataAggs = new Request("PUT", "/airline-data-aggs"); + createAirlineDataAggs.setJsonEntity("{" + " \"mappings\": {" + " \"response\": {" + " \"properties\": {" @@ -208,43 +200,33 @@ private void addAirlineData() throws IOException { + " }" + " }" + " }" - + "}"; - client().performRequest("put", "airline-data-aggs", Collections.emptyMap(), - new StringEntity(mappings, ContentType.APPLICATION_JSON)); - - client().performRequest("put", "airline-data-aggs/response/1", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":100.0}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-aggs/response/2", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T00:01:00Z\",\"airline\":\"AAA\",\"responsetime\":200.0}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-aggs/response/3", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"BBB\",\"responsetime\":1000.0}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-aggs/response/4", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T00:01:00Z\",\"airline\":\"BBB\",\"responsetime\":2000.0}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-aggs/response/5", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T01:00:00Z\",\"airline\":\"AAA\",\"responsetime\":300.0}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-aggs/response/6", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T01:01:00Z\",\"airline\":\"AAA\",\"responsetime\":400.0}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-aggs/response/7", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T01:00:00Z\",\"airline\":\"BBB\",\"responsetime\":3000.0}", - ContentType.APPLICATION_JSON)); - client().performRequest("put", "airline-data-aggs/response/8", Collections.emptyMap(), - new StringEntity("{\"time stamp\":\"2016-06-01T01:01:00Z\",\"airline\":\"BBB\",\"responsetime\":4000.0}", - ContentType.APPLICATION_JSON)); - - // Ensure all data is searchable - client().performRequest("post", "_refresh"); + + "}"); + client().performRequest(createAirlineDataAggs); + + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 1}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"AAA\",\"responsetime\":100.0}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 2}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T00:01:00Z\",\"airline\":\"AAA\",\"responsetime\":200.0}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 3}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T00:00:00Z\",\"airline\":\"BBB\",\"responsetime\":1000.0}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 4}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T00:01:00Z\",\"airline\":\"BBB\",\"responsetime\":2000.0}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 5}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T01:00:00Z\",\"airline\":\"AAA\",\"responsetime\":300.0}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 6}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T01:01:00Z\",\"airline\":\"AAA\",\"responsetime\":400.0}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 7}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T01:00:00Z\",\"airline\":\"BBB\",\"responsetime\":3000.0}\n"); + bulk.append("{\"index\": {\"_index\": \"airline-data-aggs\", \"_type\": \"response\", \"_id\": 8}}\n"); + bulk.append("{\"time stamp\":\"2016-06-01T01:01:00Z\",\"airline\":\"BBB\",\"responsetime\":4000.0}\n"); + + bulkIndex(bulk.toString()); } private void addNetworkData(String index) throws IOException { - // Create index with source = enabled, doc_values = enabled, stored = false + multi-field - String mappings = "{" + Request createIndexRequest = new Request("PUT", index); + createIndexRequest.setJsonEntity("{" + " \"mappings\": {" + " \"doc\": {" + " \"properties\": {" @@ -260,27 +242,25 @@ private void addNetworkData(String index) throws IOException { + " }" + " }" + " }" - + "}"; - client().performRequest("put", index, Collections.emptyMap(), new StringEntity(mappings, ContentType.APPLICATION_JSON)); + + "}");; + client().performRequest(createIndexRequest); + StringBuilder bulk = new StringBuilder(); String docTemplate = "{\"timestamp\":%d,\"host\":\"%s\",\"network_bytes_out\":%d}"; Date date = new Date(1464739200735L); - for (int i=0; i<120; i++) { + for (int i = 0; i < 120; i++) { long byteCount = randomNonNegativeLong(); - String jsonDoc = String.format(Locale.ROOT, docTemplate, date.getTime(), "hostA", byteCount); - client().performRequest("post", index + "/doc", Collections.emptyMap(), - new StringEntity(jsonDoc, ContentType.APPLICATION_JSON)); + bulk.append("{\"index\": {\"_index\": \"").append(index).append("\", \"_type\": \"doc\"}}\n"); + bulk.append(String.format(Locale.ROOT, docTemplate, date.getTime(), "hostA", byteCount)).append('\n'); byteCount = randomNonNegativeLong(); - jsonDoc = String.format(Locale.ROOT, docTemplate, date.getTime(), "hostB", byteCount); - client().performRequest("post", index + "/doc", Collections.emptyMap(), - new StringEntity(jsonDoc, ContentType.APPLICATION_JSON)); + bulk.append("{\"index\": {\"_index\": \"").append(index).append("\", \"_type\": \"doc\"}}\n"); + bulk.append(String.format(Locale.ROOT, docTemplate, date.getTime(), "hostB", byteCount)).append('\n'); date = new Date(date.getTime() + 10_000); } - // Ensure all data is searchable - client().performRequest("post", "_refresh"); + bulkIndex(bulk.toString()); } public void testLookbackOnlyWithMixedTypes() throws Exception { @@ -314,11 +294,21 @@ public void testLookbackOnlyWithScriptFields() throws Exception { public void testLookbackOnlyWithNestedFields() throws Exception { String jobId = "test-lookback-only-with-nested-fields"; - String job = "{\"description\":\"Nested job\", \"analysis_config\" : {\"bucket_span\":\"1h\",\"detectors\" :" - + "[{\"function\":\"mean\",\"field_name\":\"responsetime.millis\"}]}, \"data_description\" : {\"time_field\":\"time\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"description\": \"Nested job\",\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"1h\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"responsetime.millis\"\n" + + " }\n" + + " ]\n" + + " }," + + " \"data_description\": {\"time_field\": \"time\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = jobId + "-datafeed"; new DatafeedBuilder(datafeedId, jobId, "nested-data", "response").build(); @@ -326,8 +316,9 @@ public void testLookbackOnlyWithNestedFields() throws Exception { startDatafeedAndWaitUntilStopped(datafeedId); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest( + new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":2")); assertThat(jobStatsResponseAsString, containsString("\"processed_record_count\":2")); assertThat(jobStatsResponseAsString, containsString("\"missing_field_count\":0")); @@ -340,14 +331,23 @@ public void testLookbackOnlyGivenEmptyIndex() throws Exception { public void testInsufficientSearchPrivilegesOnPut() throws Exception { String jobId = "privs-put-job"; - String job = "{\"description\":\"Aggs job\",\"analysis_config\" :{\"bucket_span\":\"1h\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\"," - + "\"field_name\":\"responsetime\",\"by_field_name\":\"airline\"}]}," - + "\"data_description\" : {\"time_field\":\"time stamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, - Collections.emptyMap(), new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"description\": \"Aggs job\",\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"1h\",\n " + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"responsetime\",\n" + + " \"by_field_name\":\"airline\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\" : {\"time_field\": \"time stamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; // This should be disallowed, because even though the ml_admin user has permission to @@ -365,14 +365,23 @@ public void testInsufficientSearchPrivilegesOnPut() throws Exception { public void testInsufficientSearchPrivilegesOnPreview() throws Exception { String jobId = "privs-preview-job"; - String job = "{\"description\":\"Aggs job\",\"analysis_config\" :{\"bucket_span\":\"1h\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\"," - + "\"field_name\":\"responsetime\",\"by_field_name\":\"airline\"}]}," - + "\"data_description\" : {\"time_field\":\"time stamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, - Collections.emptyMap(), new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"description\": \"Aggs job\",\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"1h\",\n" + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"responsetime\",\n" + + " \"by_field_name\": \"airline\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\" : {\"time_field\": \"time stamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; new DatafeedBuilder(datafeedId, jobId, "airline-data-aggs", "response").build(); @@ -380,10 +389,11 @@ public void testInsufficientSearchPrivilegesOnPreview() throws Exception { // This should be disallowed, because ml_admin is trying to preview a datafeed created by // by another user (x_pack_rest_user in this case) that will reveal the content of an index they // don't have permission to search directly - ResponseException e = expectThrows(ResponseException.class, () -> - client().performRequest("get", - MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_preview", - new BasicHeader("Authorization", BASIC_AUTH_VALUE_ML_ADMIN))); + Request getFeed = new Request("GET", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_preview"); + RequestOptions.Builder options = getFeed.getOptions().toBuilder(); + options.addHeader("Authorization", BASIC_AUTH_VALUE_ML_ADMIN); + getFeed.setOptions(options); + ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(getFeed)); assertThat(e.getMessage(), containsString("[indices:data/read/field_caps] is unauthorized for user [ml_admin]")); @@ -391,13 +401,23 @@ public void testInsufficientSearchPrivilegesOnPreview() throws Exception { public void testLookbackOnlyGivenAggregationsWithHistogram() throws Exception { String jobId = "aggs-histogram-job"; - String job = "{\"description\":\"Aggs job\",\"analysis_config\" :{\"bucket_span\":\"1h\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\",\"field_name\":\"responsetime\",\"by_field_name\":\"airline\"}]}," - + "\"data_description\" : {\"time_field\":\"time stamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"description\": \"Aggs job\",\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"1h\",\n" + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"responsetime\",\n" + + " \"by_field_name\": \"airline\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\": {\"time_field\": \"time stamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; String aggregations = "{\"buckets\":{\"histogram\":{\"field\":\"time stamp\",\"interval\":3600000}," @@ -410,8 +430,9 @@ public void testLookbackOnlyGivenAggregationsWithHistogram() throws Exception { startDatafeedAndWaitUntilStopped(datafeedId); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":4")); assertThat(jobStatsResponseAsString, containsString("\"processed_record_count\":4")); assertThat(jobStatsResponseAsString, containsString("\"missing_field_count\":0")); @@ -419,13 +440,23 @@ public void testLookbackOnlyGivenAggregationsWithHistogram() throws Exception { public void testLookbackOnlyGivenAggregationsWithDateHistogram() throws Exception { String jobId = "aggs-date-histogram-job"; - String job = "{\"description\":\"Aggs job\",\"analysis_config\" :{\"bucket_span\":\"3600s\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\",\"field_name\":\"responsetime\",\"by_field_name\":\"airline\"}]}," - + "\"data_description\" : {\"time_field\":\"time stamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"description\": \"Aggs job\",\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"3600s\",\n" + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"responsetime\",\n" + + " \"by_field_name\": \"airline\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\": {\"time_field\": \"time stamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; String aggregations = "{\"time stamp\":{\"date_histogram\":{\"field\":\"time stamp\",\"interval\":\"1h\"}," @@ -438,8 +469,9 @@ public void testLookbackOnlyGivenAggregationsWithDateHistogram() throws Exceptio startDatafeedAndWaitUntilStopped(datafeedId); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":4")); assertThat(jobStatsResponseAsString, containsString("\"processed_record_count\":4")); assertThat(jobStatsResponseAsString, containsString("\"missing_field_count\":0")); @@ -447,13 +479,22 @@ public void testLookbackOnlyGivenAggregationsWithDateHistogram() throws Exceptio public void testLookbackUsingDerivativeAggWithLargerHistogramBucketThanDataRate() throws Exception { String jobId = "derivative-agg-network-job"; - String job = "{\"analysis_config\" :{\"bucket_span\":\"300s\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\",\"field_name\":\"bytes-delta\",\"by_field_name\":\"hostname\"}]}," - + "\"data_description\" : {\"time_field\":\"timestamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"300s\",\n" + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"bytes-delta\",\n" + + " \"by_field_name\": \"hostname\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\": {\"time_field\": \"timestamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; String aggregations = @@ -471,8 +512,9 @@ public void testLookbackUsingDerivativeAggWithLargerHistogramBucketThanDataRate( startDatafeedAndWaitUntilStopped(datafeedId); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":40")); assertThat(jobStatsResponseAsString, containsString("\"processed_record_count\":40")); assertThat(jobStatsResponseAsString, containsString("\"out_of_order_timestamp_count\":0")); @@ -483,13 +525,22 @@ public void testLookbackUsingDerivativeAggWithLargerHistogramBucketThanDataRate( public void testLookbackUsingDerivativeAggWithSmallerHistogramBucketThanDataRate() throws Exception { String jobId = "derivative-agg-network-job"; - String job = "{\"analysis_config\" :{\"bucket_span\":\"300s\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\",\"field_name\":\"bytes-delta\",\"by_field_name\":\"hostname\"}]}," - + "\"data_description\" : {\"time_field\":\"timestamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"300s\",\n" + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"bytes-delta\",\n" + + " \"by_field_name\": \"hostname\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\": {\"time_field\": \"timestamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; String aggregations = @@ -507,21 +558,31 @@ public void testLookbackUsingDerivativeAggWithSmallerHistogramBucketThanDataRate startDatafeedAndWaitUntilStopped(datafeedId); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":240")); assertThat(jobStatsResponseAsString, containsString("\"processed_record_count\":240")); } public void testLookbackWithoutPermissions() throws Exception { String jobId = "permission-test-network-job"; - String job = "{\"analysis_config\" :{\"bucket_span\":\"300s\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\",\"field_name\":\"bytes-delta\",\"by_field_name\":\"hostname\"}]}," - + "\"data_description\" : {\"time_field\":\"timestamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"300s\",\n" + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"bytes-delta\",\n" + + " \"by_field_name\": \"hostname\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\": {\"time_field\": \"timestamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; String aggregations = @@ -545,29 +606,39 @@ public void testLookbackWithoutPermissions() throws Exception { startDatafeedAndWaitUntilStopped(datafeedId, BASIC_AUTH_VALUE_ML_ADMIN_WITH_SOME_DATA_ACCESS); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); // We expect that no data made it through to the job assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":0")); assertThat(jobStatsResponseAsString, containsString("\"processed_record_count\":0")); // There should be a notification saying that there was a problem extracting data - client().performRequest("post", "_refresh"); - Response notificationsResponse = client().performRequest("get", AuditorField.NOTIFICATIONS_INDEX + "/_search?q=job_id:" + jobId); - String notificationsResponseAsString = responseEntityToString(notificationsResponse); + client().performRequest(new Request("POST", "/_refresh")); + Response notificationsResponse = client().performRequest( + new Request("GET", AuditorField.NOTIFICATIONS_INDEX + "/_search?q=job_id:" + jobId)); + String notificationsResponseAsString = EntityUtils.toString(notificationsResponse.getEntity()); assertThat(notificationsResponseAsString, containsString("\"message\":\"Datafeed is encountering errors extracting data: " + "action [indices:data/read/search] is unauthorized for user [ml_admin_plus_data]\"")); } public void testLookbackWithPipelineBucketAgg() throws Exception { String jobId = "pipeline-bucket-agg-job"; - String job = "{\"analysis_config\" :{\"bucket_span\":\"1h\"," - + "\"summary_count_field_name\":\"doc_count\"," - + "\"detectors\":[{\"function\":\"mean\",\"field_name\":\"percentile95_airlines_count\"}]}," - + "\"data_description\" : {\"time_field\":\"time stamp\"}" - + "}"; - client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJobRequest.setJsonEntity("{\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"1h\",\n" + + " \"summary_count_field_name\": \"doc_count\",\n" + + " \"detectors\": [\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"percentile95_airlines_count\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\": {\"time_field\": \"time stamp\"}\n" + + "}"); + client().performRequest(createJobRequest); String datafeedId = "datafeed-" + jobId; String aggregations = "{\"buckets\":{\"date_histogram\":{\"field\":\"time stamp\",\"interval\":\"15m\"}," @@ -582,8 +653,9 @@ public void testLookbackWithPipelineBucketAgg() throws Exception { startDatafeedAndWaitUntilStopped(datafeedId); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":2")); assertThat(jobStatsResponseAsString, containsString("\"input_field_count\":4")); assertThat(jobStatsResponseAsString, containsString("\"processed_record_count\":2")); @@ -599,15 +671,15 @@ public void testRealtime() throws Exception { new DatafeedBuilder(datafeedId, jobId, "airline-data", "response").build(); openJob(client(), jobId); - Response response = client().performRequest("post", - MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start?start=2016-06-01T00:00:00Z"); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - assertThat(responseEntityToString(response), equalTo("{\"started\":true}")); + Request startRequest = new Request("POST", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start"); + startRequest.addParameter("start", "2016-06-01T00:00:00Z"); + Response response = client().performRequest(startRequest); + assertThat(EntityUtils.toString(response.getEntity()), equalTo("{\"started\":true}")); assertBusy(() -> { try { - Response getJobResponse = client().performRequest("get", - MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String responseAsString = responseEntityToString(getJobResponse); + Response getJobResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String responseAsString = EntityUtils.toString(getJobResponse.getEntity()); assertThat(responseAsString, containsString("\"processed_record_count\":2")); assertThat(responseAsString, containsString("\"state\":\"opened\"")); } catch (Exception e1) { @@ -619,9 +691,9 @@ public void testRealtime() throws Exception { // test a model snapshot is present assertBusy(() -> { try { - Response getJobResponse = client().performRequest("get", - MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/model_snapshots"); - String responseAsString = responseEntityToString(getJobResponse); + Response getJobResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/model_snapshots")); + String responseAsString = EntityUtils.toString(getJobResponse.getEntity()); assertThat(responseAsString, containsString("\"count\":1")); } catch (Exception e1) { throw new RuntimeException(e1); @@ -629,25 +701,25 @@ public void testRealtime() throws Exception { }); ResponseException e = expectThrows(ResponseException.class, - () -> client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); + () -> client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId))); response = e.getResponse(); assertThat(response.getStatusLine().getStatusCode(), equalTo(409)); - assertThat(responseEntityToString(response), containsString("Cannot delete job [" + jobId + "] because datafeed [" + datafeedId - + "] refers to it")); + assertThat(EntityUtils.toString(response.getEntity()), + containsString("Cannot delete job [" + jobId + "] because datafeed [" + datafeedId + "] refers to it")); - response = client().performRequest("post", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_stop"); + response = client().performRequest(new Request("POST", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_stop")); assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - assertThat(responseEntityToString(response), equalTo("{\"stopped\":true}")); + assertThat(EntityUtils.toString(response.getEntity()), equalTo("{\"stopped\":true}")); - client().performRequest("POST", "/_xpack/ml/anomaly_detectors/" + jobId + "/_close"); + client().performRequest(new Request("POST", "/_xpack/ml/anomaly_detectors/" + jobId + "/_close")); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId); + response = client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId)); assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - assertThat(responseEntityToString(response), equalTo("{\"acknowledged\":true}")); + assertThat(EntityUtils.toString(response.getEntity()), equalTo("{\"acknowledged\":true}")); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + response = client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - assertThat(responseEntityToString(response), equalTo("{\"acknowledged\":true}")); + assertThat(EntityUtils.toString(response.getEntity()), equalTo("{\"acknowledged\":true}")); } public void testForceDeleteWhileDatafeedIsRunning() throws Exception { @@ -657,25 +729,26 @@ public void testForceDeleteWhileDatafeedIsRunning() throws Exception { new DatafeedBuilder(datafeedId, jobId, "airline-data", "response").build(); openJob(client(), jobId); - Response response = client().performRequest("post", - MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start?start=2016-06-01T00:00:00Z"); + Request startRequest = new Request("POST", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start"); + startRequest.addParameter("start", "2016-06-01T00:00:00Z"); + Response response = client().performRequest(startRequest); assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - assertThat(responseEntityToString(response), equalTo("{\"started\":true}")); + assertThat(EntityUtils.toString(response.getEntity()), equalTo("{\"started\":true}")); ResponseException e = expectThrows(ResponseException.class, - () -> client().performRequest("delete", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId)); + () -> client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId))); response = e.getResponse(); assertThat(response.getStatusLine().getStatusCode(), equalTo(409)); - assertThat(responseEntityToString(response), containsString("Cannot delete datafeed [" + datafeedId - + "] while its status is started")); + assertThat(EntityUtils.toString(response.getEntity()), + containsString("Cannot delete datafeed [" + datafeedId + "] while its status is started")); - response = client().performRequest("delete", - MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "?force=true"); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - assertThat(responseEntityToString(response), equalTo("{\"acknowledged\":true}")); + Request forceDeleteRequest = new Request("DELETE", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId); + forceDeleteRequest.addParameter("force", "true"); + response = client().performRequest(forceDeleteRequest); + assertThat(EntityUtils.toString(response.getEntity()), equalTo("{\"acknowledged\":true}")); expectThrows(ResponseException.class, - () -> client().performRequest("get", "/_xpack/ml/datafeeds/" + datafeedId)); + () -> client().performRequest(new Request("GET", "/_xpack/ml/datafeeds/" + datafeedId))); } private class LookbackOnlyTestHelper { @@ -727,9 +800,9 @@ public void execute() throws Exception { startDatafeedAndWaitUntilStopped(datafeedId); waitUntilJobIsClosed(jobId); - Response jobStatsResponse = client().performRequest("get", - MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - String jobStatsResponseAsString = responseEntityToString(jobStatsResponse); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + String jobStatsResponseAsString = EntityUtils.toString(jobStatsResponse.getEntity()); if (shouldSucceedInput) { assertThat(jobStatsResponseAsString, containsString("\"input_record_count\":2")); } else { @@ -748,16 +821,20 @@ private void startDatafeedAndWaitUntilStopped(String datafeedId) throws Exceptio } private void startDatafeedAndWaitUntilStopped(String datafeedId, String authHeader) throws Exception { - Response startDatafeedRequest = client().performRequest("post", - MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start?start=2016-06-01T00:00:00Z&end=2016-06-02T00:00:00Z", - new BasicHeader("Authorization", authHeader)); - assertThat(startDatafeedRequest.getStatusLine().getStatusCode(), equalTo(200)); - assertThat(responseEntityToString(startDatafeedRequest), equalTo("{\"started\":true}")); + Request request = new Request("POST", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_start"); + request.addParameter("start", "2016-06-01T00:00:00Z"); + request.addParameter("end", "2016-06-02T00:00:00Z"); + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader("Authorization", authHeader); + request.setOptions(options); + Response startDatafeedResponse = client().performRequest(request); + assertThat(EntityUtils.toString(startDatafeedResponse.getEntity()), equalTo("{\"started\":true}")); assertBusy(() -> { try { - Response datafeedStatsResponse = client().performRequest("get", - MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_stats"); - assertThat(responseEntityToString(datafeedStatsResponse), containsString("\"state\":\"stopped\"")); + Response datafeedStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "datafeeds/" + datafeedId + "/_stats")); + assertThat(EntityUtils.toString(datafeedStatsResponse.getEntity()), + containsString("\"state\":\"stopped\"")); } catch (Exception e) { throw new RuntimeException(e); } @@ -767,9 +844,9 @@ private void startDatafeedAndWaitUntilStopped(String datafeedId, String authHead private void waitUntilJobIsClosed(String jobId) throws Exception { assertBusy(() -> { try { - Response jobStatsResponse = client().performRequest("get", - MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"); - assertThat(responseEntityToString(jobStatsResponse), containsString("\"state\":\"closed\"")); + Response jobStatsResponse = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + assertThat(EntityUtils.toString(jobStatsResponse.getEntity()), containsString("\"state\":\"closed\"")); } catch (Exception e) { throw new RuntimeException(e); } @@ -777,27 +854,30 @@ private void waitUntilJobIsClosed(String jobId) throws Exception { } private Response createJob(String id, String airlineVariant) throws Exception { - String job = "{\n" + " \"description\":\"Analysis of response time by airline\",\n" - + " \"analysis_config\" : {\n" + " \"bucket_span\":\"1h\",\n" - + " \"detectors\" :[\n" - + " {\"function\":\"mean\",\"field_name\":\"responsetime\",\"by_field_name\":\"" + airlineVariant + "\"}]\n" - + " },\n" + " \"data_description\" : {\n" - + " \"format\":\"xcontent\",\n" - + " \"time_field\":\"time stamp\",\n" + " \"time_format\":\"yyyy-MM-dd'T'HH:mm:ssX\"\n" + " }\n" - + "}"; - return client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + id, - Collections.emptyMap(), new StringEntity(job, ContentType.APPLICATION_JSON)); - } - - private static String responseEntityToString(Response response) throws Exception { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))) { - return reader.lines().collect(Collectors.joining("\n")); - } + Request request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + id); + request.setJsonEntity("{\n" + + " \"description\": \"Analysis of response time by airline\",\n" + + " \"analysis_config\": {\n" + + " \"bucket_span\": \"1h\",\n" + + " \"detectors\" :[\n" + + " {\n" + + " \"function\": \"mean\",\n" + + " \"field_name\": \"responsetime\",\n" + + " \"by_field_name\": \"" + airlineVariant + "\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"data_description\": {\n" + + " \"format\": \"xcontent\",\n" + + " \"time_field\": \"time stamp\",\n" + + " \"time_format\": \"yyyy-MM-dd'T'HH:mm:ssX\"\n" + + " }\n" + + "}"); + return client().performRequest(request); } public static void openJob(RestClient client, String jobId) throws IOException { - Response response = client.performRequest("post", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open"); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + client.performRequest(new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_open")); } @After @@ -850,17 +930,28 @@ DatafeedBuilder setChunkingTimespan(String timespan) { } Response build() throws IOException { - String datafeedConfig = "{" + Request request = new Request("PUT", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId); + request.setJsonEntity("{" + "\"job_id\": \"" + jobId + "\",\"indexes\":[\"" + index + "\"],\"types\":[\"" + type + "\"]" + (source ? ",\"_source\":true" : "") + (scriptedFields == null ? "" : ",\"script_fields\":" + scriptedFields) + (aggregations == null ? "" : ",\"aggs\":" + aggregations) + (chunkingTimespan == null ? "" : ",\"chunking_config\":{\"mode\":\"MANUAL\",\"time_span\":\"" + chunkingTimespan + "\"}") - + "}"; - return client().performRequest("put", MachineLearning.BASE_PATH + "datafeeds/" + datafeedId, Collections.emptyMap(), - new StringEntity(datafeedConfig, ContentType.APPLICATION_JSON), - new BasicHeader("Authorization", authHeader)); + + "}"); + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader("Authorization", authHeader); + request.setOptions(options); + return client().performRequest(request); } } + + private void bulkIndex(String bulk) throws IOException { + Request bulkRequest = new Request("POST", "/_bulk"); + bulkRequest.setJsonEntity(bulk); + bulkRequest.addParameter("refresh", "true"); + bulkRequest.addParameter("pretty", null); + String bulkResponse = EntityUtils.toString(client().performRequest(bulkRequest).getEntity()); + assertThat(bulkResponse, not(containsString("\"errors\": false"))); + } } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java index 07529acdb881..5fc204cbf1f8 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/MlJobIT.java @@ -5,8 +5,7 @@ */ package org.elasticsearch.xpack.ml.integration; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; @@ -23,15 +22,10 @@ import org.elasticsearch.xpack.test.rest.XPackRestTestHelper; import org.junit.After; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.containsString; @@ -55,15 +49,13 @@ protected boolean preserveTemplatesUponCompletion() { public void testPutJob_GivenFarequoteConfig() throws Exception { Response response = createFarequoteJob("given-farequote-config-job"); - - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - String responseAsString = responseEntityToString(response); + String responseAsString = EntityUtils.toString(response.getEntity()); assertThat(responseAsString, containsString("\"job_id\":\"given-farequote-config-job\"")); } public void testGetJob_GivenNoSuchJob() throws Exception { - ResponseException e = expectThrows(ResponseException.class, - () -> client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/non-existing-job/_stats")); + ResponseException e = expectThrows(ResponseException.class, () -> + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/non-existing-job/_stats"))); assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(404)); assertThat(e.getMessage(), containsString("No known job with id 'non-existing-job'")); @@ -72,11 +64,9 @@ public void testGetJob_GivenNoSuchJob() throws Exception { public void testGetJob_GivenJobExists() throws Exception { createFarequoteJob("get-job_given-job-exists-job"); - Response response = client().performRequest("get", - MachineLearning.BASE_PATH + "anomaly_detectors/get-job_given-job-exists-job/_stats"); - - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - String responseAsString = responseEntityToString(response); + Response response = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/get-job_given-job-exists-job/_stats")); + String responseAsString = EntityUtils.toString(response.getEntity()); assertThat(responseAsString, containsString("\"count\":1")); assertThat(responseAsString, containsString("\"job_id\":\"get-job_given-job-exists-job\"")); } @@ -86,20 +76,16 @@ public void testGetJobs_GivenSingleJob() throws Exception { createFarequoteJob(jobId); // Explicit _all - Response response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/_all"); - - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - String responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":1")); - assertThat(responseAsString, containsString("\"job_id\":\"" + jobId + "\"")); + String explictAll = EntityUtils.toString( + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/_all")).getEntity()); + assertThat(explictAll, containsString("\"count\":1")); + assertThat(explictAll, containsString("\"job_id\":\"" + jobId + "\"")); // Implicit _all - response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors"); - - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":1")); - assertThat(responseAsString, containsString("\"job_id\":\"" + jobId + "\"")); + String implicitAll = EntityUtils.toString( + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors")).getEntity()); + assertThat(implicitAll, containsString("\"count\":1")); + assertThat(implicitAll, containsString("\"job_id\":\"" + jobId + "\"")); } public void testGetJobs_GivenMultipleJobs() throws Exception { @@ -108,36 +94,37 @@ public void testGetJobs_GivenMultipleJobs() throws Exception { createFarequoteJob("given-multiple-jobs-job-3"); // Explicit _all - Response response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/_all"); - - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - String responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":3")); - assertThat(responseAsString, containsString("\"job_id\":\"given-multiple-jobs-job-1\"")); - assertThat(responseAsString, containsString("\"job_id\":\"given-multiple-jobs-job-2\"")); - assertThat(responseAsString, containsString("\"job_id\":\"given-multiple-jobs-job-3\"")); + String explicitAll = EntityUtils.toString( + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/_all")).getEntity()); + assertThat(explicitAll, containsString("\"count\":3")); + assertThat(explicitAll, containsString("\"job_id\":\"given-multiple-jobs-job-1\"")); + assertThat(explicitAll, containsString("\"job_id\":\"given-multiple-jobs-job-2\"")); + assertThat(explicitAll, containsString("\"job_id\":\"given-multiple-jobs-job-3\"")); // Implicit _all - response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors"); - - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":3")); - assertThat(responseAsString, containsString("\"job_id\":\"given-multiple-jobs-job-1\"")); - assertThat(responseAsString, containsString("\"job_id\":\"given-multiple-jobs-job-2\"")); - assertThat(responseAsString, containsString("\"job_id\":\"given-multiple-jobs-job-3\"")); + String implicitAll = EntityUtils.toString( + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors")).getEntity()); + assertThat(implicitAll, containsString("\"count\":3")); + assertThat(implicitAll, containsString("\"job_id\":\"given-multiple-jobs-job-1\"")); + assertThat(implicitAll, containsString("\"job_id\":\"given-multiple-jobs-job-2\"")); + assertThat(implicitAll, containsString("\"job_id\":\"given-multiple-jobs-job-3\"")); } private Response createFarequoteJob(String jobId) throws IOException { - String job = "{\n" + " \"description\":\"Analysis of response time by airline\",\n" - + " \"analysis_config\" : {\n" + " \"bucket_span\": \"3600s\",\n" + Request request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + request.setJsonEntity( + "{\n" + + " \"description\":\"Analysis of response time by airline\",\n" + + " \"analysis_config\" : {\n" + + " \"bucket_span\": \"3600s\",\n" + " \"detectors\" :[{\"function\":\"metric\",\"field_name\":\"responsetime\",\"by_field_name\":\"airline\"}]\n" - + " },\n" + " \"data_description\" : {\n" + " \"field_delimiter\":\",\",\n" + " " + - "\"time_field\":\"time\",\n" - + " \"time_format\":\"yyyy-MM-dd HH:mm:ssX\"\n" + " }\n" + "}"; - - return client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, - Collections.emptyMap(), new StringEntity(job, ContentType.APPLICATION_JSON)); + + " },\n" + " \"data_description\" : {\n" + + " \"field_delimiter\":\",\",\n" + + " \"time_field\":\"time\",\n" + + " \"time_format\":\"yyyy-MM-dd HH:mm:ssX\"\n" + + " }\n" + + "}"); + return client().performRequest(request); } public void testCantCreateJobWithSameID() throws Exception { @@ -148,18 +135,14 @@ public void testCantCreateJobWithSameID() throws Exception { " \"data_description\": {},\n" + " \"results_index_name\" : \"%s\"}"; - String jobConfig = String.format(Locale.ROOT, jobTemplate, "index-1"); - String jobId = "cant-create-job-with-same-id-job"; - Response response = client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId , - Collections.emptyMap(), - new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); + Request createJob1 = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJob1.setJsonEntity(String.format(Locale.ROOT, jobTemplate, "index-1")); + client().performRequest(createJob1); - final String jobConfig2 = String.format(Locale.ROOT, jobTemplate, "index-2"); - ResponseException e = expectThrows(ResponseException.class, - () ->client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId, - Collections.emptyMap(), new StringEntity(jobConfig2, ContentType.APPLICATION_JSON))); + Request createJob2 = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + createJob2.setJsonEntity(String.format(Locale.ROOT, jobTemplate, "index-2")); + ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(createJob2)); assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(400)); assertThat(e.getMessage(), containsString("The job cannot be created with the Id '" + jobId + "'. The Id is already used.")); @@ -175,94 +158,78 @@ public void testCreateJobsWithIndexNameOption() throws Exception { String jobId1 = "create-jobs-with-index-name-option-job-1"; String indexName = "non-default-index"; - String jobConfig = String.format(Locale.ROOT, jobTemplate, indexName); - - Response response = client().performRequest("put", MachineLearning.BASE_PATH - + "anomaly_detectors/" + jobId1, Collections.emptyMap(), new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); + Request createJob1 = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1); + createJob1.setJsonEntity(String.format(Locale.ROOT, jobTemplate, indexName)); + client().performRequest(createJob1); String jobId2 = "create-jobs-with-index-name-option-job-2"; - response = client().performRequest("put", MachineLearning.BASE_PATH - + "anomaly_detectors/" + jobId2, Collections.emptyMap(), new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); + Request createJob2 = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId2); + createJob2.setEntity(createJob1.getEntity()); + client().performRequest(createJob2); // With security enabled GET _aliases throws an index_not_found_exception // if no aliases have been created. In multi-node tests the alias may not // appear immediately so wait here. assertBusy(() -> { try { - Response aliasesResponse = client().performRequest("get", "_aliases"); - assertEquals(200, aliasesResponse.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(aliasesResponse); - assertThat(responseAsString, + String aliasesResponse = EntityUtils.toString(client().performRequest(new Request("GET", "/_aliases")).getEntity()); + assertThat(aliasesResponse, containsString("\"" + AnomalyDetectorsIndex.jobResultsAliasedName("custom-" + indexName) + "\":{\"aliases\":{")); - assertThat(responseAsString, containsString("\"" + AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + assertThat(aliasesResponse, containsString("\"" + AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + "\":{\"filter\":{\"term\":{\"job_id\":{\"value\":\"" + jobId1 + "\",\"boost\":1.0}}}}")); - assertThat(responseAsString, containsString("\"" + AnomalyDetectorsIndex.resultsWriteAlias(jobId1) + "\":{}")); - assertThat(responseAsString, containsString("\"" + AnomalyDetectorsIndex.jobResultsAliasedName(jobId2) + assertThat(aliasesResponse, containsString("\"" + AnomalyDetectorsIndex.resultsWriteAlias(jobId1) + "\":{}")); + assertThat(aliasesResponse, containsString("\"" + AnomalyDetectorsIndex.jobResultsAliasedName(jobId2) + "\":{\"filter\":{\"term\":{\"job_id\":{\"value\":\"" + jobId2 + "\",\"boost\":1.0}}}}")); - assertThat(responseAsString, containsString("\"" + AnomalyDetectorsIndex.resultsWriteAlias(jobId2) + "\":{}")); + assertThat(aliasesResponse, containsString("\"" + AnomalyDetectorsIndex.resultsWriteAlias(jobId2) + "\":{}")); } catch (ResponseException e) { throw new AssertionError(e); } }); - Response indicesResponse = client().performRequest("get", "_cat/indices"); - assertEquals(200, indicesResponse.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(indicesResponse); + String responseAsString = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); assertThat(responseAsString, containsString(AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + "custom-" + indexName)); assertThat(responseAsString, not(containsString(AnomalyDetectorsIndex.jobResultsAliasedName(jobId1)))); assertThat(responseAsString, not(containsString(AnomalyDetectorsIndex.jobResultsAliasedName(jobId2)))); - String bucketResult = String.format(Locale.ROOT, - "{\"job_id\":\"%s\", \"timestamp\": \"%s\", \"result_type\":\"bucket\", \"bucket_span\": \"%s\"}", - jobId1, "1234", 1); String id = String.format(Locale.ROOT, "%s_bucket_%s_%s", jobId1, "1234", 300); - response = client().performRequest("put", AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + "/doc/" + id, - Collections.emptyMap(), new StringEntity(bucketResult, ContentType.APPLICATION_JSON)); - assertEquals(201, response.getStatusLine().getStatusCode()); - - bucketResult = String.format(Locale.ROOT, + Request createResultRequest = new Request("PUT", AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + "/doc/" + id); + createResultRequest.setJsonEntity(String.format(Locale.ROOT, "{\"job_id\":\"%s\", \"timestamp\": \"%s\", \"result_type\":\"bucket\", \"bucket_span\": \"%s\"}", - jobId1, "1236", 1); + jobId1, "1234", 1)); + client().performRequest(createResultRequest); + id = String.format(Locale.ROOT, "%s_bucket_%s_%s", jobId1, "1236", 300); - response = client().performRequest("put", AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + "/doc/" + id, - Collections.emptyMap(), new StringEntity(bucketResult, ContentType.APPLICATION_JSON)); - assertEquals(201, response.getStatusLine().getStatusCode()); + createResultRequest = new Request("PUT", AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + "/doc/" + id); + createResultRequest.setJsonEntity(String.format(Locale.ROOT, + "{\"job_id\":\"%s\", \"timestamp\": \"%s\", \"result_type\":\"bucket\", \"bucket_span\": \"%s\"}", + jobId1, "1236", 1)); + client().performRequest(createResultRequest); - client().performRequest("post", "_refresh"); + client().performRequest(new Request("POST", "/_refresh")); - response = client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1 + "/results/buckets"); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - responseAsString = responseEntityToString(response); + responseAsString = EntityUtils.toString(client().performRequest( + new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1 + "/results/buckets")).getEntity()); assertThat(responseAsString, containsString("\"count\":2")); - response = client().performRequest("get", AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + "/_search"); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); - responseAsString = responseEntityToString(response); + responseAsString = EntityUtils.toString(client().performRequest( + new Request("GET", AnomalyDetectorsIndex.jobResultsAliasedName(jobId1) + "/_search")).getEntity()); assertThat(responseAsString, containsString("\"total\":2")); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1)); // check that indices still exist, but are empty and aliases are gone - response = client().performRequest("get", "_aliases"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); + responseAsString = EntityUtils.toString(client().performRequest(new Request("GET", "/_aliases")).getEntity()); assertThat(responseAsString, not(containsString(AnomalyDetectorsIndex.jobResultsAliasedName(jobId1)))); assertThat(responseAsString, containsString(AnomalyDetectorsIndex.jobResultsAliasedName(jobId2))); //job2 still exists - response = client().performRequest("get", "_cat/indices"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); + responseAsString = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); assertThat(responseAsString, containsString(AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + "custom-" + indexName)); - client().performRequest("post", "_refresh"); + client().performRequest(new Request("POST", "/_refresh")); - response = client().performRequest("get", AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + "custom-" + indexName + "/_count"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); + responseAsString = EntityUtils.toString(client().performRequest( + new Request("GET", AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + "custom-" + indexName + "/_count")).getEntity()); assertThat(responseAsString, containsString("\"count\":0")); } @@ -278,32 +245,27 @@ public void testCreateJobInSharedIndexUpdatesMapping() throws Exception { String byFieldName1 = "responsetime"; String jobId2 = "create-job-in-shared-index-updates-mapping-job-2"; String byFieldName2 = "cpu-usage"; - String jobConfig = String.format(Locale.ROOT, jobTemplate, byFieldName1); - Response response = client().performRequest("put", MachineLearning.BASE_PATH - + "anomaly_detectors/" + jobId1, Collections.emptyMap(), new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); + Request createJob1Request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1); + createJob1Request.setJsonEntity(String.format(Locale.ROOT, jobTemplate, byFieldName1)); + client().performRequest(createJob1Request); // Check the index mapping contains the first by_field_name - response = client().performRequest("get", AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX - + AnomalyDetectorsIndexFields.RESULTS_INDEX_DEFAULT + "/_mapping?pretty"); - assertEquals(200, response.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(byFieldName1)); - assertThat(responseAsString, not(containsString(byFieldName2))); - - jobConfig = String.format(Locale.ROOT, jobTemplate, byFieldName2); - response = client().performRequest("put", MachineLearning.BASE_PATH - + "anomaly_detectors/" + jobId2, Collections.emptyMap(), new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); + Request getResultsMappingRequest = new Request("GET", + AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + AnomalyDetectorsIndexFields.RESULTS_INDEX_DEFAULT + "/_mapping"); + getResultsMappingRequest.addParameter("pretty", null); + String resultsMappingAfterJob1 = EntityUtils.toString(client().performRequest(getResultsMappingRequest).getEntity()); + assertThat(resultsMappingAfterJob1, containsString(byFieldName1)); + assertThat(resultsMappingAfterJob1, not(containsString(byFieldName2))); + + Request createJob2Request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId2); + createJob2Request.setJsonEntity(String.format(Locale.ROOT, jobTemplate, byFieldName2)); + client().performRequest(createJob2Request); // Check the index mapping now contains both fields - response = client().performRequest("get", AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX - + AnomalyDetectorsIndexFields.RESULTS_INDEX_DEFAULT + "/_mapping?pretty"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(byFieldName1)); - assertThat(responseAsString, containsString(byFieldName2)); + String resultsMappingAfterJob2 = EntityUtils.toString(client().performRequest(getResultsMappingRequest).getEntity()); + assertThat(resultsMappingAfterJob2, containsString(byFieldName1)); + assertThat(resultsMappingAfterJob2, containsString(byFieldName2)); } public void testCreateJobInCustomSharedIndexUpdatesMapping() throws Exception { @@ -318,32 +280,27 @@ public void testCreateJobInCustomSharedIndexUpdatesMapping() throws Exception { String byFieldName1 = "responsetime"; String jobId2 = "create-job-in-custom-shared-index-updates-mapping-job-2"; String byFieldName2 = "cpu-usage"; - String jobConfig = String.format(Locale.ROOT, jobTemplate, byFieldName1); - Response response = client().performRequest("put", MachineLearning.BASE_PATH - + "anomaly_detectors/" + jobId1, Collections.emptyMap(), new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); + Request createJob1Request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1); + createJob1Request.setJsonEntity(String.format(Locale.ROOT, jobTemplate, byFieldName1)); + client().performRequest(createJob1Request); // Check the index mapping contains the first by_field_name - response = client().performRequest("get", - AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + "custom-shared-index" + "/_mapping?pretty"); - assertEquals(200, response.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(byFieldName1)); - assertThat(responseAsString, not(containsString(byFieldName2))); - - jobConfig = String.format(Locale.ROOT, jobTemplate, byFieldName2); - response = client().performRequest("put", MachineLearning.BASE_PATH - + "anomaly_detectors/" + jobId2, Collections.emptyMap(), new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); + Request getResultsMappingRequest = new Request("GET", + AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + "custom-shared-index/_mapping"); + getResultsMappingRequest.addParameter("pretty", null); + String resultsMappingAfterJob1 = EntityUtils.toString(client().performRequest(getResultsMappingRequest).getEntity()); + assertThat(resultsMappingAfterJob1, containsString(byFieldName1)); + assertThat(resultsMappingAfterJob1, not(containsString(byFieldName2))); + + Request createJob2Request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId2); + createJob2Request.setJsonEntity(String.format(Locale.ROOT, jobTemplate, byFieldName2)); + client().performRequest(createJob2Request); // Check the index mapping now contains both fields - response = client().performRequest("get", - AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + "custom-shared-index" + "/_mapping?pretty"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(byFieldName1)); - assertThat(responseAsString, containsString(byFieldName2)); + String resultsMappingAfterJob2 = EntityUtils.toString(client().performRequest(getResultsMappingRequest).getEntity()); + assertThat(resultsMappingAfterJob2, containsString(byFieldName1)); + assertThat(resultsMappingAfterJob2, containsString(byFieldName2)); } public void testCreateJob_WithClashingFieldMappingsFails() throws Exception { @@ -366,17 +323,14 @@ public void testCreateJob_WithClashingFieldMappingsFails() throws Exception { byFieldName1 = "response.time"; byFieldName2 = "response"; } - String jobConfig = String.format(Locale.ROOT, jobTemplate, byFieldName1); - Response response = client().performRequest("put", MachineLearning.BASE_PATH - + "anomaly_detectors/" + jobId1, Collections.emptyMap(), new StringEntity(jobConfig, ContentType.APPLICATION_JSON)); - assertEquals(200, response.getStatusLine().getStatusCode()); - - final String failingJobConfig = String.format(Locale.ROOT, jobTemplate, byFieldName2); - ResponseException e = expectThrows(ResponseException.class, - () -> client().performRequest("put", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId2, - Collections.emptyMap(), new StringEntity(failingJobConfig, ContentType.APPLICATION_JSON))); + Request createJob1Request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId1); + createJob1Request.setJsonEntity(String.format(Locale.ROOT, jobTemplate, byFieldName1)); + client().performRequest(createJob1Request); + Request createJob2Request = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId2); + createJob2Request.setJsonEntity(String.format(Locale.ROOT, jobTemplate, byFieldName2)); + ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(createJob2Request)); assertThat(e.getMessage(), containsString("This job would cause a mapping clash with existing field [response] - " + "avoid the clash by assigning a dedicated results index")); @@ -387,35 +341,27 @@ public void testDeleteJob() throws Exception { String indexName = AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + AnomalyDetectorsIndexFields.RESULTS_INDEX_DEFAULT; createFarequoteJob(jobId); - Response response = client().performRequest("get", "_cat/indices"); - assertEquals(200, response.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(indexName)); + String indicesBeforeDelete = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); + assertThat(indicesBeforeDelete, containsString(indexName)); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); // check that the index still exists (it's shared by default) - response = client().performRequest("get", "_cat/indices"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(indexName)); + String indicesAfterDelete = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); + assertThat(indicesAfterDelete, containsString(indexName)); assertBusy(() -> { try { - Response r = client().performRequest("get", indexName + "/_count"); - assertEquals(200, r.getStatusLine().getStatusCode()); - String responseString = responseEntityToString(r); - assertThat(responseString, containsString("\"count\":0")); + String count = EntityUtils.toString(client().performRequest(new Request("GET", indexName + "/_count")).getEntity()); + assertThat(count, containsString("\"count\":0")); } catch (Exception e) { fail(e.getMessage()); } - }); // check that the job itself is gone expectThrows(ResponseException.class, () -> - client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"))); } public void testDeleteJobAfterMissingIndex() throws Exception { @@ -424,28 +370,22 @@ public void testDeleteJobAfterMissingIndex() throws Exception { String indexName = AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + AnomalyDetectorsIndexFields.RESULTS_INDEX_DEFAULT; createFarequoteJob(jobId); - Response response = client().performRequest("get", "_cat/indices"); - assertEquals(200, response.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(indexName)); + String indicesBeforeDelete = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); + assertThat(indicesBeforeDelete, containsString(indexName)); // Manually delete the index so that we can test that deletion proceeds // normally anyway - response = client().performRequest("delete", indexName); - assertEquals(200, response.getStatusLine().getStatusCode()); + client().performRequest(new Request("DELETE", indexName)); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); // check index was deleted - response = client().performRequest("get", "_cat/indices"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, not(containsString(aliasName))); - assertThat(responseAsString, not(containsString(indexName))); + String indicesAfterDelete = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); + assertThat(indicesAfterDelete, not(containsString(aliasName))); + assertThat(indicesAfterDelete, not(containsString(indexName))); expectThrows(ResponseException.class, () -> - client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"))); } public void testDeleteJobAfterMissingAliases() throws Exception { @@ -460,11 +400,9 @@ public void testDeleteJobAfterMissingAliases() throws Exception { // appear immediately so wait here. assertBusy(() -> { try { - Response aliasesResponse = client().performRequest(new Request("get", "_cat/aliases")); - assertEquals(200, aliasesResponse.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(aliasesResponse); - assertThat(responseAsString, containsString(readAliasName)); - assertThat(responseAsString, containsString(writeAliasName)); + String aliases = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/aliases")).getEntity()); + assertThat(aliases, containsString(readAliasName)); + assertThat(aliases, containsString(writeAliasName)); } catch (ResponseException e) { throw new AssertionError(e); } @@ -472,17 +410,14 @@ public void testDeleteJobAfterMissingAliases() throws Exception { // Manually delete the aliases so that we can test that deletion proceeds // normally anyway - Response response = client().performRequest("delete", indexName + "/_alias/" + readAliasName); - assertEquals(200, response.getStatusLine().getStatusCode()); - response = client().performRequest("delete", indexName + "/_alias/" + writeAliasName); - assertEquals(200, response.getStatusLine().getStatusCode()); + client().performRequest(new Request("DELETE", indexName + "/_alias/" + readAliasName)); + client().performRequest(new Request("DELETE", indexName + "/_alias/" + writeAliasName)); // check aliases were deleted - expectThrows(ResponseException.class, () -> client().performRequest("get", indexName + "/_alias/" + readAliasName)); - expectThrows(ResponseException.class, () -> client().performRequest("get", indexName + "/_alias/" + writeAliasName)); + expectThrows(ResponseException.class, () -> client().performRequest(new Request("GET", indexName + "/_alias/" + readAliasName))); + expectThrows(ResponseException.class, () -> client().performRequest(new Request("GET", indexName + "/_alias/" + writeAliasName))); - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); } public void testMultiIndexDelete() throws Exception { @@ -490,86 +425,63 @@ public void testMultiIndexDelete() throws Exception { String indexName = AnomalyDetectorsIndexFields.RESULTS_INDEX_PREFIX + AnomalyDetectorsIndexFields.RESULTS_INDEX_DEFAULT; createFarequoteJob(jobId); - Response response = client().performRequest("put", indexName + "-001"); - assertEquals(200, response.getStatusLine().getStatusCode()); + client().performRequest(new Request("PUT", indexName + "-001")); + client().performRequest(new Request("PUT", indexName + "-002")); - response = client().performRequest("put", indexName + "-002"); - assertEquals(200, response.getStatusLine().getStatusCode()); - - response = client().performRequest("get", "_cat/indices"); - assertEquals(200, response.getStatusLine().getStatusCode()); - String responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(indexName)); - assertThat(responseAsString, containsString(indexName + "-001")); - assertThat(responseAsString, containsString(indexName + "-002")); + String indicesBeforeDelete = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); + assertThat(indicesBeforeDelete, containsString(indexName)); + assertThat(indicesBeforeDelete, containsString(indexName + "-001")); + assertThat(indicesBeforeDelete, containsString(indexName + "-002")); // Add some documents to each index to make sure the DBQ clears them out - String recordResult = - String.format(Locale.ROOT, + Request createDoc0 = new Request("PUT", indexName + "/doc/" + 123); + createDoc0.setJsonEntity(String.format(Locale.ROOT, "{\"job_id\":\"%s\", \"timestamp\": \"%s\", \"bucket_span\":%d, \"result_type\":\"record\"}", - jobId, 123, 1); - client().performRequest("put", indexName + "/doc/" + 123, - Collections.singletonMap("refresh", "true"), new StringEntity(recordResult, ContentType.APPLICATION_JSON)); - client().performRequest("put", indexName + "-001/doc/" + 123, - Collections.singletonMap("refresh", "true"), new StringEntity(recordResult, ContentType.APPLICATION_JSON)); - client().performRequest("put", indexName + "-002/doc/" + 123, - Collections.singletonMap("refresh", "true"), new StringEntity(recordResult, ContentType.APPLICATION_JSON)); + jobId, 123, 1)); + client().performRequest(createDoc0); + Request createDoc1 = new Request("PUT", indexName + "-001/doc/" + 123); + createDoc1.setEntity(createDoc0.getEntity()); + client().performRequest(createDoc1); + Request createDoc2 = new Request("PUT", indexName + "-002/doc/" + 123); + createDoc2.setEntity(createDoc0.getEntity()); + client().performRequest(createDoc2); // Also index a few through the alias for the first job - client().performRequest("put", indexName + "/doc/" + 456, - Collections.singletonMap("refresh", "true"), new StringEntity(recordResult, ContentType.APPLICATION_JSON)); - + Request createDoc3 = new Request("PUT", indexName + "/doc/" + 456); + createDoc3.setEntity(createDoc0.getEntity()); + client().performRequest(createDoc3); - client().performRequest("post", "_refresh"); + client().performRequest(new Request("POST", "/_refresh")); // check for the documents - response = client().performRequest("get", indexName+ "/_count"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":2")); - - response = client().performRequest("get", indexName + "-001/_count"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":1")); - - response = client().performRequest("get", indexName + "-002/_count"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":1")); + assertThat(EntityUtils.toString(client().performRequest(new Request("GET", indexName+ "/_count")).getEntity()), + containsString("\"count\":2")); + assertThat(EntityUtils.toString(client().performRequest(new Request("GET", indexName+ "-001/_count")).getEntity()), + containsString("\"count\":1")); + assertThat(EntityUtils.toString(client().performRequest(new Request("GET", indexName+ "-002/_count")).getEntity()), + containsString("\"count\":1")); // Delete - response = client().performRequest("delete", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); - assertThat(response.getStatusLine().getStatusCode(), equalTo(200)); + client().performRequest(new Request("DELETE", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); - client().performRequest("post", "_refresh"); + client().performRequest(new Request("POST", "/_refresh")); // check that the indices still exist but are empty - response = client().performRequest("get", "_cat/indices"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString(indexName)); - assertThat(responseAsString, containsString(indexName + "-001")); - assertThat(responseAsString, containsString(indexName + "-002")); - - response = client().performRequest("get", indexName + "/_count"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":0")); - - response = client().performRequest("get", indexName + "-001/_count"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":0")); + String indicesAfterDelete = EntityUtils.toString(client().performRequest(new Request("GET", "/_cat/indices")).getEntity()); + assertThat(indicesAfterDelete, containsString(indexName)); + assertThat(indicesAfterDelete, containsString(indexName + "-001")); + assertThat(indicesAfterDelete, containsString(indexName + "-002")); - response = client().performRequest("get", indexName + "-002/_count"); - assertEquals(200, response.getStatusLine().getStatusCode()); - responseAsString = responseEntityToString(response); - assertThat(responseAsString, containsString("\"count\":0")); + assertThat(EntityUtils.toString(client().performRequest(new Request("GET", indexName+ "/_count")).getEntity()), + containsString("\"count\":0")); + assertThat(EntityUtils.toString(client().performRequest(new Request("GET", indexName+ "-001/_count")).getEntity()), + containsString("\"count\":0")); + assertThat(EntityUtils.toString(client().performRequest(new Request("GET", indexName+ "-002/_count")).getEntity()), + containsString("\"count\":0")); expectThrows(ResponseException.class, () -> - client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats")); + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId + "/_stats"))); } public void testDelete_multipleRequest() throws Exception { @@ -590,7 +502,7 @@ public void testDelete_multipleRequest() throws Exception { if (forceDelete) { url += "?force=true"; } - Response response = client().performRequest("delete", url); + Response response = client().performRequest(new Request("DELETE", url)); responses.put(Thread.currentThread().getId(), response); } catch (ResponseException re) { responseExceptions.put(Thread.currentThread().getId(), re); @@ -640,11 +552,12 @@ public void testDelete_multipleRequest() throws Exception { } for (Response response : responses.values()) { - assertEquals(responseEntityToString(response), 200, response.getStatusLine().getStatusCode()); + assertEquals(EntityUtils.toString(response.getEntity()), 200, response.getStatusLine().getStatusCode()); } assertNotNull(recreationResponse.get()); - assertEquals(responseEntityToString(recreationResponse.get()), 200, recreationResponse.get().getStatusLine().getStatusCode()); + assertEquals(EntityUtils.toString(recreationResponse.get().getEntity()), + 200, recreationResponse.get().getStatusLine().getStatusCode()); if (recreationException.get() != null) { assertNull(recreationException.get().getMessage(), recreationException.get()); @@ -656,7 +569,7 @@ public void testDelete_multipleRequest() throws Exception { // but in the case that it does not the job that is recreated may get deleted. // It is not a error if the job does not exist but the following assertions // will fail in that case. - client().performRequest("get", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId); + client().performRequest(new Request("GET", MachineLearning.BASE_PATH + "anomaly_detectors/" + jobId)); // Check that the job aliases exist. These are the last thing to be deleted when a job is deleted, so // if there's been a race between deletion and recreation these are what will be missing. @@ -682,15 +595,8 @@ public void testDelete_multipleRequest() throws Exception { } private String getAliases() throws IOException { - Response response = client().performRequest("get", "_aliases"); - assertEquals(200, response.getStatusLine().getStatusCode()); - return responseEntityToString(response); - } - - private static String responseEntityToString(Response response) throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))) { - return reader.lines().collect(Collectors.joining("\n")); - } + Response response = client().performRequest(new Request("GET", "/_aliases")); + return EntityUtils.toString(response.getEntity()); } @After diff --git a/x-pack/plugin/ml/qa/single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java b/x-pack/plugin/ml/qa/single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java index 0751d7307ae9..ffd869a4a6e2 100644 --- a/x-pack/plugin/ml/qa/single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java +++ b/x-pack/plugin/ml/qa/single-node-tests/src/test/java/org/elasticsearch/xpack/ml/transforms/PainlessDomainSplitIT.java @@ -5,9 +5,8 @@ */ package org.elasticsearch.xpack.ml.transforms; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; @@ -18,7 +17,6 @@ import org.joda.time.DateTime; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -185,9 +183,10 @@ public void testIsolated() throws Exception { .put(IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0); createIndex("painless", settings.build()); - client().performRequest("PUT", "painless/test/1", Collections.emptyMap(), - new StringEntity("{\"test\": \"test\"}", ContentType.APPLICATION_JSON)); - client().performRequest("POST", "painless/_refresh"); + Request createDoc = new Request("PUT", "/painless/test/1"); + createDoc.setJsonEntity("{\"test\": \"test\"}"); + createDoc.addParameter("refresh", "true"); + client().performRequest(createDoc); Pattern pattern = Pattern.compile("domain_split\":\\[(.*?),(.*?)\\]"); @@ -198,7 +197,9 @@ public void testIsolated() throws Exception { String mapAsJson = Strings.toString(jsonBuilder().map(params)); logger.info("params={}", mapAsJson); - StringEntity body = new StringEntity("{\n" + + Request searchRequest = new Request("GET", "/painless/test/_search"); + searchRequest.setJsonEntity( + "{\n" + " \"query\" : {\n" + " \"match_all\": {}\n" + " },\n" + @@ -212,10 +213,8 @@ public void testIsolated() throws Exception { " }\n" + " }\n" + " }\n" + - "}", ContentType.APPLICATION_JSON); - - Response response = client().performRequest("GET", "painless/test/_search", Collections.emptyMap(), body); - String responseBody = EntityUtils.toString(response.getEntity()); + "}"); + String responseBody = EntityUtils.toString(client().performRequest(searchRequest).getEntity()); Matcher m = pattern.matcher(responseBody); String actualSubDomain = ""; @@ -242,24 +241,23 @@ public void testIsolated() throws Exception { @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32966") public void testHRDSplit() throws Exception { - // Create job - String job = "{\n" + - " \"description\":\"Domain splitting\",\n" + - " \"analysis_config\" : {\n" + - " \"bucket_span\":\"3600s\",\n" + - " \"detectors\" :[{\"function\":\"count\", \"by_field_name\" : \"domain_split\"}]\n" + - " },\n" + - " \"data_description\" : {\n" + - " \"field_delimiter\":\",\",\n" + - " \"time_field\":\"time\"\n" + - " \n" + - " }\n" + - " }"; - - client().performRequest("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/hrd-split-job", Collections.emptyMap(), - new StringEntity(job, ContentType.APPLICATION_JSON)); - client().performRequest("POST", MachineLearning.BASE_PATH + "anomaly_detectors/hrd-split-job/_open"); + Request createJobRequest = new Request("PUT", MachineLearning.BASE_PATH + "anomaly_detectors/hrd-split-job"); + createJobRequest.setJsonEntity( + "{\n" + + " \"description\":\"Domain splitting\",\n" + + " \"analysis_config\" : {\n" + + " \"bucket_span\":\"3600s\",\n" + + " \"detectors\" :[{\"function\":\"count\", \"by_field_name\" : \"domain_split\"}]\n" + + " },\n" + + " \"data_description\" : {\n" + + " \"field_delimiter\":\",\",\n" + + " \"time_field\":\"time\"\n" + + " \n" + + " }\n" + + "}"); + client().performRequest(createJobRequest); + client().performRequest(new Request("POST", MachineLearning.BASE_PATH + "anomaly_detectors/hrd-split-job/_open")); // Create index to hold data Settings.Builder settings = Settings.builder() @@ -284,44 +282,43 @@ public void testHRDSplit() throws Exception { if (i == 64) { // Anomaly has 100 docs, but we don't care about the value for (int j = 0; j < 100; j++) { - client().performRequest("PUT", "painless/test/" + time.toDateTimeISO() + "_" + j, - Collections.emptyMap(), - new StringEntity("{\"domain\": \"" + "bar.bar.com\", \"time\": \"" + time.toDateTimeISO() - + "\"}", ContentType.APPLICATION_JSON)); + Request createDocRequest = new Request("PUT", "/painless/test/" + time.toDateTimeISO() + "_" + j); + createDocRequest.setJsonEntity("{\"domain\": \"" + "bar.bar.com\", \"time\": \"" + time.toDateTimeISO() + "\"}"); + client().performRequest(createDocRequest); } } else { // Non-anomalous values will be what's seen when the anomaly is reported - client().performRequest("PUT", "painless/test/" + time.toDateTimeISO(), - Collections.emptyMap(), - new StringEntity("{\"domain\": \"" + test.hostName + "\", \"time\": \"" + time.toDateTimeISO() - + "\"}", ContentType.APPLICATION_JSON)); + Request createDocRequest = new Request("PUT", "/painless/test/" + time.toDateTimeISO()); + createDocRequest.setJsonEntity("{\"domain\": \"" + test.hostName + "\", \"time\": \"" + time.toDateTimeISO() + "\"}"); + client().performRequest(createDocRequest); } } - client().performRequest("POST", "painless/_refresh"); + client().performRequest(new Request("POST", "/painless/_refresh")); // Create and start datafeed - String body = "{\n" + - " \"job_id\":\"hrd-split-job\",\n" + - " \"indexes\":[\"painless\"],\n" + - " \"types\":[\"test\"],\n" + - " \"script_fields\": {\n" + - " \"domain_split\": {\n" + - " \"script\": \"return domainSplit(doc['domain'].value, params);\"\n" + - " }\n" + - " }\n" + - " }"; - - client().performRequest("PUT", MachineLearning.BASE_PATH + "datafeeds/hrd-split-datafeed", Collections.emptyMap(), - new StringEntity(body, ContentType.APPLICATION_JSON)); - client().performRequest("POST", MachineLearning.BASE_PATH + "datafeeds/hrd-split-datafeed/_start"); + Request createFeedRequest = new Request("PUT", MachineLearning.BASE_PATH + "datafeeds/hrd-split-datafeed"); + createFeedRequest.setJsonEntity( + "{\n" + + " \"job_id\":\"hrd-split-job\",\n" + + " \"indexes\":[\"painless\"],\n" + + " \"types\":[\"test\"],\n" + + " \"script_fields\": {\n" + + " \"domain_split\": {\n" + + " \"script\": \"return domainSplit(doc['domain'].value, params);\"\n" + + " }\n" + + " }\n" + + "}"); + + client().performRequest(createFeedRequest); + client().performRequest(new Request("POST", MachineLearning.BASE_PATH + "datafeeds/hrd-split-datafeed/_start")); boolean passed = awaitBusy(() -> { try { - client().performRequest("POST", "/_refresh"); + client().performRequest(new Request("POST", "/_refresh")); - Response response = client().performRequest("GET", - MachineLearning.BASE_PATH + "anomaly_detectors/hrd-split-job/results/records"); + Response response = client().performRequest(new Request("GET", + MachineLearning.BASE_PATH + "anomaly_detectors/hrd-split-job/results/records")); String responseBody = EntityUtils.toString(response.getEntity()); if (responseBody.contains("\"count\":2")) { From dd4a8dc444f0fba20f486e1f9eed1bcb327e85b7 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 24 Aug 2018 16:37:25 -0400 Subject: [PATCH 150/283] Switch remaining tests to new style Requests (#33109) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. This changes all calls in the `client` and `distribution` projects to use the new versions. --- .../elasticsearch/client/StoredScriptsIT.java | 22 +++++-------------- .../StoredScriptsDocumentationIT.java | 12 ++++------ .../rest/WaitForRefreshAndCloseTests.java | 2 +- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/StoredScriptsIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/StoredScriptsIT.java index 14734c4ab60a..1d693eee8396 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/StoredScriptsIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/StoredScriptsIT.java @@ -17,9 +17,6 @@ * under the License. */ - -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.admin.cluster.storedscripts.DeleteStoredScriptRequest; @@ -35,7 +32,6 @@ import java.util.Collections; -import static java.util.Collections.emptyMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; @@ -52,12 +48,9 @@ public void testGetStoredScript() throws Exception { final String script = Strings.toString(scriptSource.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)); // TODO: change to HighLevel PutStoredScriptRequest when it will be ready // so far - using low-level REST API - Response putResponse = - adminClient() - .performRequest("PUT", "/_scripts/calculate-score", emptyMap(), - new StringEntity("{\"script\":" + script + "}", - ContentType.APPLICATION_JSON)); - assertEquals(putResponse.getStatusLine().getReasonPhrase(), 200, putResponse.getStatusLine().getStatusCode()); + Request putRequest = new Request("PUT", "/_scripts/calculate-score"); + putRequest.setJsonEntity("{\"script\":" + script + "}"); + Response putResponse = adminClient().performRequest(putRequest); assertEquals("{\"acknowledged\":true}", EntityUtils.toString(putResponse.getEntity())); GetStoredScriptRequest getRequest = new GetStoredScriptRequest("calculate-score"); @@ -78,12 +71,9 @@ public void testDeleteStoredScript() throws Exception { final String script = Strings.toString(scriptSource.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)); // TODO: change to HighLevel PutStoredScriptRequest when it will be ready // so far - using low-level REST API - Response putResponse = - adminClient() - .performRequest("PUT", "/_scripts/" + id, emptyMap(), - new StringEntity("{\"script\":" + script + "}", - ContentType.APPLICATION_JSON)); - assertEquals(putResponse.getStatusLine().getReasonPhrase(), 200, putResponse.getStatusLine().getStatusCode()); + Request putRequest = new Request("PUT", "/_scripts/" + id); + putRequest.setJsonEntity("{\"script\":" + script + "}"); + Response putResponse = adminClient().performRequest(putRequest); assertEquals("{\"acknowledged\":true}", EntityUtils.toString(putResponse.getEntity())); DeleteStoredScriptRequest deleteRequest = new DeleteStoredScriptRequest(id); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/StoredScriptsDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/StoredScriptsDocumentationIT.java index b1374ca85b6a..fc38090ef5b5 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/StoredScriptsDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/StoredScriptsDocumentationIT.java @@ -17,8 +17,6 @@ * under the License. */ -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; @@ -27,6 +25,7 @@ import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ESRestHighLevelClientTestCase; +import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; @@ -43,7 +42,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static java.util.Collections.emptyMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; @@ -193,11 +191,9 @@ private void putStoredScript(String id, StoredScriptSource scriptSource) throws final String script = Strings.toString(scriptSource.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)); // TODO: change to HighLevel PutStoredScriptRequest when it will be ready // so far - using low-level REST API - Response putResponse = - adminClient() - .performRequest("PUT", "/_scripts/" + id, emptyMap(), - new StringEntity("{\"script\":" + script + "}", - ContentType.APPLICATION_JSON)); + Request request = new Request("PUT", "/_scripts/" + id); + request.setJsonEntity("{\"script\":" + script + "}"); + Response putResponse = adminClient().performRequest(request); assertEquals(putResponse.getStatusLine().getReasonPhrase(), 200, putResponse.getStatusLine().getStatusCode()); assertEquals("{\"acknowledged\":true}", EntityUtils.toString(putResponse.getEntity())); } diff --git a/distribution/archives/integ-test-zip/src/test/java/org/elasticsearch/test/rest/WaitForRefreshAndCloseTests.java b/distribution/archives/integ-test-zip/src/test/java/org/elasticsearch/test/rest/WaitForRefreshAndCloseTests.java index fab809a51bcc..756d26745b2c 100644 --- a/distribution/archives/integ-test-zip/src/test/java/org/elasticsearch/test/rest/WaitForRefreshAndCloseTests.java +++ b/distribution/archives/integ-test-zip/src/test/java/org/elasticsearch/test/rest/WaitForRefreshAndCloseTests.java @@ -53,7 +53,7 @@ public void setupIndex() throws IOException { @After public void cleanupIndex() throws IOException { - client().performRequest("DELETE", indexName()); + client().performRequest(new Request("DELETE", indexName())); } private String indexName() { From 1e9144d8e6358d1aff5fa377206c66520424022c Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 24 Aug 2018 16:39:08 -0400 Subject: [PATCH 151/283] Switch remaining x-pack tests to new style Requests (#33108) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. This changes all calls in the `x-pack/qa/saml-idp-tests` and `x-pack/qa/security-setup-password-tests` projects to use the new versions. --- .../monitoring/integration/MonitoringIT.java | 6 +- .../authc/saml/SamlAuthenticationIT.java | 76 +++++++++---------- .../esnative/tool/SetupPasswordToolIT.java | 14 ++-- 3 files changed, 50 insertions(+), 46 deletions(-) diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java index e44d6da073ef..6669c796018e 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/integration/MonitoringIT.java @@ -120,8 +120,10 @@ public void testMonitoringBulk() throws Exception { // REST is the realistic way that these operations happen, so it's the most realistic way to integration test it too // Use Monitoring Bulk API to index 3 documents - //final Response bulkResponse = getRestClient().performRequest("POST", "/_xpack/monitoring/_bulk", - // parameters, createBulkEntity()); + //final Request bulkRequest = new Request("POST", "/_xpack/monitoring/_bulk"); + //< + //bulkRequest.setJsonEntity(createBulkEntity()); + //final Response bulkResponse = getRestClient().performRequest(request); final MonitoringBulkResponse bulkResponse = new MonitoringBulkRequestBuilder(client()) diff --git a/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java b/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java index bf4ad79c59d4..031ee20ba0c7 100644 --- a/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java +++ b/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java @@ -24,8 +24,6 @@ import org.apache.http.cookie.Cookie; import org.apache.http.cookie.CookieOrigin; import org.apache.http.cookie.MalformedCookieException; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.cookie.DefaultCookieSpec; @@ -39,6 +37,8 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cli.SuppressForbidden; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Strings; @@ -85,7 +85,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static java.util.Collections.emptyMap; import static org.elasticsearch.common.xcontent.XContentHelper.convertToMap; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.hamcrest.Matchers.contains; @@ -176,9 +175,9 @@ protected Settings restAdminSettings() { */ @Before public void setKibanaPassword() throws IOException { - final HttpEntity json = new StringEntity("{ \"password\" : \"" + KIBANA_PASSWORD + "\" }", ContentType.APPLICATION_JSON); - final Response response = adminClient().performRequest("PUT", "/_xpack/security/user/kibana/_password", emptyMap(), json); - assertOK(response); + Request request = new Request("PUT", "/_xpack/security/user/kibana/_password"); + request.setJsonEntity("{ \"password\" : \"" + KIBANA_PASSWORD + "\" }"); + adminClient().performRequest(request); } /** @@ -188,21 +187,19 @@ public void setKibanaPassword() throws IOException { */ @Before public void setupRoleMapping() throws IOException { - final StringEntity json = new StringEntity(Strings // top-level - .toString(XContentBuilder.builder(XContentType.JSON.xContent()) - .startObject() - .array("roles", new String[] { "kibana_user"} ) - .field("enabled", true) - .startObject("rules") + Request request = new Request("PUT", "/_xpack/security/role_mapping/thor-kibana"); + request.setJsonEntity(Strings.toString(XContentBuilder.builder(XContentType.JSON.xContent()) + .startObject() + .array("roles", new String[] { "kibana_user"} ) + .field("enabled", true) + .startObject("rules") .startArray("all") - .startObject().startObject("field").field("username", "thor").endObject().endObject() - .startObject().startObject("field").field("realm.name", "shibboleth").endObject().endObject() + .startObject().startObject("field").field("username", "thor").endObject().endObject() + .startObject().startObject("field").field("realm.name", "shibboleth").endObject().endObject() .endArray() // "all" - .endObject() // "rules" - .endObject()), ContentType.APPLICATION_JSON); - - final Response response = adminClient().performRequest("PUT", "/_xpack/security/role_mapping/thor-kibana", emptyMap(), json); - assertOK(response); + .endObject() // "rules" + .endObject())); + adminClient().performRequest(request); } /** @@ -251,10 +248,11 @@ public void testLoginUser() throws Exception { * is for the expected user with the expected name and roles. */ private void verifyElasticsearchAccessToken(String accessToken) throws IOException { - final BasicHeader authorization = new BasicHeader("Authorization", "Bearer " + accessToken); - final Response response = client().performRequest("GET", "/_xpack/security/_authenticate", authorization); - assertOK(response); - final Map map = parseResponseAsMap(response.getEntity()); + Request request = new Request("GET", "/_xpack/security/_authenticate"); + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader("Authorization", "Bearer " + accessToken); + request.setOptions(options); + final Map map = entityAsMap(client().performRequest(request)); assertThat(map.get("username"), equalTo("thor")); assertThat(map.get("full_name"), equalTo("Thor Odinson")); assertSingletonList(map.get("roles"), "kibana_user"); @@ -272,12 +270,11 @@ private void verifyElasticsearchAccessToken(String accessToken) throws IOExcepti * can be used to get a new valid access token and refresh token. */ private void verifyElasticsearchRefreshToken(String refreshToken) throws IOException { - final String body = "{ \"grant_type\":\"refresh_token\", \"refresh_token\":\"" + refreshToken + "\" }"; - final Response response = client().performRequest("POST", "/_xpack/security/oauth2/token", - emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), kibanaAuth()); - assertOK(response); + Request request = new Request("POST", "/_xpack/security/oauth2/token"); + request.setJsonEntity("{ \"grant_type\":\"refresh_token\", \"refresh_token\":\"" + refreshToken + "\" }"); + kibanaAuth(request); - final Map result = parseResponseAsMap(response.getEntity()); + final Map result = entityAsMap(client().performRequest(request)); final Object newRefreshToken = result.get("refresh_token"); assertThat(newRefreshToken, notNullValue()); assertThat(newRefreshToken, instanceOf(String.class)); @@ -463,10 +460,10 @@ private String getUrl(String path) { * sends a redirect to that page. */ private void httpLogin(HttpExchange http) throws IOException { - final Response prepare = client().performRequest("POST", "/_xpack/security/saml/prepare", - emptyMap(), new StringEntity("{}", ContentType.APPLICATION_JSON), kibanaAuth()); - assertOK(prepare); - final Map body = parseResponseAsMap(prepare.getEntity()); + Request request = new Request("POST", "/_xpack/security/saml/prepare"); + request.setJsonEntity("{}"); + kibanaAuth(request); + final Map body = entityAsMap(client().performRequest(request)); logger.info("Created SAML authentication request {}", body); http.getResponseHeaders().add("Set-Cookie", REQUEST_ID_COOKIE + "=" + body.get("id")); http.getResponseHeaders().add("Location", (String) body.get("redirect")); @@ -504,9 +501,10 @@ private Response samlAuthenticate(HttpExchange http) throws IOException { final String id = getCookie(REQUEST_ID_COOKIE, http); assertThat(id, notNullValue()); - final String body = "{ \"content\" : \"" + saml + "\", \"ids\": [\"" + id + "\"] }"; - return client().performRequest("POST", "/_xpack/security/saml/authenticate", - emptyMap(), new StringEntity(body, ContentType.APPLICATION_JSON), kibanaAuth()); + Request request = new Request("POST", "/_xpack/security/saml/authenticate"); + request.setJsonEntity("{ \"content\" : \"" + saml + "\", \"ids\": [\"" + id + "\"] }"); + kibanaAuth(request); + return client().performRequest(request); } private List parseRequestForm(HttpExchange http) throws IOException { @@ -542,9 +540,11 @@ private static void assertSingletonList(Object value, String expectedElement) { assertThat(((List) value), contains(expectedElement)); } - private static BasicHeader kibanaAuth() { - final String auth = UsernamePasswordToken.basicAuthHeaderValue("kibana", new SecureString(KIBANA_PASSWORD.toCharArray())); - return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, auth); + private static void kibanaAuth(Request request) { + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader("Authorization", + UsernamePasswordToken.basicAuthHeaderValue("kibana", new SecureString(KIBANA_PASSWORD.toCharArray()))); + request.setOptions(options); } private CloseableHttpClient getHttpClient() throws Exception { diff --git a/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java b/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java index 74f1223f4a6a..7b5e0dc40d10 100644 --- a/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java +++ b/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java @@ -5,8 +5,9 @@ */ package org.elasticsearch.xpack.security.authc.esnative.tool; -import org.apache.http.message.BasicHeader; import org.elasticsearch.cli.MockTerminal; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.common.Strings; import org.elasticsearch.common.SuppressForbidden; @@ -52,7 +53,7 @@ public void testSetupPasswordToolAutoSetup() throws Exception { final Path configPath = PathUtils.get(testConfigDir); setSystemPropsForTool(configPath); - Response nodesResponse = client().performRequest("GET", "/_nodes/http"); + Response nodesResponse = client().performRequest(new Request("GET", "/_nodes/http")); Map nodesMap = entityAsMap(nodesResponse); Map nodes = (Map) nodesMap.get("nodes"); @@ -102,10 +103,11 @@ public void testSetupPasswordToolAutoSetup() throws Exception { final String basicHeader = "Basic " + Base64.getEncoder().encodeToString((entry.getKey() + ":" + entry.getValue()).getBytes(StandardCharsets.UTF_8)); try { - Response authenticateResponse = client().performRequest("GET", "/_xpack/security/_authenticate", - new BasicHeader("Authorization", basicHeader)); - assertEquals(200, authenticateResponse.getStatusLine().getStatusCode()); - Map userInfoMap = entityAsMap(authenticateResponse); + Request request = new Request("GET", "/_xpack/security/_authenticate"); + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader("Authorization", basicHeader); + request.setOptions(options); + Map userInfoMap = entityAsMap(client().performRequest(request)); assertEquals(entry.getKey(), userInfoMap.get("username")); } catch (IOException e) { throw new UncheckedIOException(e); From 739a8d3d44e0f535491a53f13ab4a26a518dd73c Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 24 Aug 2018 20:25:13 -0400 Subject: [PATCH 152/283] TEST: resync operation on replica should acquire shard permit (#33103) This change makes sure that resync operations on replicas in the test framework are executed under shard permits as the production code. --- .../ESIndexLevelReplicationTestCase.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 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 77bc644909ab..9b63f0d233e0 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 @@ -728,7 +728,7 @@ protected PrimaryResult performOnPrimary(IndexShard primary, ResyncReplicationRe @Override protected void performOnReplica(ResyncReplicationRequest request, IndexShard replica) throws Exception { - executeResyncOnReplica(replica, request); + executeResyncOnReplica(replica, request, getPrimaryShard().getPendingPrimaryTerm(), getPrimaryShard().getGlobalCheckpoint()); } } @@ -741,8 +741,15 @@ private TransportWriteAction.WritePrimaryResult acquirePermitFuture = new PlainActionFuture<>(); + replica.acquireReplicaOperationPermit( + operationPrimaryTerm, globalCheckpointOnPrimary, acquirePermitFuture, ThreadPool.Names.SAME, request); + try (Releasable ignored = acquirePermitFuture.actionGet()) { + location = TransportResyncReplicationAction.performOnReplica(request, replica); + } TransportWriteActionTestHelper.performPostWriteActions(replica, request, location, logger); } } From 9dad82ece8e13dd11b8301d41d4f0020c461050f Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 24 Aug 2018 21:02:13 -0400 Subject: [PATCH 153/283] TEST: Skip assertSeqNos for closed shards (#33130) If a shard was closed, we return null for SeqNoStats. Therefore the assertion assertSeqNos will hit NPE when it verifies a closed shard. This commit skips closed shards in assertSeqNos and enables this assertion in AbstractDisruptionTestCase. --- .../elasticsearch/discovery/AbstractDisruptionTestCase.java | 1 + .../src/main/java/org/elasticsearch/test/ESIntegTestCase.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java index 0f3288b1973e..6bdd8ea3f2e0 100644 --- a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java +++ b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java @@ -109,6 +109,7 @@ public void setDisruptionScheme(ServiceDisruptionScheme scheme) { protected void beforeIndexDeletion() throws Exception { if (disableBeforeIndexDeletion == false) { super.beforeIndexDeletion(); + assertSeqNos(); } } 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 3a479aad8977..1f51ad495e18 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -2349,6 +2349,9 @@ protected void assertSeqNos() throws Exception { final ObjectLongMap globalCheckpoints = indexShard.getInSyncGlobalCheckpoints(); for (ShardStats shardStats : indexShardStats) { final SeqNoStats seqNoStats = shardStats.getSeqNoStats(); + if (seqNoStats == null) { + continue; // this shard was closed + } assertThat(shardStats.getShardRouting() + " local checkpoint mismatch", seqNoStats.getLocalCheckpoint(), equalTo(primarySeqNoStats.getLocalCheckpoint())); assertThat(shardStats.getShardRouting() + " global checkpoint mismatch", From 3376922e8b1a587eb09c7f3d1016b0ee49cd4b9f Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Sat, 25 Aug 2018 20:41:32 +0200 Subject: [PATCH 154/283] Add proxy support to RemoteClusterConnection (#33062) This adds support for connecting to a remote cluster through a tcp proxy. A remote cluster can configured with an additional `search.remote.$clustername.proxy` setting. This proxy will be used to connect to remote nodes for every node connection established. We still try to sniff the remote clsuter and connect to nodes directly through the proxy which has to support some kind of routing to these nodes. Yet, this routing mechanism requires the handshake request to include some kind of information where to route to which is not yet implemented. The effort to use the hostname and an optional node attribute for routing is tracked in #32517 Closes #31840 --- .../common/settings/ClusterSettings.java | 1 + .../common/settings/Setting.java | 4 + .../transport/RemoteClusterAware.java | 61 +++++++-- .../transport/RemoteClusterConnection.java | 35 ++++- .../transport/RemoteClusterService.java | 41 +++--- .../RemoteClusterConnectionTests.java | 110 ++++++++++++++- .../transport/RemoteClusterServiceTests.java | 125 +++++++++++++++--- .../test/transport/MockTransportService.java | 9 +- .../test/transport/StubbableTransport.java | 8 +- .../authz/IndicesAndAliasesResolver.java | 4 +- 10 files changed, 334 insertions(+), 64 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index bf53a3dc01a7..237fc911db62 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -272,6 +272,7 @@ public void apply(Settings value, Settings current, Settings previous) { ElectMasterService.DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING, TransportSearchAction.SHARD_COUNT_LIMIT_SETTING, RemoteClusterAware.REMOTE_CLUSTERS_SEEDS, + RemoteClusterAware.REMOTE_CLUSTERS_PROXY, RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE, RemoteClusterService.REMOTE_CONNECTIONS_PER_CLUSTER, RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING, 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 b98c2753d701..7b432c0ed1e1 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -1009,6 +1009,10 @@ public static Setting simpleString(String key, Property... properties) { return new Setting<>(key, s -> "", Function.identity(), properties); } + public static Setting simpleString(String key, Function parser, Property... properties) { + return new Setting<>(key, s -> "", parser, properties); + } + public static Setting simpleString(String key, Setting fallback, Property... properties) { return new Setting<>(key, fallback, Function.identity(), properties); } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java index a12f27c93e3c..16d3c292bfe3 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterAware.java @@ -18,10 +18,14 @@ */ package org.elasticsearch.transport; +import java.util.EnumSet; import java.util.function.Supplier; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.ClusterNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; @@ -66,6 +70,22 @@ public abstract class RemoteClusterAware extends AbstractComponent { public static final char REMOTE_CLUSTER_INDEX_SEPARATOR = ':'; public static final String LOCAL_CLUSTER_GROUP_KEY = ""; + /** + * A proxy address for the remote cluster. + * NOTE: this settings is undocumented until we have at last one transport that supports passing + * on the hostname via a mechanism like SNI. + */ + public static final Setting.AffixSetting REMOTE_CLUSTERS_PROXY = Setting.affixKeySetting( + "search.remote.", + "proxy", + key -> Setting.simpleString(key, s -> { + if (Strings.hasLength(s)) { + parsePort(s); + } + return s; + }, Setting.Property.NodeScope, Setting.Property.Dynamic), REMOTE_CLUSTERS_SEEDS); + + protected final ClusterNameExpressionResolver clusterNameResolver; /** @@ -77,25 +97,42 @@ protected RemoteClusterAware(Settings settings) { this.clusterNameResolver = new ClusterNameExpressionResolver(settings); } - protected static Map>> buildRemoteClustersSeeds(Settings settings) { + /** + * Builds the dynamic per-cluster config from the given settings. This is a map keyed by the cluster alias that points to a tuple + * (ProxyAddresss, [SeedNodeSuppliers]). If a cluster is configured with a proxy address all seed nodes will point to + * {@link TransportAddress#META_ADDRESS} and their configured address will be used as the hostname for the generated discovery node. + */ + protected static Map>>> buildRemoteClustersDynamicConfig(Settings settings) { Stream>> allConcreteSettings = REMOTE_CLUSTERS_SEEDS.getAllConcreteSettings(settings); return allConcreteSettings.collect( Collectors.toMap(REMOTE_CLUSTERS_SEEDS::getNamespace, concreteSetting -> { String clusterName = REMOTE_CLUSTERS_SEEDS.getNamespace(concreteSetting); List addresses = concreteSetting.get(settings); + final boolean proxyMode = REMOTE_CLUSTERS_PROXY.getConcreteSettingForNamespace(clusterName).exists(settings); List> nodes = new ArrayList<>(addresses.size()); for (String address : addresses) { - nodes.add(() -> { - TransportAddress transportAddress = new TransportAddress(RemoteClusterAware.parseSeedAddress(address)); - return new DiscoveryNode(clusterName + "#" + transportAddress.toString(), - transportAddress, - Version.CURRENT.minimumCompatibilityVersion()); - }); + nodes.add(() -> buildSeedNode(clusterName, address, proxyMode)); } - return nodes; + return new Tuple<>(REMOTE_CLUSTERS_PROXY.getConcreteSettingForNamespace(clusterName).get(settings), nodes); })); } + static DiscoveryNode buildSeedNode(String clusterName, String address, boolean proxyMode) { + if (proxyMode) { + TransportAddress transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 0); + String hostName = address.substring(0, indexOfPortSeparator(address)); + return new DiscoveryNode("", clusterName + "#" + address, UUIDs.randomBase64UUID(), hostName, address, + transportAddress, Collections + .emptyMap(), EnumSet.allOf(DiscoveryNode.Role.class), + Version.CURRENT.minimumCompatibilityVersion()); + } else { + TransportAddress transportAddress = new TransportAddress(RemoteClusterAware.parseSeedAddress(address)); + return new DiscoveryNode(clusterName + "#" + transportAddress.toString(), + transportAddress, + Version.CURRENT.minimumCompatibilityVersion()); + } + } + /** * Groups indices per cluster by splitting remote cluster-alias, index-name pairs on {@link #REMOTE_CLUSTER_INDEX_SEPARATOR}. All * indices per cluster are collected as a list in the returned map keyed by the cluster alias. Local indices are grouped under @@ -138,20 +175,24 @@ public Map> groupClusterIndices(String[] requestIndices, Pr protected abstract Set getRemoteClusterNames(); + /** * Subclasses must implement this to receive information about updated cluster aliases. If the given address list is * empty the cluster alias is unregistered and should be removed. */ - protected abstract void updateRemoteCluster(String clusterAlias, List addresses); + protected abstract void updateRemoteCluster(String clusterAlias, List addresses, String proxy); /** * Registers this instance to listen to updates on the cluster settings. */ public void listenForUpdates(ClusterSettings clusterSettings) { - clusterSettings.addAffixUpdateConsumer(RemoteClusterAware.REMOTE_CLUSTERS_SEEDS, this::updateRemoteCluster, + clusterSettings.addAffixUpdateConsumer(RemoteClusterAware.REMOTE_CLUSTERS_PROXY, + RemoteClusterAware.REMOTE_CLUSTERS_SEEDS, + (key, value) -> updateRemoteCluster(key, value.v2(), value.v1()), (namespace, value) -> {}); } + protected static InetSocketAddress parseSeedAddress(String remoteHost) { String host = remoteHost.substring(0, indexOfPortSeparator(remoteHost)); InetAddress hostAddress; diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java index 5621b3855781..6b1909434655 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.transport; +import java.net.InetSocketAddress; import java.util.function.Supplier; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.store.AlreadyClosedException; @@ -88,6 +89,7 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo private final int maxNumRemoteConnections; private final Predicate nodePredicate; private final ThreadPool threadPool; + private volatile String proxyAddress; private volatile List> seedNodes; private volatile boolean skipUnavailable; private final ConnectHandler connectHandler; @@ -106,6 +108,13 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo RemoteClusterConnection(Settings settings, String clusterAlias, List> seedNodes, TransportService transportService, ConnectionManager connectionManager, int maxNumRemoteConnections, Predicate nodePredicate) { + this(settings, clusterAlias, seedNodes, transportService, connectionManager, maxNumRemoteConnections, nodePredicate, null); + } + + RemoteClusterConnection(Settings settings, String clusterAlias, List> seedNodes, + TransportService transportService, ConnectionManager connectionManager, int maxNumRemoteConnections, Predicate + nodePredicate, + String proxyAddress) { super(settings); this.transportService = transportService; this.maxNumRemoteConnections = maxNumRemoteConnections; @@ -130,13 +139,26 @@ final class RemoteClusterConnection extends AbstractComponent implements Transpo connectionManager.addListener(this); // we register the transport service here as a listener to make sure we notify handlers on disconnect etc. connectionManager.addListener(transportService); + this.proxyAddress = proxyAddress; + } + + private static DiscoveryNode maybeAddProxyAddress(String proxyAddress, DiscoveryNode node) { + if (proxyAddress == null || proxyAddress.isEmpty()) { + return node; + } else { + // resovle proxy address lazy here + InetSocketAddress proxyInetAddress = RemoteClusterAware.parseSeedAddress(proxyAddress); + return new DiscoveryNode(node.getName(), node.getId(), node.getEphemeralId(), node.getHostName(), node + .getHostAddress(), new TransportAddress(proxyInetAddress), node.getAttributes(), node.getRoles(), node.getVersion()); + } } /** * Updates the list of seed nodes for this cluster connection */ - synchronized void updateSeedNodes(List> seedNodes, ActionListener connectListener) { + synchronized void updateSeedNodes(String proxyAddress, List> seedNodes, ActionListener connectListener) { this.seedNodes = Collections.unmodifiableList(new ArrayList<>(seedNodes)); + this.proxyAddress = proxyAddress; connectHandler.connect(connectListener); } @@ -281,6 +303,7 @@ Transport.Connection getConnection(DiscoveryNode remoteClusterNode) { return new ProxyConnection(connection, remoteClusterNode); } + static final class ProxyConnection implements Transport.Connection { private final Transport.Connection proxyConnection; private final DiscoveryNode targetNode; @@ -461,7 +484,7 @@ private void collectRemoteNodes(Iterator> seedNodes, try { if (seedNodes.hasNext()) { cancellableThreads.executeIO(() -> { - final DiscoveryNode seedNode = seedNodes.next().get(); + final DiscoveryNode seedNode = maybeAddProxyAddress(proxyAddress, seedNodes.next().get()); final TransportService.HandshakeResponse handshakeResponse; Transport.Connection connection = manager.openConnection(seedNode, ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, null, null)); @@ -476,7 +499,7 @@ private void collectRemoteNodes(Iterator> seedNodes, throw ex; } - final DiscoveryNode handshakeNode = handshakeResponse.getDiscoveryNode(); + final DiscoveryNode handshakeNode = maybeAddProxyAddress(proxyAddress, handshakeResponse.getDiscoveryNode()); if (nodePredicate.test(handshakeNode) && connectedNodes.size() < maxNumRemoteConnections) { manager.connectToNode(handshakeNode, remoteProfile, transportService.connectionValidator(handshakeNode)); if (remoteClusterName.get() == null) { @@ -583,7 +606,8 @@ public void handleResponse(ClusterStateResponse response) { cancellableThreads.executeIO(() -> { DiscoveryNodes nodes = response.getState().nodes(); Iterable nodesIter = nodes.getNodes()::valuesIt; - for (DiscoveryNode node : nodesIter) { + for (DiscoveryNode n : nodesIter) { + DiscoveryNode node = maybeAddProxyAddress(proxyAddress, n); if (nodePredicate.test(node) && connectedNodes.size() < maxNumRemoteConnections) { try { connectionManager.connectToNode(node, remoteProfile, @@ -646,7 +670,8 @@ void addConnectedNode(DiscoveryNode node) { * Get the information about remote nodes to be rendered on {@code _remote/info} requests. */ public RemoteConnectionInfo getConnectionInfo() { - List seedNodeAddresses = seedNodes.stream().map(node -> node.get().getAddress()).collect(Collectors.toList()); + List seedNodeAddresses = seedNodes.stream().map(node -> node.get().getAddress()).collect + (Collectors.toList()); TimeValue initialConnectionTimeout = RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings); return new RemoteConnectionInfo(clusterAlias, seedNodeAddresses, maxNumRemoteConnections, connectedNodes.size(), initialConnectionTimeout, skipUnavailable); diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index 34f13b672874..60126847cbea 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -31,10 +31,10 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Booleans; 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.Settings; -import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.core.internal.io.IOUtils; @@ -116,8 +116,8 @@ public final class RemoteClusterService extends RemoteClusterAware implements Cl * @param seeds a cluster alias to discovery node mapping representing the remote clusters seeds nodes * @param connectionListener a listener invoked once every configured cluster has been connected to */ - private synchronized void updateRemoteClusters(Map>> seeds, - ActionListener connectionListener) { + private synchronized void updateRemoteClusters(Map>>> seeds, + ActionListener connectionListener) { if (seeds.containsKey(LOCAL_CLUSTER_GROUP_KEY)) { throw new IllegalArgumentException("remote clusters must not have the empty string as its key"); } @@ -127,9 +127,12 @@ private synchronized void updateRemoteClusters(Map>> entry : seeds.entrySet()) { + for (Map.Entry>>> entry : seeds.entrySet()) { + List> seedList = entry.getValue().v2(); + String proxyAddress = entry.getValue().v1(); + RemoteClusterConnection remote = this.remoteClusters.get(entry.getKey()); - if (entry.getValue().isEmpty()) { // with no seed nodes we just remove the connection + if (seedList.isEmpty()) { // with no seed nodes we just remove the connection try { IOUtils.close(remote); } catch (IOException e) { @@ -140,15 +143,15 @@ private synchronized void updateRemoteClusters(Map { if (countDown.countDown()) { connectionListener.onResponse(response); @@ -302,8 +305,7 @@ protected Set getRemoteClusterNames() { @Override public void listenForUpdates(ClusterSettings clusterSettings) { super.listenForUpdates(clusterSettings); - clusterSettings.addAffixUpdateConsumer(REMOTE_CLUSTER_SKIP_UNAVAILABLE, this::updateSkipUnavailable, - (clusterAlias, value) -> {}); + clusterSettings.addAffixUpdateConsumer(REMOTE_CLUSTER_SKIP_UNAVAILABLE, this::updateSkipUnavailable, (alias, value) -> {}); } synchronized void updateSkipUnavailable(String clusterAlias, Boolean skipUnavailable) { @@ -313,22 +315,21 @@ synchronized void updateSkipUnavailable(String clusterAlias, Boolean skipUnavail } } + @Override - protected void updateRemoteCluster(String clusterAlias, List addresses) { - updateRemoteCluster(clusterAlias, addresses, ActionListener.wrap((x) -> {}, (x) -> {})); + protected void updateRemoteCluster(String clusterAlias, List addresses, String proxyAddress) { + updateRemoteCluster(clusterAlias, addresses, proxyAddress, ActionListener.wrap((x) -> {}, (x) -> {})); } void updateRemoteCluster( final String clusterAlias, final List addresses, + final String proxyAddress, final ActionListener connectionListener) { - final List> nodes = addresses.stream().>map(address -> () -> { - final TransportAddress transportAddress = new TransportAddress(RemoteClusterAware.parseSeedAddress(address)); - final String id = clusterAlias + "#" + transportAddress.toString(); - final Version version = Version.CURRENT.minimumCompatibilityVersion(); - return new DiscoveryNode(id, transportAddress, version); - }).collect(Collectors.toList()); - updateRemoteClusters(Collections.singletonMap(clusterAlias, nodes), connectionListener); + final List> nodes = addresses.stream().>map(address -> () -> + buildSeedNode(clusterAlias, address, Strings.hasLength(proxyAddress)) + ).collect(Collectors.toList()); + updateRemoteClusters(Collections.singletonMap(clusterAlias, new Tuple<>(proxyAddress, nodes)), connectionListener); } /** @@ -338,7 +339,7 @@ void updateRemoteCluster( void initializeRemoteClusters() { final TimeValue timeValue = REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings); final PlainActionFuture future = new PlainActionFuture<>(); - Map>> seeds = RemoteClusterAware.buildRemoteClustersSeeds(settings); + Map>>> seeds = RemoteClusterAware.buildRemoteClustersDynamicConfig(settings); updateRemoteClusters(seeds, future); try { future.get(timeValue.millis(), TimeUnit.MILLISECONDS); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index e40486d63dc4..88b01c66898a 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -18,6 +18,8 @@ */ package org.elasticsearch.transport; +import java.util.HashMap; +import java.util.Map; import java.util.function.Supplier; import org.apache.lucene.store.AlreadyClosedException; import org.elasticsearch.Version; @@ -52,6 +54,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.test.transport.StubbableTransport; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; @@ -378,15 +381,19 @@ public void testFilterDiscoveredNodes() throws Exception { } } } - private void updateSeedNodes(RemoteClusterConnection connection, List> seedNodes) throws Exception { + updateSeedNodes(connection, seedNodes, null); + } + + private void updateSeedNodes(RemoteClusterConnection connection, List> seedNodes, String proxyAddress) + throws Exception { CountDownLatch latch = new CountDownLatch(1); AtomicReference exceptionAtomicReference = new AtomicReference<>(); ActionListener listener = ActionListener.wrap(x -> latch.countDown(), x -> { exceptionAtomicReference.set(x); latch.countDown(); }); - connection.updateSeedNodes(seedNodes, listener); + connection.updateSeedNodes(proxyAddress, seedNodes, listener); latch.await(); if (exceptionAtomicReference.get() != null) { throw exceptionAtomicReference.get(); @@ -517,7 +524,7 @@ public void run() { exceptionReference.set(x); listenerCalled.countDown(); }); - connection.updateSeedNodes(Arrays.asList(() -> seedNode), listener); + connection.updateSeedNodes(null, Arrays.asList(() -> seedNode), listener); acceptedLatch.await(); connection.close(); // now close it, this should trigger an interrupt on the socket and we can move on assertTrue(connection.assertNoRunningConnections()); @@ -787,7 +794,7 @@ public void run() { throw new AssertionError(x); } }); - connection.updateSeedNodes(seedNodes, listener); + connection.updateSeedNodes(null, seedNodes, listener); } latch.await(); } catch (Exception ex) { @@ -875,7 +882,7 @@ public void run() { } }); try { - connection.updateSeedNodes(seedNodes, listener); + connection.updateSeedNodes(null, seedNodes, listener); } catch (Exception e) { // it's ok if we're shutting down assertThat(e.getMessage(), containsString("threadcontext is already closed")); @@ -1384,4 +1391,97 @@ public void testLazyResolveTransportAddress() throws Exception { } } } + + public void testProxyMode() throws Exception { + List knownNodes = new CopyOnWriteArrayList<>(); + try (MockTransportService seedTransport = startTransport("node_0", knownNodes, Version.CURRENT); + MockTransportService discoverableTransport = startTransport("node_1", knownNodes, Version.CURRENT)) { + knownNodes.add(seedTransport.getLocalDiscoNode()); + knownNodes.add(discoverableTransport.getLocalDiscoNode()); + Collections.shuffle(knownNodes, random()); + final String proxyAddress = "1.1.1.1:99"; + Map nodes = new HashMap<>(); + nodes.put("node_0", seedTransport.getLocalDiscoNode()); + nodes.put("node_1", discoverableTransport.getLocalDiscoNode()); + Transport mockTcpTransport = getProxyTransport(threadPool, Collections.singletonMap(proxyAddress, nodes)); + try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, mockTcpTransport, Version.CURRENT, + threadPool, null, Collections.emptySet())) { + service.start(); + service.acceptIncomingRequests(); + Supplier seedSupplier = () -> + RemoteClusterAware.buildSeedNode("some-remote-cluster", "node_0:" + randomIntBetween(1, 10000), true); + try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", + Arrays.asList(seedSupplier), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, proxyAddress)) { + updateSeedNodes(connection, Arrays.asList(seedSupplier), proxyAddress); + assertEquals(2, connection.getNumNodesConnected()); + assertNotNull(connection.getConnection(discoverableTransport.getLocalDiscoNode())); + assertNotNull(connection.getConnection(seedTransport.getLocalDiscoNode())); + assertEquals(proxyAddress, connection.getConnection(seedTransport.getLocalDiscoNode()) + .getNode().getAddress().toString()); + assertEquals(proxyAddress, connection.getConnection(discoverableTransport.getLocalDiscoNode()) + .getNode().getAddress().toString()); + service.getConnectionManager().disconnectFromNode(knownNodes.get(0)); + // ensure we reconnect + assertBusy(() -> { + assertEquals(2, connection.getNumNodesConnected()); + }); + discoverableTransport.close(); + seedTransport.close(); + } + } + } + } + + public static Transport getProxyTransport(ThreadPool threadPool, Map> nodeMap) { + if (nodeMap.isEmpty()) { + throw new IllegalArgumentException("nodeMap must be non-empty"); + } + + StubbableTransport stubbableTransport = new StubbableTransport(MockTransportService.newMockTransport(Settings.EMPTY, Version + .CURRENT, threadPool)); + stubbableTransport.setDefaultConnectBehavior((t, node, profile) -> { + Map proxyMapping = nodeMap.get(node.getAddress().toString()); + if (proxyMapping == null) { + throw new IllegalStateException("no proxy mapping for node: " + node); + } + DiscoveryNode proxyNode = proxyMapping.get(node.getName()); + if (proxyNode == null) { + // this is a seednode - lets pick one randomly + assertEquals("seed node must not have a port in the hostname: " + node.getHostName(), + -1, node.getHostName().lastIndexOf(':')); + assertTrue("missing hostname: " + node, proxyMapping.containsKey(node.getHostName())); + // route by seed hostname + proxyNode = proxyMapping.get(node.getHostName()); + } + Transport.Connection connection = t.openConnection(proxyNode, profile); + return new Transport.Connection() { + @Override + public DiscoveryNode getNode() { + return node; + } + + @Override + public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) + throws IOException, TransportException { + connection.sendRequest(requestId, action, request, options); + } + + @Override + public void addCloseListener(ActionListener listener) { + connection.addCloseListener(listener); + } + + @Override + public boolean isClosed() { + return connection.isClosed(); + } + + @Override + public void close() { + connection.close(); + } + }; + }); + return stubbableTransport; + } } diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java index 84a6ce54d1ed..9d42b4e458db 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsResponse; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.AbstractScopedSettings; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; @@ -55,6 +56,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Predicate; +import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.instanceOf; @@ -115,25 +117,38 @@ public void testRemoteClusterSeedSetting() { assertEquals("failed to parse port", e.getMessage()); } - public void testBuiltRemoteClustersSeeds() throws Exception { - Map>> map = RemoteClusterService.buildRemoteClustersSeeds( - Settings.builder().put("search.remote.foo.seeds", "192.168.0.1:8080").put("search.remote.bar.seeds", "[::1]:9090").build()); - assertEquals(2, map.size()); + public void testBuildRemoteClustersDynamicConfig() throws Exception { + Map>>> map = RemoteClusterService.buildRemoteClustersDynamicConfig( + Settings.builder().put("search.remote.foo.seeds", "192.168.0.1:8080") + .put("search.remote.bar.seeds", "[::1]:9090") + .put("search.remote.boom.seeds", "boom-node1.internal:1000") + .put("search.remote.boom.proxy", "foo.bar.com:1234").build()); + assertEquals(3, map.size()); assertTrue(map.containsKey("foo")); assertTrue(map.containsKey("bar")); - assertEquals(1, map.get("foo").size()); - assertEquals(1, map.get("bar").size()); - - DiscoveryNode foo = map.get("foo").get(0).get(); + assertTrue(map.containsKey("boom")); + assertEquals(1, map.get("foo").v2().size()); + assertEquals(1, map.get("bar").v2().size()); + assertEquals(1, map.get("boom").v2().size()); + DiscoveryNode foo = map.get("foo").v2().get(0).get(); + assertEquals("", map.get("foo").v1()); assertEquals(foo.getAddress(), new TransportAddress(new InetSocketAddress(InetAddress.getByName("192.168.0.1"), 8080))); assertEquals(foo.getId(), "foo#192.168.0.1:8080"); assertEquals(foo.getVersion(), Version.CURRENT.minimumCompatibilityVersion()); - DiscoveryNode bar = map.get("bar").get(0).get(); + DiscoveryNode bar = map.get("bar").v2().get(0).get(); assertEquals(bar.getAddress(), new TransportAddress(new InetSocketAddress(InetAddress.getByName("[::1]"), 9090))); assertEquals(bar.getId(), "bar#[::1]:9090"); + assertEquals("", map.get("bar").v1()); assertEquals(bar.getVersion(), Version.CURRENT.minimumCompatibilityVersion()); + + DiscoveryNode boom = map.get("boom").v2().get(0).get(); + assertEquals(boom.getAddress(), new TransportAddress(TransportAddress.META_ADDRESS, 0)); + assertEquals("boom-node1.internal", boom.getHostName()); + assertEquals(boom.getId(), "boom#boom-node1.internal:1000"); + assertEquals("foo.bar.com:1234", map.get("boom").v1()); + assertEquals(boom.getVersion(), Version.CURRENT.minimumCompatibilityVersion()); } @@ -204,17 +219,17 @@ public void testIncrementallyAddClusters() throws IOException { assertFalse(service.isCrossClusterSearchEnabled()); service.initializeRemoteClusters(); assertFalse(service.isCrossClusterSearchEnabled()); - service.updateRemoteCluster("cluster_1", Collections.singletonList(seedNode.getAddress().toString())); + service.updateRemoteCluster("cluster_1", Collections.singletonList(seedNode.getAddress().toString()), null); assertTrue(service.isCrossClusterSearchEnabled()); assertTrue(service.isRemoteClusterRegistered("cluster_1")); - service.updateRemoteCluster("cluster_2", Collections.singletonList(otherSeedNode.getAddress().toString())); + service.updateRemoteCluster("cluster_2", Collections.singletonList(otherSeedNode.getAddress().toString()), null); assertTrue(service.isCrossClusterSearchEnabled()); assertTrue(service.isRemoteClusterRegistered("cluster_1")); assertTrue(service.isRemoteClusterRegistered("cluster_2")); - service.updateRemoteCluster("cluster_2", Collections.emptyList()); + service.updateRemoteCluster("cluster_2", Collections.emptyList(), null); assertFalse(service.isRemoteClusterRegistered("cluster_2")); IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, - () -> service.updateRemoteCluster(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY, Collections.emptyList())); + () -> service.updateRemoteCluster(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY, Collections.emptyList(), null)); assertEquals("remote clusters must not have the empty string as its key", iae.getMessage()); } } @@ -265,14 +280,14 @@ public void testRemoteNodeAttribute() throws IOException, InterruptedException { final CountDownLatch firstLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_1", - Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), + Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), null, connectionListener(firstLatch)); firstLatch.await(); final CountDownLatch secondLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_2", - Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), + Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), null, connectionListener(secondLatch)); secondLatch.await(); @@ -330,14 +345,14 @@ public void testRemoteNodeRoles() throws IOException, InterruptedException { final CountDownLatch firstLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_1", - Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), + Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), null, connectionListener(firstLatch)); firstLatch.await(); final CountDownLatch secondLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_2", - Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), + Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), null, connectionListener(secondLatch)); secondLatch.await(); @@ -403,14 +418,14 @@ public void testCollectNodes() throws InterruptedException, IOException { final CountDownLatch firstLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_1", - Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), + Arrays.asList(c1N1Node.getAddress().toString(), c1N2Node.getAddress().toString()), null, connectionListener(firstLatch)); firstLatch.await(); final CountDownLatch secondLatch = new CountDownLatch(1); service.updateRemoteCluster( "cluster_2", - Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), + Arrays.asList(c2N1Node.getAddress().toString(), c2N2Node.getAddress().toString()), null, connectionListener(secondLatch)); secondLatch.await(); CountDownLatch latch = new CountDownLatch(1); @@ -822,4 +837,76 @@ public void testGetNodePredicatesCombination() { assertTrue(nodePredicate.test(node)); } } + + public void testRemoteClusterWithProxy() throws Exception { + List knownNodes = new CopyOnWriteArrayList<>(); + try (MockTransportService cluster_1_node0 = startTransport("cluster_1_node0", knownNodes, Version.CURRENT); + MockTransportService cluster_1_node_1 = startTransport("cluster_1_node1", knownNodes, Version.CURRENT); + MockTransportService cluster_2_node0 = startTransport("cluster_2_node0", Collections.emptyList(), Version.CURRENT)) { + knownNodes.add(cluster_1_node0.getLocalDiscoNode()); + knownNodes.add(cluster_1_node_1.getLocalDiscoNode()); + String cluster1Proxy = "1.1.1.1:99"; + String cluster2Proxy = "2.2.2.2:99"; + Map nodesCluster1 = new HashMap<>(); + nodesCluster1.put("cluster_1_node0", cluster_1_node0.getLocalDiscoNode()); + nodesCluster1.put("cluster_1_node1", cluster_1_node_1.getLocalDiscoNode()); + Map> mapping = new HashMap<>(); + mapping.put(cluster1Proxy, nodesCluster1); + mapping.put(cluster2Proxy, Collections.singletonMap("cluster_2_node0", cluster_2_node0.getLocalDiscoNode())); + + Collections.shuffle(knownNodes, random()); + Transport proxyTransport = RemoteClusterConnectionTests.getProxyTransport(threadPool, mapping); + try (MockTransportService transportService = MockTransportService.createNewService(Settings.EMPTY, proxyTransport, + Version.CURRENT, threadPool, null, Collections.emptySet());) { + transportService.start(); + transportService.acceptIncomingRequests(); + Settings.Builder builder = Settings.builder(); + builder.putList("search.remote.cluster_1.seeds", "cluster_1_node0:8080"); + builder.put("search.remote.cluster_1.proxy", cluster1Proxy); + try (RemoteClusterService service = new RemoteClusterService(builder.build(), transportService)) { + assertFalse(service.isCrossClusterSearchEnabled()); + service.initializeRemoteClusters(); + assertTrue(service.isCrossClusterSearchEnabled()); + updateRemoteCluster(service, "cluster_1", Collections.singletonList("cluster_1_node1:8081"), cluster1Proxy); + assertTrue(service.isCrossClusterSearchEnabled()); + assertTrue(service.isRemoteClusterRegistered("cluster_1")); + assertFalse(service.isRemoteClusterRegistered("cluster_2")); + updateRemoteCluster(service, "cluster_2", Collections.singletonList("cluster_2_node0:9300"), cluster2Proxy); + assertTrue(service.isCrossClusterSearchEnabled()); + assertTrue(service.isRemoteClusterRegistered("cluster_1")); + assertTrue(service.isRemoteClusterRegistered("cluster_2")); + List infos = service.getRemoteConnectionInfos().collect(Collectors.toList()); + for (RemoteConnectionInfo info : infos) { + switch (info.clusterAlias) { + case "cluster_1": + assertEquals(2, info.numNodesConnected); + break; + case "cluster_2": + assertEquals(1, info.numNodesConnected); + break; + default: + fail("unknown cluster: " + info.clusterAlias); + } + } + service.updateRemoteCluster("cluster_2", Collections.emptyList(), randomBoolean() ? cluster2Proxy : null); + assertFalse(service.isRemoteClusterRegistered("cluster_2")); + } + } + } + } + + private void updateRemoteCluster(RemoteClusterService service, String clusterAlias, List addresses, String proxyAddress) + throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference exceptionAtomicReference = new AtomicReference<>(); + ActionListener listener = ActionListener.wrap(x -> latch.countDown(), x -> { + exceptionAtomicReference.set(x); + latch.countDown(); + }); + service.updateRemoteCluster(clusterAlias, addresses, proxyAddress, listener); + latch.await(); + if (exceptionAtomicReference.get() != null) { + throw exceptionAtomicReference.get(); + } + } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java b/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java index 15ab06d651e9..d6c4f30a885d 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java @@ -95,6 +95,12 @@ public List> getSettings() { public static MockTransportService createNewService(Settings settings, Version version, ThreadPool threadPool, @Nullable ClusterSettings clusterSettings) { + MockTcpTransport mockTcpTransport = newMockTransport(settings, version, threadPool); + return createNewService(settings, mockTcpTransport, version, threadPool, clusterSettings, + Collections.emptySet()); + } + + public static MockTcpTransport newMockTransport(Settings settings, Version version, ThreadPool threadPool) { // some tests use MockTransportService to do network based testing. Yet, we run tests in multiple JVMs that means // concurrent tests could claim port that another JVM just released and if that test tries to simulate a disconnect it might // be smart enough to re-connect depending on what is tested. To reduce the risk, since this is very hard to debug we use @@ -102,9 +108,8 @@ public static MockTransportService createNewService(Settings settings, Version v int basePort = 10300 + (JVM_ORDINAL * 100); // use a non-default port otherwise some cluster in this JVM might reuse a port settings = Settings.builder().put(TcpTransport.PORT.getKey(), basePort + "-" + (basePort + 100)).put(settings).build(); NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(ClusterModule.getNamedWriteables()); - final Transport transport = new MockTcpTransport(settings, threadPool, BigArrays.NON_RECYCLING_INSTANCE, + return new MockTcpTransport(settings, threadPool, BigArrays.NON_RECYCLING_INSTANCE, new NoneCircuitBreakerService(), namedWriteableRegistry, new NetworkService(Collections.emptyList()), version); - return createNewService(settings, transport, version, threadPool, clusterSettings, Collections.emptySet()); } public static MockTransportService createNewService(Settings settings, Transport transport, Version version, ThreadPool threadPool, diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java index 2e78f8a9a4f0..d35fe609c085 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java @@ -41,7 +41,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -public class StubbableTransport implements Transport { +public final class StubbableTransport implements Transport { private final ConcurrentHashMap sendBehaviors = new ConcurrentHashMap<>(); private final ConcurrentHashMap connectBehaviors = new ConcurrentHashMap<>(); @@ -60,6 +60,12 @@ boolean setDefaultSendBehavior(SendRequestBehavior sendBehavior) { return prior == null; } + public boolean setDefaultConnectBehavior(OpenConnectionBehavior openConnectionBehavior) { + OpenConnectionBehavior prior = this.defaultConnectBehavior; + this.defaultConnectBehavior = openConnectionBehavior; + return prior == null; + } + boolean addSendBehavior(TransportAddress transportAddress, SendRequestBehavior sendBehavior) { return sendBehaviors.put(transportAddress, sendBehavior) == null; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java index 3cf2034cc74b..34aed55bb290 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolver.java @@ -418,7 +418,7 @@ private static class RemoteClusterResolver extends RemoteClusterAware { private RemoteClusterResolver(Settings settings, ClusterSettings clusterSettings) { super(settings); - clusters = new CopyOnWriteArraySet<>(buildRemoteClustersSeeds(settings).keySet()); + clusters = new CopyOnWriteArraySet<>(buildRemoteClustersDynamicConfig(settings).keySet()); listenForUpdates(clusterSettings); } @@ -428,7 +428,7 @@ protected Set getRemoteClusterNames() { } @Override - protected void updateRemoteCluster(String clusterAlias, List addresses) { + protected void updateRemoteCluster(String clusterAlias, List addresses, String proxyAddress) { if (addresses.isEmpty()) { clusters.remove(clusterAlias); } else { From c567ec4a0fee228be7d3c45c6be5c140cdb83db9 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Sun, 26 Aug 2018 14:09:23 +0300 Subject: [PATCH 155/283] Refactor CachingUsernamePassword realm (#32646) Refactors the logic of authentication and lookup caching in `CachingUsernamePasswordRealm`. Nothing changed about the single-inflight-request or positive caching. --- .../support/CachingUsernamePasswordRealm.java | 223 +++++++++--------- 1 file changed, 109 insertions(+), 114 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java index bcdbc1e1dd30..ab559cebe54e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java @@ -5,11 +5,9 @@ */ package org.elasticsearch.xpack.security.authc.support; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; -import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ListenableFuture; @@ -30,7 +28,7 @@ public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm implements CachingRealm { - private final Cache>> cache; + private final Cache> cache; private final ThreadPool threadPool; final Hasher cacheHasher; @@ -38,9 +36,9 @@ protected CachingUsernamePasswordRealm(String type, RealmConfig config, ThreadPo super(type, config); cacheHasher = Hasher.resolve(CachingUsernamePasswordRealmSettings.CACHE_HASH_ALGO_SETTING.get(config.settings())); this.threadPool = threadPool; - TimeValue ttl = CachingUsernamePasswordRealmSettings.CACHE_TTL_SETTING.get(config.settings()); + final TimeValue ttl = CachingUsernamePasswordRealmSettings.CACHE_TTL_SETTING.get(config.settings()); if (ttl.getNanos() > 0) { - cache = CacheBuilder.>>builder() + cache = CacheBuilder.>builder() .setExpireAfterWrite(ttl) .setMaximumWeight(CachingUsernamePasswordRealmSettings.CACHE_MAX_USERS_SETTING.get(config.settings())) .build(); @@ -49,6 +47,7 @@ protected CachingUsernamePasswordRealm(String type, RealmConfig config, ThreadPo } } + @Override public final void expire(String username) { if (cache != null) { logger.trace("invalidating cache for user [{}] in realm [{}]", username, name()); @@ -56,6 +55,7 @@ public final void expire(String username) { } } + @Override public final void expireAll() { if (cache != null) { logger.trace("invalidating cache for all users in realm [{}]", name()); @@ -72,108 +72,84 @@ public final void expireAll() { */ @Override public final void authenticate(AuthenticationToken authToken, ActionListener listener) { - UsernamePasswordToken token = (UsernamePasswordToken) authToken; + final UsernamePasswordToken token = (UsernamePasswordToken) authToken; try { if (cache == null) { doAuthenticate(token, listener); } else { authenticateWithCache(token, listener); } - } catch (Exception e) { + } catch (final Exception e) { // each realm should handle exceptions, if we get one here it should be considered fatal listener.onFailure(e); } } + /** + * This validates the {@code token} while making sure there is only one inflight + * request to the authentication source. Only successful responses are cached + * and any subsequent requests, bearing the same password, will succeed + * without reaching to the authentication source. A different password in a + * subsequent request, however, will clear the cache and try to reach to + * the authentication source. + * + * @param token The authentication token + * @param listener to be called at completion + */ private void authenticateWithCache(UsernamePasswordToken token, ActionListener listener) { try { - final SetOnce authenticatedUser = new SetOnce<>(); - final AtomicBoolean createdAndStartedFuture = new AtomicBoolean(false); - final ListenableFuture> future = cache.computeIfAbsent(token.principal(), k -> { - final ListenableFuture> created = new ListenableFuture<>(); - if (createdAndStartedFuture.compareAndSet(false, true) == false) { - throw new IllegalStateException("something else already started this. how?"); - } - return created; + final AtomicBoolean authenticationInCache = new AtomicBoolean(true); + final ListenableFuture listenableCacheEntry = cache.computeIfAbsent(token.principal(), k -> { + authenticationInCache.set(false); + return new ListenableFuture<>(); }); - - if (createdAndStartedFuture.get()) { - doAuthenticate(token, ActionListener.wrap(result -> { - if (result.isAuthenticated()) { - final User user = result.getUser(); - authenticatedUser.set(user); - final UserWithHash userWithHash = new UserWithHash(user, token.credentials(), cacheHasher); - future.onResponse(new Tuple<>(result, userWithHash)); - } else { - future.onResponse(new Tuple<>(result, null)); - } - }, future::onFailure)); - } - - future.addListener(ActionListener.wrap(tuple -> { - if (tuple != null) { - final UserWithHash userWithHash = tuple.v2(); - final boolean performedAuthentication = createdAndStartedFuture.get() && userWithHash != null && - tuple.v2().user == authenticatedUser.get(); - handleResult(future, createdAndStartedFuture.get(), performedAuthentication, token, tuple, listener); - } else { - handleFailure(future, createdAndStartedFuture.get(), token, new IllegalStateException("unknown error authenticating"), - listener); - } - }, e -> handleFailure(future, createdAndStartedFuture.get(), token, e, listener)), - threadPool.executor(ThreadPool.Names.GENERIC)); - } catch (ExecutionException e) { - listener.onResponse(AuthenticationResult.unsuccessful("", e)); - } - } - - private void handleResult(ListenableFuture> future, boolean createdAndStartedFuture, - boolean performedAuthentication, UsernamePasswordToken token, - Tuple result, ActionListener listener) { - final AuthenticationResult authResult = result.v1(); - if (authResult == null) { - // this was from a lookup; clear and redo - cache.invalidate(token.principal(), future); - authenticateWithCache(token, listener); - } else if (authResult.isAuthenticated()) { - if (performedAuthentication) { - listener.onResponse(authResult); - } else { - UserWithHash userWithHash = result.v2(); - if (userWithHash.verify(token.credentials())) { - if (userWithHash.user.enabled()) { - User user = userWithHash.user; - logger.debug("realm [{}] authenticated user [{}], with roles [{}]", - name(), token.principal(), user.roles()); + if (authenticationInCache.get()) { + // there is a cached or an inflight authenticate request + listenableCacheEntry.addListener(ActionListener.wrap(authenticatedUserWithHash -> { + if (authenticatedUserWithHash != null && authenticatedUserWithHash.verify(token.credentials())) { + // cached credential hash matches the credential hash for this forestalled request + final User user = authenticatedUserWithHash.user; + logger.debug("realm [{}] authenticated user [{}], with roles [{}], from cache", name(), token.principal(), + user.roles()); listener.onResponse(AuthenticationResult.success(user)); } else { - // re-auth to see if user has been enabled - cache.invalidate(token.principal(), future); + // The inflight request has failed or its credential hash does not match the + // hash of the credential for this forestalled request. + // clear cache and try to reach the authentication source again because password + // might have changed there and the local cached hash got stale + cache.invalidate(token.principal(), listenableCacheEntry); authenticateWithCache(token, listener); } - } else { - // could be a password change? - cache.invalidate(token.principal(), future); + }, e -> { + // the inflight request failed, so try again, but first (always) make sure cache + // is cleared of the failed authentication + cache.invalidate(token.principal(), listenableCacheEntry); authenticateWithCache(token, listener); - } - } - } else { - cache.invalidate(token.principal(), future); - if (createdAndStartedFuture) { - listener.onResponse(authResult); + }), threadPool.executor(ThreadPool.Names.GENERIC)); } else { - authenticateWithCache(token, listener); + // attempt authentication against the authentication source + doAuthenticate(token, ActionListener.wrap(authResult -> { + if (authResult.isAuthenticated() && authResult.getUser().enabled()) { + // compute the credential hash of this successful authentication request + final UserWithHash userWithHash = new UserWithHash(authResult.getUser(), token.credentials(), cacheHasher); + // notify any forestalled request listeners; they will not reach to the + // authentication request and instead will use this hash for comparison + listenableCacheEntry.onResponse(userWithHash); + } else { + // notify any forestalled request listeners; they will retry the request + listenableCacheEntry.onResponse(null); + } + // notify the listener of the inflight authentication request; this request is not retried + listener.onResponse(authResult); + }, e -> { + // notify any staved off listeners; they will retry the request + listenableCacheEntry.onFailure(e); + // notify the listener of the inflight authentication request; this request is not retried + listener.onFailure(e); + })); } - } - } - - private void handleFailure(ListenableFuture> future, boolean createdAndStarted, - UsernamePasswordToken token, Exception e, ActionListener listener) { - cache.invalidate(token.principal(), future); - if (createdAndStarted) { + } catch (final ExecutionException e) { listener.onFailure(e); - } else { - authenticateWithCache(token, listener); } } @@ -193,38 +169,57 @@ protected int getCacheSize() { @Override public final void lookupUser(String username, ActionListener listener) { - if (cache != null) { - try { - ListenableFuture> future = cache.computeIfAbsent(username, key -> { - ListenableFuture> created = new ListenableFuture<>(); - doLookupUser(username, ActionListener.wrap(user -> { - if (user != null) { - UserWithHash userWithHash = new UserWithHash(user, null, null); - created.onResponse(new Tuple<>(null, userWithHash)); - } else { - created.onResponse(new Tuple<>(null, null)); - } - }, created::onFailure)); - return created; - }); - - future.addListener(ActionListener.wrap(tuple -> { - if (tuple != null) { - if (tuple.v2() == null) { - cache.invalidate(username, future); - listener.onResponse(null); - } else { - listener.onResponse(tuple.v2().user); - } + try { + if (cache == null) { + doLookupUser(username, listener); + } else { + lookupWithCache(username, listener); + } + } catch (final Exception e) { + // each realm should handle exceptions, if we get one here it should be + // considered fatal + listener.onFailure(e); + } + } + + private void lookupWithCache(String username, ActionListener listener) { + try { + final AtomicBoolean lookupInCache = new AtomicBoolean(true); + final ListenableFuture listenableCacheEntry = cache.computeIfAbsent(username, key -> { + lookupInCache.set(false); + return new ListenableFuture<>(); + }); + if (false == lookupInCache.get()) { + // attempt lookup against the user directory + doLookupUser(username, ActionListener.wrap(user -> { + if (user != null) { + // user found + final UserWithHash userWithHash = new UserWithHash(user, null, null); + // notify forestalled request listeners + listenableCacheEntry.onResponse(userWithHash); } else { - listener.onResponse(null); + // user not found, invalidate cache so that subsequent requests are forwarded to + // the user directory + cache.invalidate(username, listenableCacheEntry); + // notify forestalled request listeners + listenableCacheEntry.onResponse(null); } - }, listener::onFailure), threadPool.executor(ThreadPool.Names.GENERIC)); - } catch (ExecutionException e) { - listener.onFailure(e); + }, e -> { + // the next request should be forwarded, not halted by a failed lookup attempt + cache.invalidate(username, listenableCacheEntry); + // notify forestalled listeners + listenableCacheEntry.onFailure(e); + })); } - } else { - doLookupUser(username, listener); + listenableCacheEntry.addListener(ActionListener.wrap(userWithHash -> { + if (userWithHash != null) { + listener.onResponse(userWithHash.user); + } else { + listener.onResponse(null); + } + }, listener::onFailure), threadPool.executor(ThreadPool.Names.GENERIC)); + } catch (final ExecutionException e) { + listener.onFailure(e); } } From fbe609d589fe57b1f822b2e5c77718af6e2faf53 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Sun, 26 Aug 2018 14:49:32 +0300 Subject: [PATCH 156/283] Reload Secure Settings REST specs & docs (#32990) This is a minimal REST API spec and docs for the REST handler for the `_nodes/reload_secure_settings endpoint`. Relates #29135 --- .../client/RestHighLevelClientTests.java | 1 + .../nodes-reload-secure-settings.asciidoc | 55 +++++++++++++++++++ .../api/nodes.reload_secure_settings.json | 23 ++++++++ .../nodes.reload_secure_settings/10_basic.yml | 8 +++ 4 files changed, 87 insertions(+) create mode 100644 docs/reference/cluster/nodes-reload-secure-settings.asciidoc create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/nodes.reload_secure_settings.json create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/nodes.reload_secure_settings/10_basic.yml diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 1036b79a4a5d..15f2b80b2523 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -685,6 +685,7 @@ public void testApiNamingConventions() throws Exception { "nodes.stats", "nodes.hot_threads", "nodes.usage", + "nodes.reload_secure_settings", "search_shards", }; Set deprecatedMethods = new HashSet<>(); diff --git a/docs/reference/cluster/nodes-reload-secure-settings.asciidoc b/docs/reference/cluster/nodes-reload-secure-settings.asciidoc new file mode 100644 index 000000000000..f02ac8e46576 --- /dev/null +++ b/docs/reference/cluster/nodes-reload-secure-settings.asciidoc @@ -0,0 +1,55 @@ +[[cluster-nodes-reload-secure-settings]] +== Nodes Reload Secure Settings + +The cluster nodes reload secure settings API is used to re-read the +local node's encrypted keystore. Specifically, it will prompt the keystore +decryption and reading accross the cluster. The keystore's plain content is +used to reinitialize all compatible plugins. A compatible plugin can be +reinitilized without restarting the node. The operation is +complete when all compatible plugins have finished reinitilizing. Subsequently, +the keystore is closed and any changes to it will not be reflected on the node. + +[source,js] +-------------------------------------------------- +POST _nodes/reload_secure_settings +POST _nodes/nodeId1,nodeId2/reload_secure_settings +-------------------------------------------------- +// CONSOLE +// TEST[setup:node] +// TEST[s/nodeId1,nodeId2/*/] + +The first command reloads the keystore on each node. The seconds allows +to selectively target `nodeId1` and `nodeId2`. The node selection options are +detailed <>. + +Note: It is an error if secure settings are inconsistent across the cluster +nodes, yet this consistency is not enforced whatsoever. Hence, reloading specific +nodes is not standard. It is only justifiable when retrying failed reload operations. + +[float] +[[rest-reload-secure-settings]] +==== REST Reload Secure Settings Response + +The response contains the `nodes` object, which is a map, keyed by the +node id. Each value has the node `name` and an optional `reload_exception` +field. The `reload_exception` field is a serialization of the exception +that was thrown during the reload process, if any. + +[source,js] +-------------------------------------------------- +{ + "_nodes": { + "total": 1, + "successful": 1, + "failed": 0 + }, + "cluster_name": "my_cluster", + "nodes": { + "pQHNt5rXTTWNvUgOrdynKg": { + "name": "node-0" + } + } +} +-------------------------------------------------- +// TESTRESPONSE[s/"my_cluster"/$body.cluster_name/] +// TESTRESPONSE[s/"pQHNt5rXTTWNvUgOrdynKg"/\$node_name/] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/nodes.reload_secure_settings.json b/rest-api-spec/src/main/resources/rest-api-spec/api/nodes.reload_secure_settings.json new file mode 100644 index 000000000000..487beaba8652 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/nodes.reload_secure_settings.json @@ -0,0 +1,23 @@ +{ + "nodes.reload_secure_settings": { + "documentation": "http://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-nodes-reload-secure-settings.html", + "methods": ["POST"], + "url": { + "path": "/_nodes/reload_secure_settings", + "paths": ["/_nodes/reload_secure_settings", "/_nodes/{node_id}/reload_secure_settings"], + "parts": { + "node_id": { + "type": "list", + "description": "A comma-separated list of node IDs to span the reload/reinit call. Should stay empty because reloading usually involves all cluster nodes." + } + }, + "params": { + "timeout": { + "type" : "time", + "description" : "Explicit operation timeout" + } + } + }, + "body": null + } +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/nodes.reload_secure_settings/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/nodes.reload_secure_settings/10_basic.yml new file mode 100644 index 000000000000..0a4cf0d64a00 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/nodes.reload_secure_settings/10_basic.yml @@ -0,0 +1,8 @@ +--- +"node_reload_secure_settings test": + + - do: + nodes.reload_secure_settings: {} + + - is_true: nodes + - is_true: cluster_name From f8b07a0d84eb5f838e2112d04b9241ba7d4597d3 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Sun, 26 Aug 2018 09:36:17 -0400 Subject: [PATCH 157/283] Fix a mappings update test (#33146) This commit fixes a mappings update test. The test is broken in the sense that it passes, but for the wrong reason. The test here is testing that if we make a mapping update but do not commit that mapping update then the mapper service still maintains the previous document mapper. This was not the case long, long ago when a mapping update would update the in-memory state before the cluster state update was committed. This test was passing, but it was passing because the mapping update was never even updated. It was never even updated because it was encountering a null pointer exception. Of course the in-memory state is not going to be updated in that case, we are simply going to end up with a failed cluster state update. Fixing that leads to another issue which is that the mapping source does not even parse so again we would, of course, end up with the in-memory state not being modified. We fix these issues, assert that the result cluster state task completed successfully, and finally that the in-memory state was not updated since we never committed the resulting cluster state. --- .../metadata/MetaDataMappingServiceTests.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java index 1e46c2c42866..6cdca8d93a10 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java @@ -16,12 +16,15 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.cluster.metadata; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingClusterStateUpdateRequest; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateTaskExecutor; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -31,6 +34,7 @@ import java.util.Collections; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; public class MetaDataMappingServiceTests extends ESSingleNodeTestCase { @@ -47,8 +51,18 @@ public void testMappingClusterStateUpdateDoesntChangeExistingIndices() throws Ex final ClusterService clusterService = getInstanceFromNode(ClusterService.class); // TODO - it will be nice to get a random mapping generator final PutMappingClusterStateUpdateRequest request = new PutMappingClusterStateUpdateRequest().type("type"); - request.source("{ \"properties\" { \"field\": { \"type\": \"text\" }}}"); - mappingService.putMappingExecutor.execute(clusterService.state(), Collections.singletonList(request)); + request.indices(new Index[] {indexService.index()}); + request.source("{ \"properties\": { \"field\": { \"type\": \"text\" }}}"); + final ClusterStateTaskExecutor.ClusterTasksResult result = + mappingService.putMappingExecutor.execute(clusterService.state(), Collections.singletonList(request)); + // the task completed successfully + assertThat(result.executionResults.size(), equalTo(1)); + assertTrue(result.executionResults.values().iterator().next().isSuccess()); + // the task really was a mapping update + assertThat( + indexService.mapperService().documentMapper("type").mappingSource(), + not(equalTo(result.resultingState.metaData().index("test").mapping("type").source()))); + // since we never committed the cluster state update, the in-memory state is unchanged assertThat(indexService.mapperService().documentMapper("type").mappingSource(), equalTo(currentMapping)); } @@ -69,4 +83,5 @@ public void testClusterStateIsNotChangedWithIdenticalMappings() throws Exception assertSame(result, result2); } + } From 143cd9bbaa082fa9535990bba32c47739411cb4d Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Sun, 26 Aug 2018 15:57:52 -0400 Subject: [PATCH 158/283] Do not lose default mapper on metadata updates (#33153) When applying index metadata updates we run through the mappings updating them if needed. Today if there is not an update to the default mapper, we can lose the default mapping. This means that, for example, if we apply a settings update to an index we will lose the default mapper. This happens because we were not guarding updating the default mapping with a check that the default mapping was updated in the metadata update. When there is no update in the metadata update, we need to continue to preserve the previous default mapping. This commit achieves this by moving the updating of the default mapping under the same guard that we use for updating the default mapping source. We add a test that fails before putting the update under a guard and now passes after moving the update under the guard. --- .../index/mapper/MapperService.java | 2 +- .../index/mapper/MapperServiceTests.java | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 9cd8ef1f6ac6..15448bb4003d 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -468,11 +468,11 @@ private synchronized Map internalMerge(@Nullable Documen // commit the change if (defaultMappingSource != null) { this.defaultMappingSource = defaultMappingSource; + this.defaultMapper = defaultMapper; } if (newMapper != null) { this.mapper = newMapper; } - this.defaultMapper = defaultMapper; this.fieldTypes = fieldTypes; this.hasNested = hasNested; this.fullPathObjectMappers = fullPathObjectMappers; diff --git a/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java b/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java index 51b6e9d71688..e31cee29e67b 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java @@ -21,13 +21,16 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.mapper.KeywordFieldMapper.KeywordFieldType; import org.elasticsearch.index.mapper.MapperService.MergeReason; @@ -119,6 +122,35 @@ public void testIndexIntoDefaultMapping() throws Throwable { assertNull(indexService.mapperService().documentMapper(MapperService.DEFAULT_MAPPING)); } + public void testIndexMetaDataUpdateDoesNotLoseDefaultMapper() throws IOException { + final IndexService indexService = + createIndex("test", Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_6_3_0).build()); + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + { + builder.startObject(MapperService.DEFAULT_MAPPING); + { + builder.field("date_detection", false); + } + builder.endObject(); + } + builder.endObject(); + final PutMappingRequest putMappingRequest = new PutMappingRequest(); + putMappingRequest.indices("test"); + putMappingRequest.type(MapperService.DEFAULT_MAPPING); + putMappingRequest.source(builder); + client().admin().indices().preparePutMapping("test").setType(MapperService.DEFAULT_MAPPING).setSource(builder).get(); + } + assertNotNull(indexService.mapperService().documentMapper(MapperService.DEFAULT_MAPPING)); + final Settings zeroReplicasSettings = Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0).build(); + client().admin().indices().prepareUpdateSettings("test").setSettings(zeroReplicasSettings).get(); + /* + * This assertion is a guard against a previous bug that would lose the default mapper when applying a metadata update that did not + * update the default mapping. + */ + assertNotNull(indexService.mapperService().documentMapper(MapperService.DEFAULT_MAPPING)); + } + public void testTotalFieldsExceedsLimit() throws Throwable { Function mapping = type -> { try { From 06c0055c0f4756bf7b3db84c9e476e7a20a9758d Mon Sep 17 00:00:00 2001 From: Daniel Mitterdorfer Date: Mon, 27 Aug 2018 07:09:27 +0200 Subject: [PATCH 159/283] Have circuit breaker succeed on unknown mem usage With this commit we implement a workaround for https://bugs.openjdk.java.net/browse/JDK-8207200 which is a race condition in the JVM that results in `IllegalArgumentException` to be thrown in rare cases when we determine memory usage via `MemoryMXBean`. As we do not want to fail requests in those cases we always return zero memory usage. Relates #31767 Relates #33125 --- .../breaker/HierarchyCircuitBreakerService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerService.java b/server/src/main/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerService.java index 7e6a9c29a834..3d05293f7b71 100644 --- a/server/src/main/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerService.java +++ b/server/src/main/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerService.java @@ -251,7 +251,16 @@ private ParentMemoryUsage parentUsed(long newBytesReserved) { //package private to allow overriding it in tests long currentMemoryUsage() { - return MEMORY_MX_BEAN.getHeapMemoryUsage().getUsed(); + try { + return MEMORY_MX_BEAN.getHeapMemoryUsage().getUsed(); + } catch (IllegalArgumentException ex) { + // This exception can happen (rarely) due to a race condition in the JVM when determining usage of memory pools. We do not want + // to fail requests because of this and thus return zero memory usage in this case. While we could also return the most + // recently determined memory usage, we would overestimate memory usage immediately after a garbage collection event. + assert ex.getMessage().matches("committed = \\d+ should be < max = \\d+"); + logger.info("Cannot determine current memory usage due to JDK-8207200.", ex); + return 0; + } } /** From 47e9e72df23f3bb9a6983c8c4ac286385dab100c Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 27 Aug 2018 12:14:46 +0700 Subject: [PATCH 160/283] reduce maximum number of writes to speed up test --- .../elasticsearch/xpack/ccr/action/ShardChangesActionTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ac6d8f786fbe..430e9cb48b1a 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 @@ -46,7 +46,7 @@ public void testGetOperations() throws Exception { .build(); final IndexService indexService = createIndex("index", settings); - final int numWrites = randomIntBetween(2, 8192); + final int numWrites = randomIntBetween(2, 4096); for (int i = 0; i < numWrites; i++) { client().prepareIndex("index", "doc", Integer.toString(i)).setSource("{}", XContentType.JSON).get(); } From 30c3b363950e51e6d2ff96195fc1a54f0161c590 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Mon, 27 Aug 2018 08:44:06 +0300 Subject: [PATCH 161/283] Apply publishing to genreate pom (#33094) --- x-pack/plugin/security/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 5198c3da6698..f2c78e122584 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -1,6 +1,7 @@ evaluationDependsOn(xpackModule('core')) apply plugin: 'elasticsearch.esplugin' +apply plugin: 'nebula.maven-scm' esplugin { name 'x-pack-security' description 'Elasticsearch Expanded Pack Plugin - Security' From 974f83909371920696dca4d9980c6e3f8c2c90bd Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Mon, 27 Aug 2018 08:47:42 +0300 Subject: [PATCH 162/283] Fix forbiddenapis on java 11 (#33116) Cap forbiddenapis to java version 10 --- .../gradle/precommit/ForbiddenApisCliTask.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java index e33f16709641..21a0597b38af 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java @@ -23,6 +23,8 @@ import org.gradle.api.DefaultTask; import org.gradle.api.JavaVersion; import org.gradle.api.file.FileCollection; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputFile; @@ -41,6 +43,7 @@ public class ForbiddenApisCliTask extends DefaultTask { + private final Logger logger = Logging.getLogger(ForbiddenApisCliTask.class); private FileCollection signaturesFiles; private List signatures = new ArrayList<>(); private Set bundledSignatures = new LinkedHashSet<>(); @@ -49,12 +52,21 @@ public class ForbiddenApisCliTask extends DefaultTask { private FileCollection classesDirs; private Action execAction; + @Input public JavaVersion getTargetCompatibility() { return targetCompatibility; } public void setTargetCompatibility(JavaVersion targetCompatibility) { - this.targetCompatibility = targetCompatibility; + if (targetCompatibility.compareTo(JavaVersion.VERSION_1_10) > 0) { + logger.warn( + "Target compatibility is set to {} but forbiddenapis only supports up to 10. Will cap at 10.", + targetCompatibility + ); + this.targetCompatibility = JavaVersion.VERSION_1_10; + } else { + this.targetCompatibility = targetCompatibility; + } } public Action getExecAction() { From e1e8cf382f629ae9576cd5b1d7a8add023e7dea6 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Mon, 27 Aug 2018 09:18:26 +0200 Subject: [PATCH 163/283] [Rollup] Move toBuilders() methods out of rollup config objects (#32585) --- .../rollup/job/DateHistogramGroupConfig.java | 18 --- .../core/rollup/job/HistogramGroupConfig.java | 24 --- .../xpack/core/rollup/job/MetricConfig.java | 59 +------ .../core/rollup/job/TermsGroupConfig.java | 19 --- .../job/TermsGroupConfigSerializingTests.java | 61 -------- .../xpack/rollup/job/RollupIndexer.java | 144 ++++++++++++++++-- .../rollup/action/job/RollupIndexTests.java | 83 ++++++++++ .../xpack/rollup/job/IndexerUtilsTests.java | 25 ++- 8 files changed, 236 insertions(+), 197 deletions(-) create mode 100644 x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/job/RollupIndexTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java index a9cc95bb07c9..166322b93722 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/DateHistogramGroupConfig.java @@ -20,16 +20,11 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; -import org.elasticsearch.search.aggregations.bucket.composite.DateHistogramValuesSourceBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; -import org.elasticsearch.xpack.core.rollup.RollupField; import org.joda.time.DateTimeZone; import java.io.IOException; -import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Objects; @@ -182,19 +177,6 @@ public Rounding createRounding() { return createRounding(interval.toString(), timeZone); } - /** - * This returns a set of aggregation builders which represent the configured - * set of date histograms. Used by the rollup indexer to iterate over historical data - */ - public List> toBuilders() { - DateHistogramValuesSourceBuilder vsBuilder = - new DateHistogramValuesSourceBuilder(RollupField.formatIndexerAggName(field, DateHistogramAggregationBuilder.NAME)); - vsBuilder.dateHistogramInterval(interval); - vsBuilder.field(field); - vsBuilder.timeZone(toDateTimeZone(timeZone)); - return Collections.singletonList(vsBuilder); - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java index d1bc50566faf..a22d022ee2db 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/HistogramGroupConfig.java @@ -16,18 +16,13 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; -import org.elasticsearch.search.aggregations.bucket.composite.HistogramValuesSourceBuilder; -import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder; import org.elasticsearch.xpack.core.rollup.RollupField; import java.io.IOException; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; @@ -85,25 +80,6 @@ public String[] getFields() { return fields; } - /** - * This returns a set of aggregation builders which represent the configured - * set of histograms. Used by the rollup indexer to iterate over historical data - */ - public List> toBuilders() { - if (fields.length == 0) { - return Collections.emptyList(); - } - - return Arrays.stream(fields).map(f -> { - HistogramValuesSourceBuilder vsBuilder - = new HistogramValuesSourceBuilder(RollupField.formatIndexerAggName(f, HistogramAggregationBuilder.NAME)); - vsBuilder.interval(interval); - vsBuilder.field(f); - vsBuilder.missingBucket(true); - return vsBuilder; - }).collect(Collectors.toList()); - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java index b4e022f55004..3a267e4cfa47 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/MetricConfig.java @@ -16,18 +16,9 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.search.aggregations.metrics.avg.AvgAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.max.MaxAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.min.MinAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.sum.SumAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCountAggregationBuilder; -import org.elasticsearch.search.aggregations.support.ValueType; -import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; import org.elasticsearch.xpack.core.rollup.RollupField; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -53,11 +44,11 @@ public class MetricConfig implements Writeable, ToXContentObject { // TODO: replace these with an enum - private static final ParseField MIN = new ParseField("min"); - private static final ParseField MAX = new ParseField("max"); - private static final ParseField SUM = new ParseField("sum"); - private static final ParseField AVG = new ParseField("avg"); - private static final ParseField VALUE_COUNT = new ParseField("value_count"); + public static final ParseField MIN = new ParseField("min"); + public static final ParseField MAX = new ParseField("max"); + public static final ParseField SUM = new ParseField("sum"); + public static final ParseField AVG = new ParseField("avg"); + public static final ParseField VALUE_COUNT = new ParseField("value_count"); static final String NAME = "metrics"; private static final String FIELD = "field"; @@ -111,46 +102,6 @@ public List getMetrics() { return metrics; } - /** - * This returns a set of aggregation builders which represent the configured - * set of metrics. Used by the rollup indexer to iterate over historical data - */ - public List toBuilders() { - if (metrics.size() == 0) { - return Collections.emptyList(); - } - - List aggs = new ArrayList<>(metrics.size()); - for (String metric : metrics) { - ValuesSourceAggregationBuilder.LeafOnly newBuilder; - if (metric.equals(MIN.getPreferredName())) { - newBuilder = new MinAggregationBuilder(RollupField.formatFieldName(field, MinAggregationBuilder.NAME, RollupField.VALUE)); - } else if (metric.equals(MAX.getPreferredName())) { - newBuilder = new MaxAggregationBuilder(RollupField.formatFieldName(field, MaxAggregationBuilder.NAME, RollupField.VALUE)); - } else if (metric.equals(AVG.getPreferredName())) { - // Avgs are sum + count - newBuilder = new SumAggregationBuilder(RollupField.formatFieldName(field, AvgAggregationBuilder.NAME, RollupField.VALUE)); - ValuesSourceAggregationBuilder.LeafOnly countBuilder - = new ValueCountAggregationBuilder( - RollupField.formatFieldName(field, AvgAggregationBuilder.NAME, RollupField.COUNT_FIELD), ValueType.NUMERIC); - countBuilder.field(field); - aggs.add(countBuilder); - } else if (metric.equals(SUM.getPreferredName())) { - newBuilder = new SumAggregationBuilder(RollupField.formatFieldName(field, SumAggregationBuilder.NAME, RollupField.VALUE)); - } else if (metric.equals(VALUE_COUNT.getPreferredName())) { - // TODO allow non-numeric value_counts. - // Hardcoding this is fine for now since the job validation guarantees that all metric fields are numerics - newBuilder = new ValueCountAggregationBuilder( - RollupField.formatFieldName(field, ValueCountAggregationBuilder.NAME, RollupField.VALUE), ValueType.NUMERIC); - } else { - throw new IllegalArgumentException("Unsupported metric type [" + metric + "]"); - } - newBuilder.field(field); - aggs.add(newBuilder); - } - return aggs; - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java index abd6825e9f7b..fbc039843258 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfig.java @@ -18,16 +18,11 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.TextFieldMapper; -import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; -import org.elasticsearch.search.aggregations.bucket.composite.TermsValuesSourceBuilder; -import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.elasticsearch.xpack.core.rollup.RollupField; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; @@ -79,20 +74,6 @@ public String[] getFields() { return fields; } - /** - * This returns a set of aggregation builders which represent the configured - * set of date histograms. Used by the rollup indexer to iterate over historical data - */ - public List> toBuilders() { - return Arrays.stream(fields).map(f -> { - TermsValuesSourceBuilder vsBuilder - = new TermsValuesSourceBuilder(RollupField.formatIndexerAggName(f, TermsAggregationBuilder.NAME)); - vsBuilder.field(f); - vsBuilder.missingBucket(true); - return vsBuilder; - }).collect(Collectors.toList()); - } - public void validateMappings(Map> fieldCapsResponse, ActionRequestValidationException validationException) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfigSerializingTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfigSerializingTests.java index ccdd616df7b5..b0e33579eb35 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfigSerializingTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/TermsGroupConfigSerializingTests.java @@ -9,19 +9,16 @@ import org.elasticsearch.action.fieldcaps.FieldCapabilities; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; import org.elasticsearch.test.AbstractSerializingTestCase; import java.io.IOException; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import static org.elasticsearch.xpack.core.rollup.ConfigTestHelpers.randomTermsGroupConfig; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class TermsGroupConfigSerializingTests extends AbstractSerializingTestCase { @@ -77,62 +74,4 @@ public void testValidateFieldWrongType() { assertThat(e.validationErrors().get(0), equalTo("The field referenced by a terms group must be a [numeric] or " + "[keyword/text] type, but found [geo_point] for field [my_field]")); } - - public void testValidateFieldMatchingNotAggregatable() { - ActionRequestValidationException e = new ActionRequestValidationException(); - Map> responseMap = new HashMap<>(); - - // Have to mock fieldcaps because the ctor's aren't public... - FieldCapabilities fieldCaps = mock(FieldCapabilities.class); - when(fieldCaps.isAggregatable()).thenReturn(false); - responseMap.put("my_field", Collections.singletonMap(getRandomType(), fieldCaps)); - - TermsGroupConfig config = new TermsGroupConfig("my_field"); - config.validateMappings(responseMap, e); - assertThat(e.validationErrors().get(0), equalTo("The field [my_field] must be aggregatable across all indices, but is not.")); - } - - public void testValidateMatchingField() { - ActionRequestValidationException e = new ActionRequestValidationException(); - Map> responseMap = new HashMap<>(); - String type = getRandomType(); - - // Have to mock fieldcaps because the ctor's aren't public... - FieldCapabilities fieldCaps = mock(FieldCapabilities.class); - when(fieldCaps.isAggregatable()).thenReturn(true); - responseMap.put("my_field", Collections.singletonMap(type, fieldCaps)); - - TermsGroupConfig config = new TermsGroupConfig("my_field"); - config.validateMappings(responseMap, e); - if (e.validationErrors().size() != 0) { - fail(e.getMessage()); - } - - List> builders = config.toBuilders(); - assertThat(builders.size(), equalTo(1)); - } - - private String getRandomType() { - int n = randomIntBetween(0,8); - if (n == 0) { - return "keyword"; - } else if (n == 1) { - return "text"; - } else if (n == 2) { - return "long"; - } else if (n == 3) { - return "integer"; - } else if (n == 4) { - return "short"; - } else if (n == 5) { - return "float"; - } else if (n == 6) { - return "double"; - } else if (n == 7) { - return "scaled_float"; - } else if (n == 8) { - return "half_float"; - } - return "long"; - } } diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java index d1db021361c8..6abb7ffa5675 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java @@ -15,19 +15,35 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.RangeQueryBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.DateHistogramValuesSourceBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.HistogramValuesSourceBuilder; +import org.elasticsearch.search.aggregations.bucket.composite.TermsValuesSourceBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.avg.AvgAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.max.MaxAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.min.MinAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.sum.SumAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCountAggregationBuilder; +import org.elasticsearch.search.aggregations.support.ValueType; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.xpack.core.rollup.RollupField; import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; import org.elasticsearch.xpack.core.rollup.job.GroupConfig; import org.elasticsearch.xpack.core.rollup.job.HistogramGroupConfig; import org.elasticsearch.xpack.core.rollup.job.IndexerState; +import org.elasticsearch.xpack.core.rollup.job.MetricConfig; import org.elasticsearch.xpack.core.rollup.job.RollupJob; import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; import org.elasticsearch.xpack.core.rollup.job.RollupJobStats; +import org.elasticsearch.xpack.core.rollup.job.TermsGroupConfig; +import org.joda.time.DateTimeZone; import java.util.ArrayList; import java.util.Arrays; @@ -38,6 +54,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; +import static org.elasticsearch.xpack.core.rollup.RollupField.formatFieldName; + /** * An abstract class that builds a rollup index incrementally. A background job can be launched using {@link #maybeTriggerAsyncJob(long)}, * it will create the rollup index from the source index up to the last complete bucket that is allowed to be built (based on the current @@ -392,21 +412,12 @@ private SearchRequest buildSearchRequest() { */ private CompositeAggregationBuilder createCompositeBuilder(RollupJobConfig config) { final GroupConfig groupConfig = config.getGroupConfig(); - List> builders = new ArrayList<>(); - - // Add all the agg builders to our request in order: date_histo -> histo -> terms - if (groupConfig != null) { - builders.addAll(groupConfig.getDateHistogram().toBuilders()); - if (groupConfig.getHistogram() != null) { - builders.addAll(groupConfig.getHistogram().toBuilders()); - } - if (groupConfig.getTerms() != null) { - builders.addAll(groupConfig.getTerms().toBuilders()); - } - } + List> builders = createValueSourceBuilders(groupConfig); CompositeAggregationBuilder composite = new CompositeAggregationBuilder(AGGREGATION_NAME, builders); - config.getMetricsConfig().forEach(m -> m.toBuilders().forEach(composite::subAggregation)); + + List aggregations = createAggregationBuilders(config.getMetricsConfig()); + aggregations.forEach(composite::subAggregation); final Map metadata = createMetadata(groupConfig); if (metadata.isEmpty() == false) { @@ -456,5 +467,112 @@ static Map createMetadata(final GroupConfig groupConfig) { } return metadata; } + + public static List> createValueSourceBuilders(final GroupConfig groupConfig) { + final List> builders = new ArrayList<>(); + // Add all the agg builders to our request in order: date_histo -> histo -> terms + if (groupConfig != null) { + final DateHistogramGroupConfig dateHistogram = groupConfig.getDateHistogram(); + builders.addAll(createValueSourceBuilders(dateHistogram)); + + final HistogramGroupConfig histogram = groupConfig.getHistogram(); + builders.addAll(createValueSourceBuilders(histogram)); + + final TermsGroupConfig terms = groupConfig.getTerms(); + builders.addAll(createValueSourceBuilders(terms)); + } + return unmodifiableList(builders); + } + + public static List> createValueSourceBuilders(final DateHistogramGroupConfig dateHistogram) { + final String dateHistogramField = dateHistogram.getField(); + final String dateHistogramName = RollupField.formatIndexerAggName(dateHistogramField, DateHistogramAggregationBuilder.NAME); + final DateHistogramValuesSourceBuilder dateHistogramBuilder = new DateHistogramValuesSourceBuilder(dateHistogramName); + dateHistogramBuilder.dateHistogramInterval(dateHistogram.getInterval()); + dateHistogramBuilder.field(dateHistogramField); + dateHistogramBuilder.timeZone(toDateTimeZone(dateHistogram.getTimeZone())); + return singletonList(dateHistogramBuilder); + } + + public static List> createValueSourceBuilders(final HistogramGroupConfig histogram) { + final List> builders = new ArrayList<>(); + if (histogram != null) { + for (String field : histogram.getFields()) { + final String histogramName = RollupField.formatIndexerAggName(field, HistogramAggregationBuilder.NAME); + final HistogramValuesSourceBuilder histogramBuilder = new HistogramValuesSourceBuilder(histogramName); + histogramBuilder.interval(histogram.getInterval()); + histogramBuilder.field(field); + histogramBuilder.missingBucket(true); + builders.add(histogramBuilder); + } + } + return unmodifiableList(builders); + } + + public static List> createValueSourceBuilders(final TermsGroupConfig terms) { + final List> builders = new ArrayList<>(); + if (terms != null) { + for (String field : terms.getFields()) { + final String termsName = RollupField.formatIndexerAggName(field, TermsAggregationBuilder.NAME); + final TermsValuesSourceBuilder termsBuilder = new TermsValuesSourceBuilder(termsName); + termsBuilder.field(field); + termsBuilder.missingBucket(true); + builders.add(termsBuilder); + } + } + return unmodifiableList(builders); + } + + /** + * This returns a set of aggregation builders which represent the configured + * set of metrics. Used to iterate over historical data. + */ + static List createAggregationBuilders(final List metricsConfigs) { + final List builders = new ArrayList<>(); + if (metricsConfigs != null) { + for (MetricConfig metricConfig : metricsConfigs) { + final List metrics = metricConfig.getMetrics(); + if (metrics.isEmpty() == false) { + final String field = metricConfig.getField(); + for (String metric : metrics) { + ValuesSourceAggregationBuilder.LeafOnly newBuilder; + if (metric.equals(MetricConfig.MIN.getPreferredName())) { + newBuilder = new MinAggregationBuilder(formatFieldName(field, MinAggregationBuilder.NAME, RollupField.VALUE)); + } else if (metric.equals(MetricConfig.MAX.getPreferredName())) { + newBuilder = new MaxAggregationBuilder(formatFieldName(field, MaxAggregationBuilder.NAME, RollupField.VALUE)); + } else if (metric.equals(MetricConfig.AVG.getPreferredName())) { + // Avgs are sum + count + newBuilder = new SumAggregationBuilder(formatFieldName(field, AvgAggregationBuilder.NAME, RollupField.VALUE)); + ValuesSourceAggregationBuilder.LeafOnly countBuilder + = new ValueCountAggregationBuilder( + formatFieldName(field, AvgAggregationBuilder.NAME, RollupField.COUNT_FIELD), ValueType.NUMERIC); + countBuilder.field(field); + builders.add(countBuilder); + } else if (metric.equals(MetricConfig.SUM.getPreferredName())) { + newBuilder = new SumAggregationBuilder(formatFieldName(field, SumAggregationBuilder.NAME, RollupField.VALUE)); + } else if (metric.equals(MetricConfig.VALUE_COUNT.getPreferredName())) { + // TODO allow non-numeric value_counts. + // Hardcoding this is fine for now since the job validation guarantees that all metric fields are numerics + newBuilder = new ValueCountAggregationBuilder( + formatFieldName(field, ValueCountAggregationBuilder.NAME, RollupField.VALUE), ValueType.NUMERIC); + } else { + throw new IllegalArgumentException("Unsupported metric type [" + metric + "]"); + } + newBuilder.field(field); + builders.add(newBuilder); + } + } + } + } + return unmodifiableList(builders); + } + + private static DateTimeZone toDateTimeZone(final String timezone) { + try { + return DateTimeZone.forOffsetHours(Integer.parseInt(timezone)); + } catch (NumberFormatException e) { + return DateTimeZone.forID(timezone); + } + } } diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/job/RollupIndexTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/job/RollupIndexTests.java new file mode 100644 index 000000000000..c0ba74e762de --- /dev/null +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/job/RollupIndexTests.java @@ -0,0 +1,83 @@ +/* + * 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.rollup.action.job; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.fieldcaps.FieldCapabilities; +import org.elasticsearch.search.aggregations.bucket.composite.CompositeValuesSourceBuilder; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.rollup.job.TermsGroupConfig; +import org.elasticsearch.xpack.rollup.job.RollupIndexer; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RollupIndexTests extends ESTestCase { + + public void testValidateMatchingField() { + ActionRequestValidationException e = new ActionRequestValidationException(); + Map> responseMap = new HashMap<>(); + String type = getRandomType(); + + // Have to mock fieldcaps because the ctor's aren't public... + FieldCapabilities fieldCaps = mock(FieldCapabilities.class); + when(fieldCaps.isAggregatable()).thenReturn(true); + responseMap.put("my_field", Collections.singletonMap(type, fieldCaps)); + + TermsGroupConfig config = new TermsGroupConfig("my_field"); + config.validateMappings(responseMap, e); + if (e.validationErrors().size() != 0) { + fail(e.getMessage()); + } + + List> builders = RollupIndexer.createValueSourceBuilders(config); + assertThat(builders.size(), equalTo(1)); + } + + public void testValidateFieldMatchingNotAggregatable() { + ActionRequestValidationException e = new ActionRequestValidationException(); + Map> responseMap = new HashMap<>(); + + // Have to mock fieldcaps because the ctor's aren't public... + FieldCapabilities fieldCaps = mock(FieldCapabilities.class); + when(fieldCaps.isAggregatable()).thenReturn(false); + responseMap.put("my_field", Collections.singletonMap(getRandomType(), fieldCaps)); + + TermsGroupConfig config = new TermsGroupConfig("my_field"); + config.validateMappings(responseMap, e); + assertThat(e.validationErrors().get(0), equalTo("The field [my_field] must be aggregatable across all indices, but is not.")); + } + + private String getRandomType() { + int n = randomIntBetween(0,8); + if (n == 0) { + return "keyword"; + } else if (n == 1) { + return "text"; + } else if (n == 2) { + return "long"; + } else if (n == 3) { + return "integer"; + } else if (n == 4) { + return "short"; + } else if (n == 5) { + return "float"; + } else if (n == 6) { + return "double"; + } else if (n == 7) { + return "scaled_float"; + } else if (n == 8) { + return "half_float"; + } + return "long"; + } +} diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/IndexerUtilsTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/IndexerUtilsTests.java index e8c66f7e8c11..d74e7413d15b 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/IndexerUtilsTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/IndexerUtilsTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorTestCase; @@ -57,6 +58,7 @@ import static org.elasticsearch.xpack.core.rollup.ConfigTestHelpers.randomDateHistogramGroupConfig; import static org.elasticsearch.xpack.core.rollup.ConfigTestHelpers.randomGroupConfig; import static org.elasticsearch.xpack.core.rollup.ConfigTestHelpers.randomHistogramGroupConfig; +import static org.elasticsearch.xpack.rollup.job.RollupIndexer.createAggregationBuilders; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -101,9 +103,11 @@ public void testMissingFields() throws IOException { //TODO swap this over to DateHistoConfig.Builder once DateInterval is in DateHistogramGroupConfig dateHistoGroupConfig = new DateHistogramGroupConfig(timestampField, DateHistogramInterval.DAY); CompositeAggregationBuilder compositeBuilder = - new CompositeAggregationBuilder(RollupIndexer.AGGREGATION_NAME, dateHistoGroupConfig.toBuilders()); + new CompositeAggregationBuilder(RollupIndexer.AGGREGATION_NAME, + RollupIndexer.createValueSourceBuilders(dateHistoGroupConfig)); MetricConfig metricConfig = new MetricConfig("does_not_exist", singletonList("max")); - metricConfig.toBuilders().forEach(compositeBuilder::subAggregation); + List metricAgg = createAggregationBuilders(singletonList(metricConfig)); + metricAgg.forEach(compositeBuilder::subAggregation); Aggregator aggregator = createAggregator(compositeBuilder, indexSearcher, timestampFieldType, valueFieldType); aggregator.preCollection(); @@ -170,7 +174,8 @@ public void testCorrectFields() throws IOException { singletonList(dateHisto)); MetricConfig metricConfig = new MetricConfig(valueField, singletonList("max")); - metricConfig.toBuilders().forEach(compositeBuilder::subAggregation); + List metricAgg = createAggregationBuilders(singletonList(metricConfig)); + metricAgg.forEach(compositeBuilder::subAggregation); Aggregator aggregator = createAggregator(compositeBuilder, indexSearcher, timestampFieldType, valueFieldType); aggregator.preCollection(); @@ -226,7 +231,8 @@ public void testNumericTerms() throws IOException { singletonList(terms)); MetricConfig metricConfig = new MetricConfig(valueField, singletonList("max")); - metricConfig.toBuilders().forEach(compositeBuilder::subAggregation); + List metricAgg = createAggregationBuilders(singletonList(metricConfig)); + metricAgg.forEach(compositeBuilder::subAggregation); Aggregator aggregator = createAggregator(compositeBuilder, indexSearcher, valueFieldType); aggregator.preCollection(); @@ -292,7 +298,8 @@ public void testEmptyCounts() throws IOException { singletonList(dateHisto)); MetricConfig metricConfig = new MetricConfig("another_field", Arrays.asList("avg", "sum")); - metricConfig.toBuilders().forEach(compositeBuilder::subAggregation); + List metricAgg = createAggregationBuilders(singletonList(metricConfig)); + metricAgg.forEach(compositeBuilder::subAggregation); Aggregator aggregator = createAggregator(compositeBuilder, indexSearcher, timestampFieldType, valueFieldType); aggregator.preCollection(); @@ -523,11 +530,13 @@ public void testMissingBuckets() throws IOException { // Setup the composite agg TermsGroupConfig termsGroupConfig = new TermsGroupConfig(valueField); - CompositeAggregationBuilder compositeBuilder = new CompositeAggregationBuilder(RollupIndexer.AGGREGATION_NAME, - termsGroupConfig.toBuilders()).size(numDocs*2); + CompositeAggregationBuilder compositeBuilder = + new CompositeAggregationBuilder(RollupIndexer.AGGREGATION_NAME, RollupIndexer.createValueSourceBuilders(termsGroupConfig)) + .size(numDocs*2); MetricConfig metricConfig = new MetricConfig(metricField, singletonList("max")); - metricConfig.toBuilders().forEach(compositeBuilder::subAggregation); + List metricAgg = createAggregationBuilders(singletonList(metricConfig)); + metricAgg.forEach(compositeBuilder::subAggregation); Aggregator aggregator = createAggregator(compositeBuilder, indexSearcher, valueFieldType, metricFieldType); aggregator.preCollection(); From f1f6d4ed337a7b36895f344b964043e8c74fcd6b Mon Sep 17 00:00:00 2001 From: Mikita Karaliou Date: Mon, 27 Aug 2018 13:24:51 +0300 Subject: [PATCH 164/283] Support only string `format` in date, root object & date range (#28117) Limit date `format` attribute to String values only. Closes #23650 --- .../index/mapper/TypeParsers.java | 5 +++- .../index/mapper/DateFieldMapperTests.java | 18 +++++++++++++ .../index/mapper/RangeFieldMapperTests.java | 18 +++++++++++++ .../index/mapper/RootObjectMapperTests.java | 26 +++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java index 667f4a736173..a43aed3b08de 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java @@ -264,7 +264,10 @@ private static IndexOptions nodeIndexOptionValue(final Object propNode) { } public static FormatDateTimeFormatter parseDateTimeFormatter(Object node) { - return Joda.forPattern(node.toString()); + if (node instanceof String) { + return Joda.forPattern((String) node); + } + throw new IllegalArgumentException("Invalid format: [" + node.toString() + "]: expected string value"); } public static void parseTermVector(String fieldName, String termVector, FieldMapper.Builder builder) throws MapperParsingException { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java index 51b270940998..d16bdc444e6e 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java @@ -414,4 +414,22 @@ public void testMergeText() throws Exception { () -> mapper.merge(update.mapping())); assertEquals("mapper [date] of different type, current_type [date], merged_type [text]", e.getMessage()); } + + public void testIllegalFormatField() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("field") + .field("type", "date") + .array("format", "test_format") + .endObject() + .endObject() + .endObject() + .endObject()); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> parser.parse("type", new CompressedXContent(mapping))); + assertEquals("Invalid format: [[test_format]]: expected string value", e.getMessage()); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java index 54418850e5d4..00068f76e753 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java @@ -443,4 +443,22 @@ public void testSerializeDefaults() throws Exception { } } + public void testIllegalFormatField() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startObject("properties") + .startObject("field") + .field("type", "date_range") + .array("format", "test_format") + .endObject() + .endObject() + .endObject() + .endObject()); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> parser.parse("type", new CompressedXContent(mapping))); + assertEquals("Invalid format: [[test_format]]: expected string value", e.getMessage()); + } + } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java index ec21a1f7286a..574d4eee70a0 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java @@ -159,4 +159,30 @@ public void testDynamicTemplates() throws Exception { mapper = mapperService.merge("type", new CompressedXContent(mapping3), MergeReason.MAPPING_UPDATE); assertEquals(mapping3, mapper.mappingSource().toString()); } + + public void testIllegalFormatField() throws Exception { + String dynamicMapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startArray("dynamic_date_formats") + .startArray().value("test_format").endArray() + .endArray() + .endObject() + .endObject()); + String mapping = Strings.toString(XContentFactory.jsonBuilder() + .startObject() + .startObject("type") + .startArray("date_formats") + .startArray().value("test_format").endArray() + .endArray() + .endObject() + .endObject()); + + DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser(); + for (String m : Arrays.asList(mapping, dynamicMapping)) { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> parser.parse("type", new CompressedXContent(m))); + assertEquals("Invalid format: [[test_format]]: expected string value", e.getMessage()); + } + } } From 1779d3376ac9c40d0916657c948555c2f043d5ab Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Mon, 27 Aug 2018 08:42:40 -0400 Subject: [PATCH 165/283] APM server monitoring (#32515) * Adding new MonitoredSystem for APM server * Teaching Monitoring template utils about APM server monitoring indices * Documenting new monitoring index for APM server * Adding monitoring index template for APM server * Copy pasta typo * Removing metrics.libbeat.config section from mapping * Adding built-in user and role for APM server user * Actually define the role :) * Adding missing import * Removing index template and system ID for apm server * Shortening line lengths * Updating expected number of built-in users in integration test * Removing "system" from role and user names * Rearranging users to make tests pass --- .../commands/setup-passwords.asciidoc | 2 +- docs/reference/monitoring/exporters.asciidoc | 12 ++++----- .../docs/en/security/configuring-es.asciidoc | 4 +-- .../authc/esnative/ClientReservedRealm.java | 1 + .../authz/store/ReservedRolesStore.java | 2 ++ .../core/security/user/APMSystemUser.java | 25 ++++++++++++++++++ .../core/security/user/UsernamesField.java | 2 ++ .../authz/store/ReservedRolesStoreTests.java | 26 +++++++++++++++++++ .../authc/esnative/ReservedRealm.java | 8 ++++++ .../esnative/tool/SetupPasswordTool.java | 4 ++- .../test/NativeRealmIntegTestCase.java | 3 ++- .../authc/esnative/NativeUsersStoreTests.java | 7 +++-- .../esnative/ReservedRealmIntegTests.java | 13 +++++++--- .../authc/esnative/ReservedRealmTests.java | 16 +++++++++--- .../esnative/tool/SetupPasswordToolIT.java | 2 +- 15 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java diff --git a/docs/reference/commands/setup-passwords.asciidoc b/docs/reference/commands/setup-passwords.asciidoc index a7dcd25d65e0..e2d4dfdc13d3 100644 --- a/docs/reference/commands/setup-passwords.asciidoc +++ b/docs/reference/commands/setup-passwords.asciidoc @@ -4,7 +4,7 @@ == elasticsearch-setup-passwords The `elasticsearch-setup-passwords` command sets the passwords for the built-in -`elastic`, `kibana`, `logstash_system`, and `beats_system` users. +`elastic`, `kibana`, `logstash_system`, `beats_system`, and `apm_system` users. [float] === Synopsis diff --git a/docs/reference/monitoring/exporters.asciidoc b/docs/reference/monitoring/exporters.asciidoc index 2a7729eee942..a1d4bc08ae73 100644 --- a/docs/reference/monitoring/exporters.asciidoc +++ b/docs/reference/monitoring/exporters.asciidoc @@ -105,12 +105,12 @@ route monitoring data: [options="header"] |======================= -| Template | Purpose -| `.monitoring-alerts` | All cluster alerts for monitoring data. -| `.monitoring-beats` | All Beats monitoring data. -| `.monitoring-es` | All {es} monitoring data. -| `.monitoring-kibana` | All {kib} monitoring data. -| `.monitoring-logstash` | All Logstash monitoring data. +| Template | Purpose +| `.monitoring-alerts` | All cluster alerts for monitoring data. +| `.monitoring-beats` | All Beats monitoring data. +| `.monitoring-es` | All {es} monitoring data. +| `.monitoring-kibana` | All {kib} monitoring data. +| `.monitoring-logstash` | All Logstash monitoring data. |======================= The templates are ordinary {es} templates that control the default settings and diff --git a/x-pack/docs/en/security/configuring-es.asciidoc b/x-pack/docs/en/security/configuring-es.asciidoc index 47d580491c13..5fd9ed610cb3 100644 --- a/x-pack/docs/en/security/configuring-es.asciidoc +++ b/x-pack/docs/en/security/configuring-es.asciidoc @@ -55,8 +55,8 @@ help you get up and running. The +elasticsearch-setup-passwords+ command is the simplest method to set the built-in users' passwords for the first time. For example, you can run the command in an "interactive" mode, which prompts you -to enter new passwords for the `elastic`, `kibana`, `beats_system`, and -`logstash_system` users: +to enter new passwords for the `elastic`, `kibana`, `beats_system`, +`logstash_system`, and `apm_system` users: [source,shell] -------------------------------------------------- diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/esnative/ClientReservedRealm.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/esnative/ClientReservedRealm.java index c9868f448b40..5a228133073e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/esnative/ClientReservedRealm.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/esnative/ClientReservedRealm.java @@ -19,6 +19,7 @@ public static boolean isReserved(String username, Settings settings) { case UsernamesField.KIBANA_NAME: case UsernamesField.LOGSTASH_NAME: case UsernamesField.BEATS_NAME: + case UsernamesField.APM_NAME: return XPackSettings.RESERVED_REALM_ENABLED_SETTING.get(settings); default: return AnonymousUser.isAnonymousUsername(username, settings); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 0c5934363655..22cb1c357c66 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -112,6 +112,8 @@ private static Map initializeReservedRoles() { null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put(UsernamesField.BEATS_ROLE, new RoleDescriptor(UsernamesField.BEATS_ROLE, new String[] { "monitor", MonitoringBulkAction.NAME}, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) + .put(UsernamesField.APM_ROLE, new RoleDescriptor(UsernamesField.APM_ROLE, + new String[] { "monitor", MonitoringBulkAction.NAME}, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put("machine_learning_user", new RoleDescriptor("machine_learning_user", new String[] { "monitor_ml" }, new RoleDescriptor.IndicesPrivileges[] { RoleDescriptor.IndicesPrivileges.builder().indices(".ml-anomalies*", ".ml-notifications").privileges("view_index_metadata", "read").build() }, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java new file mode 100644 index 000000000000..48a72be5c1a8 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java @@ -0,0 +1,25 @@ +/* + * 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.security.user; + +import org.elasticsearch.Version; +import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; + +/** + * Built in user for APM server internals. Currently used for APM server monitoring. + */ +public class APMSystemUser extends User { + + public static final String NAME = UsernamesField.APM_NAME; + public static final String ROLE_NAME = UsernamesField.APM_ROLE; + public static final Version DEFINED_SINCE = Version.V_6_5_0; + public static final BuiltinUserInfo USER_INFO = new BuiltinUserInfo(NAME, ROLE_NAME, DEFINED_SINCE); + + public APMSystemUser(boolean enabled) { + super(NAME, new String[]{ ROLE_NAME }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA, enabled); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java index 3b691b927b4a..bd886567ed1b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java @@ -20,6 +20,8 @@ public final class UsernamesField { public static final String LOGSTASH_ROLE = "logstash_system"; public static final String BEATS_NAME = "beats_system"; public static final String BEATS_ROLE = "beats_system"; + public static final String APM_NAME = "apm_system"; + public static final String APM_ROLE = "apm_system"; private UsernamesField() {} } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 9cb5e25c5b8d..9972fc7b74bc 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -94,6 +94,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.LogstashSystemUser; import org.elasticsearch.xpack.core.security.user.SystemUser; @@ -147,6 +148,7 @@ public void testIsReserved() { assertThat(ReservedRolesStore.isReserved(XPackUser.ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved(LogstashSystemUser.ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved(BeatsSystemUser.ROLE_NAME), is(true)); + assertThat(ReservedRolesStore.isReserved(APMSystemUser.ROLE_NAME), is(true)); } public void testIngestAdminRole() { @@ -628,6 +630,30 @@ public void testBeatsSystemRole() { is(false)); } + public void testAPMSystemRole() { + final TransportRequest request = mock(TransportRequest.class); + + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor(APMSystemUser.ROLE_NAME); + assertNotNull(roleDescriptor); + assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); + + Role APMSystemRole = Role.builder(roleDescriptor, null).build(); + assertThat(APMSystemRole.cluster().check(ClusterHealthAction.NAME, request), is(true)); + assertThat(APMSystemRole.cluster().check(ClusterStateAction.NAME, request), is(true)); + assertThat(APMSystemRole.cluster().check(ClusterStatsAction.NAME, request), is(true)); + assertThat(APMSystemRole.cluster().check(PutIndexTemplateAction.NAME, request), is(false)); + assertThat(APMSystemRole.cluster().check(ClusterRerouteAction.NAME, request), is(false)); + assertThat(APMSystemRole.cluster().check(ClusterUpdateSettingsAction.NAME, request), is(false)); + assertThat(APMSystemRole.cluster().check(MonitoringBulkAction.NAME, request), is(true)); + + assertThat(APMSystemRole.runAs().check(randomAlphaOfLengthBetween(1, 30)), is(false)); + + assertThat(APMSystemRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); + assertThat(APMSystemRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(".reporting"), is(false)); + assertThat(APMSystemRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), + is(false)); + } + public void testMachineLearningAdminRole() { final TransportRequest request = mock(TransportRequest.class); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 0b8dbd023355..c3651224c49a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -24,6 +24,7 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.support.Exceptions; +import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; @@ -149,6 +150,8 @@ private User getUser(String username, ReservedUserInfo userInfo) { return new LogstashSystemUser(userInfo.enabled); case BeatsSystemUser.NAME: return new BeatsSystemUser(userInfo.enabled); + case APMSystemUser.NAME: + return new APMSystemUser(userInfo.enabled); default: if (anonymousEnabled && anonymousUser.principal().equals(username)) { return anonymousUser; @@ -177,6 +180,9 @@ public void users(ActionListener> listener) { userInfo = reservedUserInfos.get(BeatsSystemUser.NAME); users.add(new BeatsSystemUser(userInfo == null || userInfo.enabled)); + userInfo = reservedUserInfos.get(APMSystemUser.NAME); + users.add(new APMSystemUser(userInfo == null || userInfo.enabled)); + if (anonymousEnabled) { users.add(anonymousUser); } @@ -228,6 +234,8 @@ private Version getDefinedVersion(String username) { switch (username) { case BeatsSystemUser.NAME: return BeatsSystemUser.DEFINED_SINCE; + case APMSystemUser.NAME: + return APMSystemUser.DEFINED_SINCE; default: return Version.V_6_0_0; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java index 336acbdb1817..fad10c821c85 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordTool.java @@ -27,6 +27,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.support.Validation; +import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; @@ -63,7 +64,8 @@ public class SetupPasswordTool extends LoggingAwareMultiCommand { private static final char[] CHARS = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").toCharArray(); - public static final List USERS = asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME); + public static final List USERS = asList(ElasticUser.NAME, APMSystemUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, + BeatsSystemUser.NAME); private final BiFunction clientFunction; private final CheckedFunction keyStoreFunction; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/NativeRealmIntegTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/NativeRealmIntegTestCase.java index af5b73d889dc..63a38b12a9e1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/NativeRealmIntegTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/NativeRealmIntegTestCase.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.client.SecurityClient; +import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; @@ -88,7 +89,7 @@ public void setupReservedPasswords(RestClient restClient) throws IOException { RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder(); optionsBuilder.addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(ElasticUser.NAME, reservedPassword)); RequestOptions options = optionsBuilder.build(); - for (String username : Arrays.asList(KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME)) { + for (String username : Arrays.asList(KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, APMSystemUser.NAME)) { Request request = new Request("PUT", "/_xpack/security/user/" + username + "/_password"); request.setJsonEntity("{\"password\": \"" + new String(reservedPassword.getChars()) + "\"}"); request.setOptions(options); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java index c7a7c4f07bb6..243d2d981b21 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; @@ -81,7 +82,8 @@ void doExecute(Action action, Request request, ActionListener future = new PlainActionFuture<>(); nativeUsersStore.setEnabled(user, true, WriteRequest.RefreshPolicy.IMMEDIATE, future); @@ -99,7 +101,8 @@ public void testPasswordUpsertWhenSetEnabledOnReservedUser() throws Exception { public void testBlankPasswordInIndexImpliesDefaultPassword() throws Exception { final NativeUsersStore nativeUsersStore = startNativeUsersStore(); - final String user = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME); + final String user = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, + BeatsSystemUser.NAME, APMSystemUser.NAME); final Map values = new HashMap<>(); values.put(ENABLED_FIELD, Boolean.TRUE); values.put(PASSWORD_FIELD, BLANK_PASSWORD); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java index 1824597a6adc..8f7116dd9718 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmIntegTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.xpack.core.security.action.user.ChangePasswordResponse; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.client.SecurityClient; +import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; @@ -20,6 +21,7 @@ import org.junit.BeforeClass; import java.util.Arrays; +import java.util.List; import static java.util.Collections.singletonMap; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; @@ -49,7 +51,9 @@ public Settings nodeSettings(int nodeOrdinal) { } public void testAuthenticate() { - for (String username : Arrays.asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME)) { + final List usernames = Arrays.asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, + BeatsSystemUser.NAME, APMSystemUser.NAME); + for (String username : usernames) { ClusterHealthResponse response = client() .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, getReservedPassword()))) .admin() @@ -67,7 +71,9 @@ public void testAuthenticate() { */ public void testAuthenticateAfterEnablingUser() { final SecurityClient c = securityClient(); - for (String username : Arrays.asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME)) { + final List usernames = Arrays.asList(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, + BeatsSystemUser.NAME, APMSystemUser.NAME); + for (String username : usernames) { c.prepareSetEnabled(username, true).get(); ClusterHealthResponse response = client() .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(username, getReservedPassword()))) @@ -81,7 +87,8 @@ public void testAuthenticateAfterEnablingUser() { } public void testChangingPassword() { - String username = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME); + String username = randomFrom(ElasticUser.NAME, KibanaUser.NAME, LogstashSystemUser.NAME, + BeatsSystemUser.NAME, APMSystemUser.NAME); final char[] newPassword = "supersecretvalue".toCharArray(); if (randomBoolean()) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index 39d518a73f3b..a56db450ab89 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; @@ -262,7 +263,8 @@ public void testGetUsers() { PlainActionFuture> userFuture = new PlainActionFuture<>(); reservedRealm.users(userFuture); assertThat(userFuture.actionGet(), - containsInAnyOrder(new ElasticUser(true), new KibanaUser(true), new LogstashSystemUser(true), new BeatsSystemUser(true))); + containsInAnyOrder(new ElasticUser(true), new KibanaUser(true), new LogstashSystemUser(true), + new BeatsSystemUser(true), new APMSystemUser((true)))); } public void testGetUsersDisabled() { @@ -394,7 +396,7 @@ public void testNonElasticUsersCannotUseBootstrapPasswordWhenSecurityIndexExists new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); PlainActionFuture listener = new PlainActionFuture<>(); - final String principal = randomFrom(KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME); + final String principal = randomFrom(KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, APMSystemUser.NAME); doAnswer((i) -> { ActionListener callback = (ActionListener) i.getArguments()[1]; callback.onResponse(null); @@ -416,14 +418,15 @@ public void testNonElasticUsersCannotUseBootstrapPasswordWhenSecurityIndexDoesNo new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); PlainActionFuture listener = new PlainActionFuture<>(); - final String principal = randomFrom(KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME); + final String principal = randomFrom(KibanaUser.NAME, LogstashSystemUser.NAME, BeatsSystemUser.NAME, APMSystemUser.NAME); reservedRealm.doAuthenticate(new UsernamePasswordToken(principal, mockSecureSettings.getString("bootstrap.password")), listener); final AuthenticationResult result = listener.get(); assertThat(result.getStatus(), is(AuthenticationResult.Status.TERMINATE)); } private User randomReservedUser(boolean enabled) { - return randomFrom(new ElasticUser(enabled), new KibanaUser(enabled), new LogstashSystemUser(enabled), new BeatsSystemUser(enabled)); + return randomFrom(new ElasticUser(enabled), new KibanaUser(enabled), new LogstashSystemUser(enabled), + new BeatsSystemUser(enabled), new APMSystemUser(enabled)); } /* @@ -452,6 +455,11 @@ private void verifyVersionPredicate(String principal, Predicate version assertThat(versionPredicate.test(Version.V_6_2_3), is(false)); assertThat(versionPredicate.test(Version.V_6_3_0), is(true)); break; + case APMSystemUser.NAME: + assertThat(versionPredicate.test(Version.V_5_6_9), is(false)); + assertThat(versionPredicate.test(Version.V_6_4_0), is(false)); + assertThat(versionPredicate.test(Version.V_6_5_0), is(true)); + break; default: assertThat(versionPredicate.test(Version.V_6_3_0), is(true)); break; diff --git a/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java b/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java index 7b5e0dc40d10..860c30c0ddd5 100644 --- a/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java +++ b/x-pack/qa/security-setup-password-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolIT.java @@ -98,7 +98,7 @@ public void testSetupPasswordToolAutoSetup() throws Exception { } }); - assertEquals(4, userPasswordMap.size()); + assertEquals(5, userPasswordMap.size()); userPasswordMap.entrySet().forEach(entry -> { final String basicHeader = "Basic " + Base64.getEncoder().encodeToString((entry.getKey() + ":" + entry.getValue()).getBytes(StandardCharsets.UTF_8)); From f7a9186372edcd9da31a89ac0565d53874db1839 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 27 Aug 2018 15:08:27 +0200 Subject: [PATCH 166/283] SECURITY: Fix Compile Error in ReservedRealmTests (#33166) * This was broken by #32515 since the 5.x versions were removed between PR creation and merge --- .../xpack/security/authc/esnative/ReservedRealmTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index a56db450ab89..dffbe6b3eaa4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -456,7 +456,6 @@ private void verifyVersionPredicate(String principal, Predicate version assertThat(versionPredicate.test(Version.V_6_3_0), is(true)); break; case APMSystemUser.NAME: - assertThat(versionPredicate.test(Version.V_5_6_9), is(false)); assertThat(versionPredicate.test(Version.V_6_4_0), is(false)); assertThat(versionPredicate.test(Version.V_6_5_0), is(true)); break; From c41c614527c70e54245df848ab17e23359837566 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 27 Aug 2018 10:16:57 -0400 Subject: [PATCH 167/283] Fix grammar in contributing docs This commit fixes an instance of odd comma placement in the contributing docs. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4285c8fd20c0..ba3e3c1175b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,7 +95,7 @@ Contributing to the Elasticsearch codebase JDK 10 is required to build Elasticsearch. You must have a JDK 10 installation with the environment variable `JAVA_HOME` referencing the path to Java home for your JDK 10 installation. By default, tests use the same runtime as `JAVA_HOME`. -However, since Elasticsearch, supports JDK 8 the build supports compiling with +However, since Elasticsearch supports JDK 8, the build supports compiling with JDK 10 and testing on a JDK 8 runtime; to do this, set `RUNTIME_JAVA_HOME` pointing to the Java home of a JDK 8 installation. Note that this mechanism can be used to test against other JDKs as well, this is not only limited to JDK 8. From 3d9ca4baee4f537f6b40aa29729f171899b6ea00 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Mon, 27 Aug 2018 17:56:28 +0300 Subject: [PATCH 168/283] SQL: Enable aggregations to create a separate bucket for missing values (#32832) Enable aggregations to create a separate bucket for missing values. --- .../sql/querydsl/agg/GroupByColumnKey.java | 3 +- .../sql/querydsl/agg/GroupByDateKey.java | 3 +- .../sql/querydsl/agg/GroupByScriptKey.java | 3 +- .../xpack/qa/sql/jdbc/DataLoader.java | 20 ++-- .../xpack/qa/sql/jdbc/SqlSpecTestCase.java | 6 +- .../sql/src/main/resources/agg_nulls.sql-spec | 14 +++ .../qa/sql/src/main/resources/alias.csv-spec | 7 +- .../main/resources/employees_with_nulls.csv | 101 ++++++++++++++++++ .../resources/setup_test_emp_with_nulls.sql | 12 +++ 9 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 x-pack/qa/sql/src/main/resources/agg_nulls.sql-spec create mode 100644 x-pack/qa/sql/src/main/resources/employees_with_nulls.csv create mode 100644 x-pack/qa/sql/src/main/resources/setup_test_emp_with_nulls.sql diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByColumnKey.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByColumnKey.java index e98770318d21..931eaee64647 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByColumnKey.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByColumnKey.java @@ -25,7 +25,8 @@ public GroupByColumnKey(String id, String fieldName, Direction direction) { public TermsValuesSourceBuilder asValueSource() { return new TermsValuesSourceBuilder(id()) .field(fieldName()) - .order(direction().asOrder()); + .order(direction().asOrder()) + .missingBucket(true); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByDateKey.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByDateKey.java index 43c80e75057e..61c00c706eef 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByDateKey.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByDateKey.java @@ -44,7 +44,8 @@ public DateHistogramValuesSourceBuilder asValueSource() { return new DateHistogramValuesSourceBuilder(id()) .field(fieldName()) .dateHistogramInterval(new DateHistogramInterval(interval)) - .timeZone(DateTimeZone.forTimeZone(timeZone)); + .timeZone(DateTimeZone.forTimeZone(timeZone)) + .missingBucket(true); } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByScriptKey.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByScriptKey.java index a4af765d034b..ccd2bf934ab6 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByScriptKey.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/GroupByScriptKey.java @@ -36,7 +36,8 @@ public ScriptTemplate script() { public TermsValuesSourceBuilder asValueSource() { TermsValuesSourceBuilder builder = new TermsValuesSourceBuilder(id()) .script(script.toPainless()) - .order(direction().asOrder()); + .order(direction().asOrder()) + .missingBucket(true); if (script.outputType().isNumeric()) { builder.valueType(ValueType.NUMBER); diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/DataLoader.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/DataLoader.java index 05140577bcdf..22ba2a1037d9 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/DataLoader.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/DataLoader.java @@ -42,14 +42,15 @@ protected static void loadDatasetIntoEs(RestClient client) throws Exception { } protected static void loadEmpDatasetIntoEs(RestClient client) throws Exception { - loadEmpDatasetIntoEs(client, "test_emp"); - loadEmpDatasetIntoEs(client, "test_emp_copy"); + loadEmpDatasetIntoEs(client, "test_emp", "employees"); + loadEmpDatasetIntoEs(client, "test_emp_copy", "employees"); + loadEmpDatasetIntoEs(client, "test_emp_with_nulls", "employees_with_nulls"); makeAlias(client, "test_alias", "test_emp", "test_emp_copy"); makeAlias(client, "test_alias_emp", "test_emp", "test_emp_copy"); } public static void loadDocsDatasetIntoEs(RestClient client) throws Exception { - loadEmpDatasetIntoEs(client, "emp"); + loadEmpDatasetIntoEs(client, "emp", "employees"); loadLibDatasetIntoEs(client, "library"); makeAlias(client, "employees", "emp"); } @@ -62,7 +63,7 @@ private static void createString(String name, XContentBuilder builder) throws Ex .endObject(); } - protected static void loadEmpDatasetIntoEs(RestClient client, String index) throws Exception { + protected static void loadEmpDatasetIntoEs(RestClient client, String index, String fileName) throws Exception { Request request = new Request("PUT", "/" + index); XContentBuilder createIndex = JsonXContent.contentBuilder().startObject(); createIndex.startObject("settings"); @@ -129,15 +130,18 @@ protected static void loadEmpDatasetIntoEs(RestClient client, String index) thro request = new Request("POST", "/" + index + "/emp/_bulk"); request.addParameter("refresh", "true"); StringBuilder bulk = new StringBuilder(); - csvToLines("employees", (titles, fields) -> { + csvToLines(fileName, (titles, fields) -> { bulk.append("{\"index\":{}}\n"); bulk.append('{'); String emp_no = fields.get(1); for (int f = 0; f < fields.size(); f++) { - if (f != 0) { - bulk.append(','); + // an empty value in the csv file is treated as 'null', thus skipping it in the bulk request + if (fields.get(f).trim().length() > 0) { + if (f != 0) { + bulk.append(','); + } + bulk.append('"').append(titles.get(f)).append("\":\"").append(fields.get(f)).append('"'); } - bulk.append('"').append(titles.get(f)).append("\":\"").append(fields.get(f)).append('"'); } // append department List> list = dep_emp.get(emp_no); diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java index 4d90c9cce502..b77820fc77e7 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SqlSpecTestCase.java @@ -25,7 +25,10 @@ public abstract class SqlSpecTestCase extends SpecBaseIntegrationTestCase { private String query; @ClassRule - public static LocalH2 H2 = new LocalH2((c) -> c.createStatement().execute("RUNSCRIPT FROM 'classpath:/setup_test_emp.sql'")); + public static LocalH2 H2 = new LocalH2((c) -> { + c.createStatement().execute("RUNSCRIPT FROM 'classpath:/setup_test_emp.sql'"); + c.createStatement().execute("RUNSCRIPT FROM 'classpath:/setup_test_emp_with_nulls.sql'"); + }); @ParametersFactory(argumentFormatting = PARAM_FORMATTING) public static List readScriptSpec() throws Exception { @@ -39,6 +42,7 @@ public static List readScriptSpec() throws Exception { tests.addAll(readScriptSpec("/arithmetic.sql-spec", parser)); tests.addAll(readScriptSpec("/string-functions.sql-spec", parser)); tests.addAll(readScriptSpec("/case-functions.sql-spec", parser)); + tests.addAll(readScriptSpec("/agg_nulls.sql-spec", parser)); return tests; } diff --git a/x-pack/qa/sql/src/main/resources/agg_nulls.sql-spec b/x-pack/qa/sql/src/main/resources/agg_nulls.sql-spec new file mode 100644 index 000000000000..17fbb70a40bc --- /dev/null +++ b/x-pack/qa/sql/src/main/resources/agg_nulls.sql-spec @@ -0,0 +1,14 @@ +selectGenderWithNullsAndGroupByGender +SELECT gender, COUNT(*) count FROM test_emp_with_nulls GROUP BY gender ORDER BY gender; +selectFirstNameWithNullsAndGroupByFirstName +SELECT first_name FROM test_emp_with_nulls GROUP BY first_name ORDER BY first_name; +selectCountWhereIsNull +SELECT COUNT(*) count FROM test_emp_with_nulls WHERE first_name IS NULL; +selectLanguagesCountWithNullsAndGroupByLanguage +SELECT languages l, COUNT(*) c FROM test_emp_with_nulls GROUP BY languages ORDER BY languages; +selectHireDateGroupByHireDate +SELECT hire_date HD, COUNT(*) c FROM test_emp_with_nulls GROUP BY hire_date ORDER BY hire_date DESC; +selectHireDateGroupByHireDate +SELECT hire_date HD, COUNT(*) c FROM test_emp_with_nulls GROUP BY hire_date ORDER BY hire_date DESC; +selectSalaryGroupBySalary +SELECT salary, COUNT(*) c FROM test_emp_with_nulls GROUP BY salary ORDER BY salary DESC; \ No newline at end of file diff --git a/x-pack/qa/sql/src/main/resources/alias.csv-spec b/x-pack/qa/sql/src/main/resources/alias.csv-spec index 839d2cba7945..f1fa900706a7 100644 --- a/x-pack/qa/sql/src/main/resources/alias.csv-spec +++ b/x-pack/qa/sql/src/main/resources/alias.csv-spec @@ -86,6 +86,7 @@ test_alias | ALIAS test_alias_emp | ALIAS test_emp | BASE TABLE test_emp_copy | BASE TABLE +test_emp_with_nulls | BASE TABLE ; testGroupByOnAlias @@ -98,10 +99,10 @@ F | 10099.28 ; testGroupByOnPattern -SELECT gender, PERCENTILE(emp_no, 97) p1 FROM test_* GROUP BY gender; +SELECT gender, PERCENTILE(emp_no, 97) p1 FROM test_* WHERE gender is NOT NULL GROUP BY gender; gender:s | p1:d -F | 10099.28 -M | 10095.75 +F | 10099.32 +M | 10095.98 ; \ No newline at end of file diff --git a/x-pack/qa/sql/src/main/resources/employees_with_nulls.csv b/x-pack/qa/sql/src/main/resources/employees_with_nulls.csv new file mode 100644 index 000000000000..482da640470d --- /dev/null +++ b/x-pack/qa/sql/src/main/resources/employees_with_nulls.csv @@ -0,0 +1,101 @@ +birth_date,emp_no,first_name,gender,hire_date,languages,last_name,salary +1953-09-02T00:00:00Z,10001,Georgi,,1986-06-26T00:00:00Z,2,Facello,57305 +1964-06-02T00:00:00Z,10002,Bezalel,,1985-11-21T00:00:00Z,5,Simmel,56371 +1959-12-03T00:00:00Z,10003,Parto,,1986-08-28T00:00:00Z,4,Bamford,61805 +1954-05-01T00:00:00Z,10004,Chirstian,,1986-12-01T00:00:00Z,5,Koblick,36174 +1955-01-21T00:00:00Z,10005,Kyoichi,,1989-09-12T00:00:00Z,1,Maliniak,63528 +1953-04-20T00:00:00Z,10006,Anneke,,1989-06-02T00:00:00Z,3,Preusig,60335 +1957-05-23T00:00:00Z,10007,Tzvetan,,1989-02-10T00:00:00Z,4,Zielinski,74572 +1958-02-19T00:00:00Z,10008,Saniya,,1994-09-15T00:00:00Z,2,Kalloufi,43906 +1952-04-19T00:00:00Z,10009,Sumant,,1985-02-18T00:00:00Z,1,Peac,66174 +1963-06-01T00:00:00Z,10010,Duangkaew,,1989-08-24T00:00:00Z,4,Piveteau,45797 +1953-11-07T00:00:00Z,10011,Mary,F,1990-01-22T00:00:00Z,5,Sluis,31120 +1960-10-04T00:00:00Z,10012,Patricio,M,1992-12-18T00:00:00Z,5,Bridgland,48942 +1963-06-07T00:00:00Z,10013,Eberhardt,M,1985-10-20T00:00:00Z,1,Terkki,48735 +1956-02-12T00:00:00Z,10014,Berni,M,1987-03-11T00:00:00Z,5,Genin,37137 +1959-08-19T00:00:00Z,10015,Guoxiang,M,1987-07-02T00:00:00Z,5,Nooteboom,25324 +1961-05-02T00:00:00Z,10016,Kazuhito,M,1995-01-27T00:00:00Z,2,Cappelletti,61358 +1958-07-06T00:00:00Z,10017,Cristinel,F,1993-08-03T00:00:00Z,2,Bouloucos,58715 +1954-06-19T00:00:00Z,10018,Kazuhide,F,1993-08-03T00:00:00Z,2,Peha,56760 +1953-01-23T00:00:00Z,10019,Lillian,M,1993-08-03T00:00:00Z,1,Haddadi,73717 +1952-12-24T00:00:00Z,10020,,M,1991-01-26T00:00:00Z,3,Warwick,40031 +1960-02-20T00:00:00Z,10021,,M,1989-12-17T00:00:00Z,5,Erde,60408 +1952-07-08T00:00:00Z,10022,,M,1995-08-22T00:00:00Z,3,Famili,48233 +1953-09-29T00:00:00Z,10023,,F,1989-12-17T00:00:00Z,2,Montemayor,47896 +1958-09-05T00:00:00Z,10024,,F,1997-05-19T00:00:00Z,3,Pettey,64675 +1958-10-31T00:00:00Z,10025,Prasadram,M,1987-08-17T00:00:00Z,5,Heyers,47411 +1953-04-03T00:00:00Z,10026,Yongqiao,M,1995-03-20T00:00:00Z,3,Berztiss,28336 +1962-07-10T00:00:00Z,10027,Divier,F,1989-07-07T00:00:00Z,5,Reistad,73851 +1963-11-26T00:00:00Z,10028,Domenick,M,1991-10-22T00:00:00Z,1,Tempesti,39356 +1956-12-13T00:00:00Z,10029,Otmar,M,1985-11-20T00:00:00Z,,Herbst,74999 +1958-07-14T00:00:00Z,10030,Elvis,M,1994-02-17T00:00:00Z,,Demeyer,67492 +1959-01-27T00:00:00Z,10031,Karsten,M,1994-02-17T00:00:00Z,,Joslin,37716 +1960-08-09T00:00:00Z,10032,Jeong,F,1990-06-20T00:00:00Z,,Reistad,62233 +1956-11-14T00:00:00Z,10033,Arif,M,1987-03-18T00:00:00Z,,Merlo,70011 +1962-12-29T00:00:00Z,10034,Bader,M,1988-09-05T00:00:00Z,,Swan,39878 +1953-02-08T00:00:00Z,10035,Alain,M,1988-09-05T00:00:00Z,,Chappelet,25945 +1959-08-10T00:00:00Z,10036,Adamantios,M,1992-01-03T00:00:00Z,,Portugali,60781 +1963-07-22T00:00:00Z,10037,Pradeep,M,1990-12-05T00:00:00Z,,Makrucki,37691 +1960-07-20T00:00:00Z,10038,Huan,M,1989-09-20T00:00:00Z,,Lortz,35222 +1959-10-01T00:00:00Z,10039,Alejandro,M,1988-01-19T00:00:00Z,,Brender,36051 +1959-09-13T00:00:00Z,10040,Weiyi,F,1993-02-14T00:00:00Z,,Meriste,37112 +1959-08-27T00:00:00Z,10041,Uri,F,1989-11-12T00:00:00Z,1,Lenart,56415 +1956-02-26T00:00:00Z,10042,Magy,F,1993-03-21T00:00:00Z,3,Stamatiou,30404 +1960-09-19T00:00:00Z,10043,Yishay,M,1990-10-20T00:00:00Z,1,Tzvieli,34341 +1961-09-21T00:00:00Z,10044,Mingsen,F,1994-05-21T00:00:00Z,1,Casley,39728 +1957-08-14T00:00:00Z,10045,Moss,M,1989-09-02T00:00:00Z,3,Shanbhogue,74970 +1960-07-23T00:00:00Z,10046,Lucien,M,1992-06-20T00:00:00Z,4,Rosenbaum,50064 +1952-06-29T00:00:00Z,10047,Zvonko,M,1989-03-31T00:00:00Z,4,Nyanchama,42716 +1963-07-11T00:00:00Z,10048,Florian,M,1985-02-24T00:00:00Z,3,Syrotiuk,26436 +1961-04-24T00:00:00Z,10049,Basil,F,1992-05-04T00:00:00Z,5,Tramer,37853 +1958-05-21T00:00:00Z,10050,Yinghua,M,1990-12-25T00:00:00Z,2,Dredge,43026 +1953-07-28T00:00:00Z,10051,Hidefumi,M,1992-10-15T00:00:00Z,3,Caine,58121 +1961-02-26T00:00:00Z,10052,Heping,M,1988-05-21T00:00:00Z,1,Nitsch,55360 +1954-09-13T00:00:00Z,10053,Sanjiv,F,1986-02-04T00:00:00Z,3,Zschoche,54462 +1957-04-04T00:00:00Z,10054,Mayumi,M,1995-03-13T00:00:00Z,4,Schueller,65367 +1956-06-06T00:00:00Z,10055,Georgy,M,1992-04-27T00:00:00Z,5,Dredge,49281 +1961-09-01T00:00:00Z,10056,Brendon,F,1990-02-01T00:00:00Z,2,Bernini,33370 +1954-05-30T00:00:00Z,10057,Ebbe,F,1992-01-15T00:00:00Z,4,Callaway,27215 +1954-10-01T00:00:00Z,10058,Berhard,M,1987-04-13T00:00:00Z,3,McFarlin,38376 +1953-09-19T00:00:00Z,10059,Alejandro,F,1991-06-26T00:00:00Z,2,McAlpine,44307 +1961-10-15T00:00:00Z,10060,Breannda,M,1987-11-02T00:00:00Z,2,Billingsley,29175 +1962-10-19T00:00:00Z,10061,Tse,M,1985-09-17T00:00:00Z,1,Herber,49095 +1961-11-02T00:00:00Z,10062,Anoosh,M,1991-08-30T00:00:00Z,3,Peyn,65030 +1952-08-06T00:00:00Z,10063,Gino,F,1989-04-08T00:00:00Z,3,Leonhardt,52121 +1959-04-07T00:00:00Z,10064,Udi,M,1985-11-20T00:00:00Z,5,Jansch,33956 +1963-04-14T00:00:00Z,10065,Satosi,M,1988-05-18T00:00:00Z,2,Awdeh,50249 +1952-11-13T00:00:00Z,10066,Kwee,M,1986-02-26T00:00:00Z,5,Schusler,31897 +1953-01-07T00:00:00Z,10067,Claudi,M,1987-03-04T00:00:00Z,2,Stavenow,52044 +1962-11-26T00:00:00Z,10068,Charlene,M,1987-08-07T00:00:00Z,3,Brattka,28941 +1960-09-06T00:00:00Z,10069,Margareta,F,1989-11-05T00:00:00Z,5,Bierman,41933 +1955-08-20T00:00:00Z,10070,Reuven,M,1985-10-14T00:00:00Z,3,Garigliano,54329 +1958-01-21T00:00:00Z,10071,Hisao,M,1987-10-01T00:00:00Z,2,Lipner,40612 +1952-05-15T00:00:00Z,10072,Hironoby,F,1988-07-21T00:00:00Z,5,Sidou,54518 +1954-02-23T00:00:00Z,10073,Shir,M,1991-12-01T00:00:00Z,4,McClurg,32568 +1955-08-28T00:00:00Z,10074,Mokhtar,F,1990-08-13T00:00:00Z,5,Bernatsky,38992 +1960-03-09T00:00:00Z,10075,Gao,F,1987-03-19T00:00:00Z,5,Dolinsky,51956 +1952-06-13T00:00:00Z,10076,Erez,F,1985-07-09T00:00:00Z,3,Ritzmann,62405 +1964-04-18T00:00:00Z,10077,Mona,M,1990-03-02T00:00:00Z,5,Azuma,46595 +1959-12-25T00:00:00Z,10078,Danel,F,1987-05-26T00:00:00Z,2,Mondadori,69904 +1961-10-05T00:00:00Z,10079,Kshitij,F,1986-03-27T00:00:00Z,2,Gils,32263 +1957-12-03T00:00:00Z,10080,Premal,M,1985-11-19T00:00:00Z,5,Baek,52833 +1960-12-17T00:00:00Z,10081,Zhongwei,M,1986-10-30T00:00:00Z,2,Rosen,50128 +1963-09-09T00:00:00Z,10082,Parviz,M,1990-01-03T00:00:00Z,4,Lortz,49818 +1959-07-23T00:00:00Z,10083,Vishv,M,1987-03-31T00:00:00Z,1,Zockler, +1960-05-25T00:00:00Z,10084,Tuval,M,1995-12-15T00:00:00Z,1,Kalloufi, +1962-11-07T00:00:00Z,10085,Kenroku,M,1994-04-09T00:00:00Z,5,Malabarba, +1962-11-19T00:00:00Z,10086,Somnath,M,1990-02-16T00:00:00Z,1,Foote, +1959-07-23T00:00:00Z,10087,Xinglin,F,1986-09-08T00:00:00Z,5,Eugenio, +1954-02-25T00:00:00Z,10088,Jungsoon,F,1988-09-02T00:00:00Z,5,Syrzycki, +1963-03-21T00:00:00Z,10089,Sudharsan,F,1986-08-12T00:00:00Z,4,Flasterstein, +1961-05-30T00:00:00Z,10090,Kendra,M,1986-03-14T00:00:00Z,2,Hofting,44956 +1955-10-04T00:00:00Z,10091,Amabile,M,1992-11-18T00:00:00Z,3,Gomatam,38645 +1964-10-18T00:00:00Z,10092,Valdiodio,F,1989-09-22T00:00:00Z,1,Niizuma,25976 +1964-06-11T00:00:00Z,10093,Sailaja,M,1996-11-05T00:00:00Z,3,Desikan,45656 +1957-05-25T00:00:00Z,10094,Arumugam,F,1987-04-18T00:00:00Z,5,Ossenbruggen,66817 +1965-01-03T00:00:00Z,10095,Hilari,M,1986-07-15T00:00:00Z,4,Morton,37702 +1954-09-16T00:00:00Z,10096,Jayson,M,1990-01-14T00:00:00Z,4,Mandell,43889 +1952-02-27T00:00:00Z,10097,Remzi,M,1990-09-15T00:00:00Z,3,Waschkowski,71165 +1961-09-23T00:00:00Z,10098,Sreekrishna,F,1985-05-13T00:00:00Z,4,Servieres,44817 +1956-05-25T00:00:00Z,10099,Valter,F,1988-10-18T00:00:00Z,2,Sullins,73578 +1953-04-21T00:00:00Z,10100,Hironobu,F,1987-09-21T00:00:00Z,4,Haraldson,68431 diff --git a/x-pack/qa/sql/src/main/resources/setup_test_emp_with_nulls.sql b/x-pack/qa/sql/src/main/resources/setup_test_emp_with_nulls.sql new file mode 100644 index 000000000000..c6afaa9018aa --- /dev/null +++ b/x-pack/qa/sql/src/main/resources/setup_test_emp_with_nulls.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS "test_emp_with_nulls"; +CREATE TABLE "test_emp_with_nulls" ( + "birth_date" TIMESTAMP WITH TIME ZONE, + "emp_no" INT, + "first_name" VARCHAR(50), + "gender" VARCHAR(1), + "hire_date" TIMESTAMP WITH TIME ZONE, + "languages" TINYINT, + "last_name" VARCHAR(50), + "salary" INT + ) + AS SELECT * FROM CSVREAD('classpath:/employees_with_nulls.csv'); \ No newline at end of file From 2aef7e090048138316584b180c17ba86a2bf4702 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 27 Aug 2018 12:21:11 -0400 Subject: [PATCH 169/283] Introduce mapping version to index metadata (#33147) This commit introduces mapping version to index metadata. This value is monotonically increasing and is updated on mapping updates. This will be useful in cross-cluster replication so that we can request mapping updates from the leader only when there is a mapping update as opposed to the strategy we employ today which is to request a mapping update any time there is an index metadata update. As index metadata updates can occur for many reasons other than mapping updates, this leads to some unnecessary requests and work in cross-cluster replication. --- .../elasticsearch/cluster/ClusterState.java | 2 +- .../cluster/metadata/IndexMetaData.java | 52 ++++++++++++++++++- .../metadata/MetaDataMappingService.java | 14 ++++- .../org/elasticsearch/index/IndexService.java | 4 +- .../index/mapper/MapperService.java | 50 ++++++++++++++++-- .../cluster/IndicesClusterStateService.java | 6 +-- .../snapshots/RestoreService.java | 1 + .../metadata/MetaDataMappingServiceTests.java | 30 +++++++++++ .../gateway/MetaDataStateFormatTests.java | 1 + .../index/mapper/DynamicMappingTests.java | 10 ++++ .../index/mapper/UpdateMappingTests.java | 29 +++++++++++ ...actIndicesClusterStateServiceTestCase.java | 2 +- 12 files changed, 186 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java index 276e00a2ba3d..f7606d4bb061 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java @@ -284,7 +284,7 @@ public String toString() { final String TAB = " "; for (IndexMetaData indexMetaData : metaData) { sb.append(TAB).append(indexMetaData.getIndex()); - sb.append(": v[").append(indexMetaData.getVersion()).append("]\n"); + sb.append(": v[").append(indexMetaData.getVersion()).append("], mv[").append(indexMetaData.getMappingVersion()).append("]\n"); for (int shard = 0; shard < indexMetaData.getNumberOfShards(); shard++) { sb.append(TAB).append(TAB).append(shard).append(": "); sb.append("p_term [").append(indexMetaData.primaryTerm(shard)).append("], "); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index 18b89db72a39..31bf260e9013 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -24,6 +24,7 @@ import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; +import org.elasticsearch.Assertions; import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.rollover.RolloverInfo; import org.elasticsearch.action.support.ActiveShardCount; @@ -291,6 +292,7 @@ public Iterator> settings() { public static final String KEY_IN_SYNC_ALLOCATIONS = "in_sync_allocations"; static final String KEY_VERSION = "version"; + static final String KEY_MAPPING_VERSION = "mapping_version"; static final String KEY_ROUTING_NUM_SHARDS = "routing_num_shards"; static final String KEY_SETTINGS = "settings"; static final String KEY_STATE = "state"; @@ -309,6 +311,9 @@ public Iterator> settings() { private final Index index; private final long version; + + private final long mappingVersion; + private final long[] primaryTerms; private final State state; @@ -336,7 +341,7 @@ public Iterator> settings() { private final ActiveShardCount waitForActiveShards; private final ImmutableOpenMap rolloverInfos; - private IndexMetaData(Index index, long version, long[] primaryTerms, State state, int numberOfShards, int numberOfReplicas, Settings settings, + private IndexMetaData(Index index, long version, long mappingVersion, long[] primaryTerms, State state, int numberOfShards, int numberOfReplicas, Settings settings, ImmutableOpenMap mappings, ImmutableOpenMap aliases, ImmutableOpenMap customs, ImmutableOpenIntMap> inSyncAllocationIds, DiscoveryNodeFilters requireFilters, DiscoveryNodeFilters initialRecoveryFilters, DiscoveryNodeFilters includeFilters, DiscoveryNodeFilters excludeFilters, @@ -345,6 +350,8 @@ private IndexMetaData(Index index, long version, long[] primaryTerms, State stat this.index = index; this.version = version; + assert mappingVersion >= 0 : mappingVersion; + this.mappingVersion = mappingVersion; this.primaryTerms = primaryTerms; assert primaryTerms.length == numberOfShards; this.state = state; @@ -394,6 +401,9 @@ public long getVersion() { return this.version; } + public long getMappingVersion() { + return mappingVersion; + } /** * The term of the current selected primary. This is a non-negative number incremented when @@ -644,6 +654,7 @@ private static class IndexMetaDataDiff implements Diff { private final String index; private final int routingNumShards; private final long version; + private final long mappingVersion; private final long[] primaryTerms; private final State state; private final Settings settings; @@ -656,6 +667,7 @@ private static class IndexMetaDataDiff implements Diff { IndexMetaDataDiff(IndexMetaData before, IndexMetaData after) { index = after.index.getName(); version = after.version; + mappingVersion = after.mappingVersion; routingNumShards = after.routingNumShards; state = after.state; settings = after.settings; @@ -672,6 +684,11 @@ private static class IndexMetaDataDiff implements Diff { index = in.readString(); routingNumShards = in.readInt(); version = in.readLong(); + if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + mappingVersion = in.readVLong(); + } else { + mappingVersion = 1; + } state = State.fromId(in.readByte()); settings = Settings.readSettingsFromStream(in); primaryTerms = in.readVLongArray(); @@ -707,6 +724,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(index); out.writeInt(routingNumShards); out.writeLong(version); + if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + out.writeVLong(mappingVersion); + } out.writeByte(state.id); Settings.writeSettingsToStream(settings, out); out.writeVLongArray(primaryTerms); @@ -723,6 +743,7 @@ public void writeTo(StreamOutput out) throws IOException { public IndexMetaData apply(IndexMetaData part) { Builder builder = builder(index); builder.version(version); + builder.mappingVersion(mappingVersion); builder.setRoutingNumShards(routingNumShards); builder.state(state); builder.settings(settings); @@ -739,6 +760,11 @@ public IndexMetaData apply(IndexMetaData part) { public static IndexMetaData readFrom(StreamInput in) throws IOException { Builder builder = new Builder(in.readString()); builder.version(in.readLong()); + if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + builder.mappingVersion(in.readVLong()); + } else { + builder.mappingVersion(1); + } builder.setRoutingNumShards(in.readInt()); builder.state(State.fromId(in.readByte())); builder.settings(readSettingsFromStream(in)); @@ -778,6 +804,9 @@ public static IndexMetaData readFrom(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { out.writeString(index.getName()); // uuid will come as part of settings out.writeLong(version); + if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + out.writeVLong(mappingVersion); + } out.writeInt(routingNumShards); out.writeByte(state.id()); writeSettingsToStream(settings, out); @@ -821,6 +850,7 @@ public static class Builder { private String index; private State state = State.OPEN; private long version = 1; + private long mappingVersion = 1; private long[] primaryTerms = null; private Settings settings = Settings.Builder.EMPTY_SETTINGS; private final ImmutableOpenMap.Builder mappings; @@ -843,6 +873,7 @@ public Builder(IndexMetaData indexMetaData) { this.index = indexMetaData.getIndex().getName(); this.state = indexMetaData.state; this.version = indexMetaData.version; + this.mappingVersion = indexMetaData.mappingVersion; this.settings = indexMetaData.getSettings(); this.primaryTerms = indexMetaData.primaryTerms.clone(); this.mappings = ImmutableOpenMap.builder(indexMetaData.mappings); @@ -1009,6 +1040,15 @@ public Builder version(long version) { return this; } + public long mappingVersion() { + return mappingVersion; + } + + public Builder mappingVersion(final long mappingVersion) { + this.mappingVersion = mappingVersion; + return this; + } + /** * returns the primary term for the given shard. * See {@link IndexMetaData#primaryTerm(int)} for more information. @@ -1136,7 +1176,7 @@ public IndexMetaData build() { final String uuid = settings.get(SETTING_INDEX_UUID, INDEX_UUID_NA_VALUE); - return new IndexMetaData(new Index(index, uuid), version, primaryTerms, state, numberOfShards, numberOfReplicas, tmpSettings, mappings.build(), + return new IndexMetaData(new Index(index, uuid), version, mappingVersion, primaryTerms, state, numberOfShards, numberOfReplicas, tmpSettings, mappings.build(), tmpAliases.build(), customs.build(), filledInSyncAllocationIds.build(), requireFilters, initialRecoveryFilters, includeFilters, excludeFilters, indexCreatedVersion, indexUpgradedVersion, getRoutingNumShards(), routingPartitionSize, waitForActiveShards, rolloverInfos.build()); } @@ -1145,6 +1185,7 @@ public static void toXContent(IndexMetaData indexMetaData, XContentBuilder build builder.startObject(indexMetaData.getIndex().getName()); builder.field(KEY_VERSION, indexMetaData.getVersion()); + builder.field(KEY_MAPPING_VERSION, indexMetaData.getMappingVersion()); builder.field(KEY_ROUTING_NUM_SHARDS, indexMetaData.getRoutingNumShards()); builder.field(KEY_STATE, indexMetaData.getState().toString().toLowerCase(Locale.ENGLISH)); @@ -1218,6 +1259,7 @@ public static IndexMetaData fromXContent(XContentParser parser) throws IOExcepti if (token != XContentParser.Token.START_OBJECT) { throw new IllegalArgumentException("expected object but got a " + token); } + boolean mappingVersion = false; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); @@ -1316,6 +1358,9 @@ public static IndexMetaData fromXContent(XContentParser parser) throws IOExcepti builder.state(State.fromString(parser.text())); } else if (KEY_VERSION.equals(currentFieldName)) { builder.version(parser.longValue()); + } else if (KEY_MAPPING_VERSION.equals(currentFieldName)) { + mappingVersion = true; + builder.mappingVersion(parser.longValue()); } else if (KEY_ROUTING_NUM_SHARDS.equals(currentFieldName)) { builder.setRoutingNumShards(parser.intValue()); } else { @@ -1325,6 +1370,9 @@ public static IndexMetaData fromXContent(XContentParser parser) throws IOExcepti throw new IllegalArgumentException("Unexpected token " + token); } } + if (Assertions.ENABLED && Version.indexCreated(builder.settings).onOrAfter(Version.V_7_0_0_alpha1)) { + assert mappingVersion : "mapping version should be present for indices created on or after 7.0.0"; + } return builder.build(); } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java index 82d947b4158a..616fd13d1fad 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java @@ -287,6 +287,7 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt MetaData.Builder builder = MetaData.builder(metaData); boolean updated = false; for (IndexMetaData indexMetaData : updateList) { + boolean updatedMapping = false; // do the actual merge here on the master, and update the mapping source // we use the exact same indexService and metadata we used to validate above here to actually apply the update final Index index = indexMetaData.getIndex(); @@ -303,7 +304,7 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt if (existingSource.equals(updatedSource)) { // same source, no changes, ignore it } else { - updated = true; + updatedMapping = true; // use the merged mapping source if (logger.isDebugEnabled()) { logger.debug("{} update_mapping [{}] with source [{}]", index, mergedMapper.type(), updatedSource); @@ -313,7 +314,7 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt } } else { - updated = true; + updatedMapping = true; if (logger.isDebugEnabled()) { logger.debug("{} create_mapping [{}] with source [{}]", index, mappingType, updatedSource); } else if (logger.isInfoEnabled()) { @@ -329,7 +330,16 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt indexMetaDataBuilder.putMapping(new MappingMetaData(mapper.mappingSource())); } } + if (updatedMapping) { + indexMetaDataBuilder.mappingVersion(1 + indexMetaDataBuilder.mappingVersion()); + } + /* + * This implicitly increments the index metadata version and builds the index metadata. This means that we need to have + * already incremented the mapping version if necessary. Therefore, the mapping version increment must remain before this + * statement. + */ builder.put(indexMetaDataBuilder); + updated |= updatedMapping; } if (updated) { return ClusterState.builder(currentState).metaData(builder).build(); diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 5e9e811bc32e..6ffbc44676e0 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -522,8 +522,8 @@ List getSearchOperationListener() { // pkg private for } @Override - public boolean updateMapping(IndexMetaData indexMetaData) throws IOException { - return mapperService().updateMapping(indexMetaData); + public boolean updateMapping(final IndexMetaData currentIndexMetaData, final IndexMetaData newIndexMetaData) throws IOException { + return mapperService().updateMapping(currentIndexMetaData, newIndexMetaData); } private class StoreCloseListener implements Store.OnClose { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 15448bb4003d..5ebfc5bb51e7 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -25,6 +25,7 @@ import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.DelegatingAnalyzerWrapper; import org.apache.lucene.index.Term; +import org.elasticsearch.Assertions; import org.elasticsearch.ElasticsearchGenerationException; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -192,8 +193,8 @@ public static Map parseMapping(NamedXContentRegistry xContentReg /** * Update mapping by only merging the metadata that is different between received and stored entries */ - public boolean updateMapping(IndexMetaData indexMetaData) throws IOException { - assert indexMetaData.getIndex().equals(index()) : "index mismatch: expected " + index() + " but was " + indexMetaData.getIndex(); + public boolean updateMapping(final IndexMetaData currentIndexMetaData, final IndexMetaData newIndexMetaData) throws IOException { + assert newIndexMetaData.getIndex().equals(index()) : "index mismatch: expected " + index() + " but was " + newIndexMetaData.getIndex(); // go over and add the relevant mappings (or update them) Set existingMappers = new HashSet<>(); if (mapper != null) { @@ -205,7 +206,7 @@ public boolean updateMapping(IndexMetaData indexMetaData) throws IOException { final Map updatedEntries; try { // only update entries if needed - updatedEntries = internalMerge(indexMetaData, MergeReason.MAPPING_RECOVERY, true); + updatedEntries = internalMerge(newIndexMetaData, MergeReason.MAPPING_RECOVERY, true); } catch (Exception e) { logger.warn(() -> new ParameterizedMessage("[{}] failed to apply mappings", index()), e); throw e; @@ -213,9 +214,11 @@ public boolean updateMapping(IndexMetaData indexMetaData) throws IOException { boolean requireRefresh = false; + assertMappingVersion(currentIndexMetaData, newIndexMetaData, updatedEntries); + for (DocumentMapper documentMapper : updatedEntries.values()) { String mappingType = documentMapper.type(); - CompressedXContent incomingMappingSource = indexMetaData.mapping(mappingType).source(); + CompressedXContent incomingMappingSource = newIndexMetaData.mapping(mappingType).source(); String op = existingMappers.contains(mappingType) ? "updated" : "added"; if (logger.isDebugEnabled() && incomingMappingSource.compressed().length < 512) { @@ -240,6 +243,45 @@ public boolean updateMapping(IndexMetaData indexMetaData) throws IOException { return requireRefresh; } + private void assertMappingVersion( + final IndexMetaData currentIndexMetaData, + final IndexMetaData newIndexMetaData, + final Map updatedEntries) { + if (Assertions.ENABLED + && currentIndexMetaData != null + && currentIndexMetaData.getCreationVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (currentIndexMetaData.getMappingVersion() == newIndexMetaData.getMappingVersion()) { + // if the mapping version is unchanged, then there should not be any updates and all mappings should be the same + assert updatedEntries.isEmpty() : updatedEntries; + for (final ObjectCursor mapping : newIndexMetaData.getMappings().values()) { + final CompressedXContent currentSource = currentIndexMetaData.mapping(mapping.value.type()).source(); + final CompressedXContent newSource = mapping.value.source(); + assert currentSource.equals(newSource) : + "expected current mapping [" + currentSource + "] for type [" + mapping.value.type() + "] " + + "to be the same as new mapping [" + newSource + "]"; + } + } else { + // if the mapping version is changed, it should increase, there should be updates, and the mapping should be different + final long currentMappingVersion = currentIndexMetaData.getMappingVersion(); + final long newMappingVersion = newIndexMetaData.getMappingVersion(); + assert currentMappingVersion < newMappingVersion : + "expected current mapping version [" + currentMappingVersion + "] " + + "to be less than new mapping version [" + newMappingVersion + "]"; + assert updatedEntries.isEmpty() == false; + for (final DocumentMapper documentMapper : updatedEntries.values()) { + final MappingMetaData currentMapping = currentIndexMetaData.mapping(documentMapper.type()); + if (currentMapping != null) { + final CompressedXContent currentSource = currentMapping.source(); + final CompressedXContent newSource = documentMapper.mappingSource(); + assert currentSource.equals(newSource) == false : + "expected current mapping [" + currentSource + "] for type [" + documentMapper.type() + "] " + + "to be different than new mapping"; + } + } + } + } + } + public void merge(Map> mappings, MergeReason reason) { Map mappingSourcesCompressed = new LinkedHashMap<>(mappings.size()); for (Map.Entry> entry : mappings.entrySet()) { diff --git a/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java b/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java index e6a86d47f55c..692010119dc2 100644 --- a/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java +++ b/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java @@ -456,7 +456,7 @@ private void createIndices(final ClusterState state) { AllocatedIndex indexService = null; try { indexService = indicesService.createIndex(indexMetaData, buildInIndexListener); - if (indexService.updateMapping(indexMetaData) && sendRefreshMapping) { + if (indexService.updateMapping(null, indexMetaData) && sendRefreshMapping) { nodeMappingRefreshAction.nodeMappingRefresh(state.nodes().getMasterNode(), new NodeMappingRefreshAction.NodeMappingRefreshRequest(indexMetaData.getIndex().getName(), indexMetaData.getIndexUUID(), state.nodes().getLocalNodeId()) @@ -490,7 +490,7 @@ private void updateIndices(ClusterChangedEvent event) { if (ClusterChangedEvent.indexMetaDataChanged(currentIndexMetaData, newIndexMetaData)) { indexService.updateMetaData(newIndexMetaData); try { - if (indexService.updateMapping(newIndexMetaData) && sendRefreshMapping) { + if (indexService.updateMapping(currentIndexMetaData, newIndexMetaData) && sendRefreshMapping) { nodeMappingRefreshAction.nodeMappingRefresh(state.nodes().getMasterNode(), new NodeMappingRefreshAction.NodeMappingRefreshRequest(newIndexMetaData.getIndex().getName(), newIndexMetaData.getIndexUUID(), state.nodes().getLocalNodeId()) @@ -778,7 +778,7 @@ public interface AllocatedIndex extends Iterable, IndexCompo /** * Checks if index requires refresh from master. */ - boolean updateMapping(IndexMetaData indexMetaData) throws IOException; + boolean updateMapping(IndexMetaData currentIndexMetaData, IndexMetaData newIndexMetaData) throws IOException; /** * Returns shard with given id. diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java index a7df9bdfdfd8..702d63d0d940 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java @@ -292,6 +292,7 @@ public ClusterState execute(ClusterState currentState) { // Index exists and it's closed - open it in metadata and start recovery IndexMetaData.Builder indexMdBuilder = IndexMetaData.builder(snapshotIndexMetaData).state(IndexMetaData.State.OPEN); indexMdBuilder.version(Math.max(snapshotIndexMetaData.getVersion(), currentIndexMetaData.getVersion() + 1)); + indexMdBuilder.mappingVersion(Math.max(snapshotIndexMetaData.getMappingVersion(), currentIndexMetaData.getMappingVersion() + 1)); if (!request.includeAliases()) { // Remove all snapshot aliases if (!snapshotIndexMetaData.getAliases().isEmpty()) { diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java index 6cdca8d93a10..865059c33790 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java @@ -84,4 +84,34 @@ public void testClusterStateIsNotChangedWithIdenticalMappings() throws Exception assertSame(result, result2); } + public void testMappingVersion() throws Exception { + final IndexService indexService = createIndex("test", client().admin().indices().prepareCreate("test").addMapping("type")); + final long previousVersion = indexService.getMetaData().getMappingVersion(); + final MetaDataMappingService mappingService = getInstanceFromNode(MetaDataMappingService.class); + final ClusterService clusterService = getInstanceFromNode(ClusterService.class); + final PutMappingClusterStateUpdateRequest request = new PutMappingClusterStateUpdateRequest().type("type"); + request.indices(new Index[] {indexService.index()}); + request.source("{ \"properties\": { \"field\": { \"type\": \"text\" }}}"); + final ClusterStateTaskExecutor.ClusterTasksResult result = + mappingService.putMappingExecutor.execute(clusterService.state(), Collections.singletonList(request)); + assertThat(result.executionResults.size(), equalTo(1)); + assertTrue(result.executionResults.values().iterator().next().isSuccess()); + assertThat(result.resultingState.metaData().index("test").getMappingVersion(), equalTo(1 + previousVersion)); + } + + public void testMappingVersionUnchanged() throws Exception { + final IndexService indexService = createIndex("test", client().admin().indices().prepareCreate("test").addMapping("type")); + final long previousVersion = indexService.getMetaData().getMappingVersion(); + final MetaDataMappingService mappingService = getInstanceFromNode(MetaDataMappingService.class); + final ClusterService clusterService = getInstanceFromNode(ClusterService.class); + final PutMappingClusterStateUpdateRequest request = new PutMappingClusterStateUpdateRequest().type("type"); + request.indices(new Index[] {indexService.index()}); + request.source("{ \"properties\": {}}"); + final ClusterStateTaskExecutor.ClusterTasksResult result = + mappingService.putMappingExecutor.execute(clusterService.state(), Collections.singletonList(request)); + assertThat(result.executionResults.size(), equalTo(1)); + assertTrue(result.executionResults.values().iterator().next().isSuccess()); + assertThat(result.resultingState.metaData().index("test").getMappingVersion(), equalTo(previousVersion)); + } + } diff --git a/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java b/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java index d236d01f049d..0bf80e523987 100644 --- a/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java +++ b/server/src/test/java/org/elasticsearch/gateway/MetaDataStateFormatTests.java @@ -267,6 +267,7 @@ public void testLoadState() throws IOException { IndexMetaData deserialized = indices.get(original.getIndex().getName()); assertThat(deserialized, notNullValue()); assertThat(deserialized.getVersion(), equalTo(original.getVersion())); + assertThat(deserialized.getMappingVersion(), equalTo(original.getMappingVersion())); assertThat(deserialized.getNumberOfReplicas(), equalTo(original.getNumberOfReplicas())); assertThat(deserialized.getNumberOfShards(), equalTo(original.getNumberOfShards())); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java index 7d022b554544..cb2ed785699c 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; @@ -741,4 +742,13 @@ public void testDynamicTemplateOrder() throws IOException { client().prepareIndex("test", "type", "1").setSource("foo", "abc").get(); assertThat(index.mapperService().fullName("foo"), instanceOf(KeywordFieldMapper.KeywordFieldType.class)); } + + public void testMappingVersionAfterDynamicMappingUpdate() { + createIndex("test", client().admin().indices().prepareCreate("test").addMapping("type")); + final ClusterService clusterService = getInstanceFromNode(ClusterService.class); + final long previousVersion = clusterService.state().metaData().index("test").getMappingVersion(); + client().prepareIndex("test", "type", "1").setSource("field", "text").get(); + assertThat(clusterService.state().metaData().index("test").getMappingVersion(), equalTo(1 + previousVersion)); + } + } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/UpdateMappingTests.java b/server/src/test/java/org/elasticsearch/index/mapper/UpdateMappingTests.java index 3f8e8e9efec3..d8650331d232 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/UpdateMappingTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/UpdateMappingTests.java @@ -19,6 +19,8 @@ package org.elasticsearch.index.mapper; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; @@ -30,6 +32,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.InternalSettingsPlugin; +import org.hamcrest.Matchers; import java.io.IOException; import java.util.Collection; @@ -188,4 +191,30 @@ public void testRejectFieldDefinedTwice() throws IOException { () -> mapperService2.merge("type", new CompressedXContent(mapping1), MergeReason.MAPPING_UPDATE)); assertThat(e.getMessage(), equalTo("mapper [foo] of different type, current_type [long], merged_type [ObjectMapper]")); } + + public void testMappingVersion() { + createIndex("test", client().admin().indices().prepareCreate("test").addMapping("type")); + final ClusterService clusterService = getInstanceFromNode(ClusterService.class); + { + final long previousVersion = clusterService.state().metaData().index("test").getMappingVersion(); + final PutMappingRequest request = new PutMappingRequest(); + request.indices("test"); + request.type("type"); + request.source("field", "type=text"); + client().admin().indices().putMapping(request).actionGet(); + assertThat(clusterService.state().metaData().index("test").getMappingVersion(), Matchers.equalTo(1 + previousVersion)); + } + + { + final long previousVersion = clusterService.state().metaData().index("test").getMappingVersion(); + final PutMappingRequest request = new PutMappingRequest(); + request.indices("test"); + request.type("type"); + request.source("field", "type=text"); + client().admin().indices().putMapping(request).actionGet(); + // the version should be unchanged after putting the same mapping again + assertThat(clusterService.state().metaData().index("test").getMappingVersion(), Matchers.equalTo(previousVersion)); + } + } + } diff --git a/server/src/test/java/org/elasticsearch/indices/cluster/AbstractIndicesClusterStateServiceTestCase.java b/server/src/test/java/org/elasticsearch/indices/cluster/AbstractIndicesClusterStateServiceTestCase.java index 580696264bdd..c68e4870aaeb 100644 --- a/server/src/test/java/org/elasticsearch/indices/cluster/AbstractIndicesClusterStateServiceTestCase.java +++ b/server/src/test/java/org/elasticsearch/indices/cluster/AbstractIndicesClusterStateServiceTestCase.java @@ -273,7 +273,7 @@ public IndexSettings getIndexSettings() { } @Override - public boolean updateMapping(IndexMetaData indexMetaData) throws IOException { + public boolean updateMapping(final IndexMetaData currentIndexMetaData, final IndexMetaData newIndexMetaData) throws IOException { failRandomly(); return false; } From 309fb2218197b0469ec27d10eebcaac18729e910 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Mon, 27 Aug 2018 10:26:25 -0600 Subject: [PATCH 170/283] Build: forked compiler max memory matches jvmArgs (#33138) This commit removes the setting of the fork options maximum memory size in our build plugin and instead adds the value in the gradle.properties file to be alongside the value set in jvmArgs. This change is necessary when using parallel compilation as 512m is not sufficient for parallel compilation on some machines. --- .../src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy | 1 - gradle.properties | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index fb979a77dace..bce00ae8f6d3 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -601,7 +601,6 @@ class BuildPlugin implements Plugin { } else { options.fork = true options.forkOptions.javaHome = compilerJavaHomeFile - options.forkOptions.memoryMaximumSize = "512m" } if (targetCompatibilityVersion == JavaVersion.VERSION_1_8) { // compile with compact 3 profile by default diff --git a/gradle.properties b/gradle.properties index 08b03629ad53..6b04e99c2044 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ org.gradle.daemon=false org.gradle.jvmargs=-Xmx2g +options.forkOptions.memoryMaximumSize=2g From 5d9c2706085e6d68c98227fbe6df747892af3de6 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Mon, 27 Aug 2018 10:56:21 -0600 Subject: [PATCH 171/283] Token API supports the client_credentials grant (#33106) This change adds support for the client credentials grant type to the token api. The client credentials grant allows for a client to authenticate with the authorization server and obtain a token to access as itself. Per RFC 6749, a refresh token should not be included with the access token and as such a refresh token is not issued when the client credentials grant is used. The addition of the client credentials grant will allow users authenticated with mechanisms such as kerberos or PKI to obtain a token that can be used for subsequent access. --- .../en/rest-api/security/get-tokens.asciidoc | 57 +++-- .../action/token/CreateTokenRequest.java | 113 +++++++--- .../action/token/CreateTokenResponse.java | 30 ++- .../action/token/CreateTokenRequestTests.java | 18 +- .../token/CreateTokenResponseTests.java | 92 +++++++++ .../saml/TransportSamlAuthenticateAction.java | 2 +- .../token/TransportCreateTokenAction.java | 60 ++++-- .../xpack/security/authc/TokenService.java | 11 +- ...sportSamlInvalidateSessionActionTests.java | 2 +- .../saml/TransportSamlLogoutActionTests.java | 2 +- .../TransportCreateTokenActionTests.java | 195 ++++++++++++++++++ .../authc/AuthenticationServiceTests.java | 4 +- .../security/authc/TokenAuthIntegTests.java | 33 +++ .../security/authc/TokenServiceTests.java | 25 +-- x-pack/qa/rolling-upgrade/build.gradle | 4 + 15 files changed, 567 insertions(+), 81 deletions(-) rename x-pack/plugin/{security/src/test/java/org/elasticsearch/xpack => core/src/test/java/org/elasticsearch/xpack/core}/security/action/token/CreateTokenRequestTests.java (78%) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponseTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java diff --git a/x-pack/docs/en/rest-api/security/get-tokens.asciidoc b/x-pack/docs/en/rest-api/security/get-tokens.asciidoc index a2c4e6d7a37e..c80b4f60c6bc 100644 --- a/x-pack/docs/en/rest-api/security/get-tokens.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-tokens.asciidoc @@ -38,16 +38,19 @@ The following parameters can be specified in the body of a POST request and pertain to creating a token: `grant_type`:: -(string) The type of grant. Valid grant types are: `password` and `refresh_token`. +(string) The type of grant. Supported grant types are: `password`, +`client_credentials` and `refresh_token`. `password`:: (string) The user's password. If you specify the `password` grant type, this -parameter is required. +parameter is required. This parameter is not valid with any other supported +grant type. `refresh_token`:: (string) If you specify the `refresh_token` grant type, this parameter is required. It contains the string that was returned when you created the token -and enables you to extend its life. +and enables you to extend its life. This parameter is not valid with any other +supported grant type. `scope`:: (string) The scope of the token. Currently tokens are only issued for a scope of @@ -55,19 +58,19 @@ and enables you to extend its life. `username`:: (string) The username that identifies the user. If you specify the `password` -grant type, this parameter is required. +grant type, this parameter is required. This parameter is not valid with any +other supported grant type. ==== Examples -The following example obtains a token for the `test_admin` user: +The following example obtains a token using the `client_credentials` grant type, +which simply creates a token as the authenticated user: [source,js] -------------------------------------------------- POST /_xpack/security/oauth2/token { - "grant_type" : "password", - "username" : "test_admin", - "password" : "x-pack-test-password" + "grant_type" : "client_credentials" } -------------------------------------------------- // CONSOLE @@ -80,12 +83,10 @@ seconds) that the token expires in, and the type: { "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==", "type" : "Bearer", - "expires_in" : 1200, - "refresh_token": "vLBPvmAB6KvwvJZr27cS" + "expires_in" : 1200 } -------------------------------------------------- // TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/] -// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/] The token returned by this API can be used by sending a request with a `Authorization` header with a value having the prefix `Bearer ` followed @@ -97,9 +98,39 @@ curl -H "Authorization: Bearer dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb -------------------------------------------------- // NOTCONSOLE +The following example obtains a token for the `test_admin` user using the +`password` grant type: + +[source,js] +-------------------------------------------------- +POST /_xpack/security/oauth2/token +{ + "grant_type" : "password", + "username" : "test_admin", + "password" : "x-pack-test-password" +} +-------------------------------------------------- +// CONSOLE + +The following example output contains the access token, the amount of time (in +seconds) that the token expires in, the type, and the refresh token: + +[source,js] +-------------------------------------------------- +{ + "access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==", + "type" : "Bearer", + "expires_in" : 1200, + "refresh_token": "vLBPvmAB6KvwvJZr27cS" +} +-------------------------------------------------- +// TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/] +// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/] + [[security-api-refresh-token]] -To extend the life of an existing token, you can call the API again with the -refresh token within 24 hours of the token's creation. For example: +To extend the life of an existing token obtained using the `password` grant type, +you can call the API again with the refresh token within 24 hours of the token's +creation. For example: [source,js] -------------------------------------------------- diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java index fdb46711c0c5..4d57da06b921 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java @@ -19,6 +19,10 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import java.util.stream.Collectors; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -29,6 +33,37 @@ */ public final class CreateTokenRequest extends ActionRequest { + public enum GrantType { + PASSWORD("password"), + REFRESH_TOKEN("refresh_token"), + AUTHORIZATION_CODE("authorization_code"), + CLIENT_CREDENTIALS("client_credentials"); + + private final String value; + + GrantType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static GrantType fromString(String grantType) { + if (grantType != null) { + for (GrantType type : values()) { + if (type.getValue().equals(grantType)) { + return type; + } + } + } + return null; + } + } + + private static final Set SUPPORTED_GRANT_TYPES = Collections.unmodifiableSet( + EnumSet.of(GrantType.PASSWORD, GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS)); + private String grantType; private String username; private SecureString password; @@ -49,33 +84,58 @@ public CreateTokenRequest(String grantType, @Nullable String username, @Nullable @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; - if ("password".equals(grantType)) { - if (Strings.isNullOrEmpty(username)) { - validationException = addValidationError("username is missing", validationException); - } - if (password == null || password.getChars() == null || password.getChars().length == 0) { - validationException = addValidationError("password is missing", validationException); - } - if (refreshToken != null) { - validationException = - addValidationError("refresh_token is not supported with the password grant_type", validationException); - } - } else if ("refresh_token".equals(grantType)) { - if (username != null) { - validationException = - addValidationError("username is not supported with the refresh_token grant_type", validationException); - } - if (password != null) { - validationException = - addValidationError("password is not supported with the refresh_token grant_type", validationException); - } - if (refreshToken == null) { - validationException = addValidationError("refresh_token is missing", validationException); + GrantType type = GrantType.fromString(grantType); + if (type != null) { + switch (type) { + case PASSWORD: + if (Strings.isNullOrEmpty(username)) { + validationException = addValidationError("username is missing", validationException); + } + if (password == null || password.getChars() == null || password.getChars().length == 0) { + validationException = addValidationError("password is missing", validationException); + } + if (refreshToken != null) { + validationException = + addValidationError("refresh_token is not supported with the password grant_type", validationException); + } + break; + case REFRESH_TOKEN: + if (username != null) { + validationException = + addValidationError("username is not supported with the refresh_token grant_type", validationException); + } + if (password != null) { + validationException = + addValidationError("password is not supported with the refresh_token grant_type", validationException); + } + if (refreshToken == null) { + validationException = addValidationError("refresh_token is missing", validationException); + } + break; + case CLIENT_CREDENTIALS: + if (username != null) { + validationException = + addValidationError("username is not supported with the client_credentials grant_type", validationException); + } + if (password != null) { + validationException = + addValidationError("password is not supported with the client_credentials grant_type", validationException); + } + if (refreshToken != null) { + validationException = addValidationError("refresh_token is not supported with the client_credentials grant_type", + validationException); + } + break; + default: + validationException = addValidationError("grant_type only supports the values: [" + + SUPPORTED_GRANT_TYPES.stream().map(GrantType::getValue).collect(Collectors.joining(", ")) + "]", + validationException); } } else { - validationException = addValidationError("grant_type only supports the values: [password, refresh_token]", validationException); + validationException = addValidationError("grant_type only supports the values: [" + + SUPPORTED_GRANT_TYPES.stream().map(GrantType::getValue).collect(Collectors.joining(", ")) + "]", + validationException); } - return validationException; } @@ -126,6 +186,11 @@ public String getRefreshToken() { @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); + if (out.getVersion().before(Version.V_7_0_0_alpha1) && GrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) { + throw new IllegalArgumentException("a request with the client_credentials grant_type cannot be sent to version [" + + out.getVersion() + "]"); + } + out.writeString(grantType); if (out.getVersion().onOrAfter(Version.V_6_2_0)) { out.writeOptionalString(username); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java index 1cb1029e820e..439247356789 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java @@ -59,8 +59,14 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(tokenString); out.writeTimeValue(expiresIn); out.writeOptionalString(scope); - if (out.getVersion().onOrAfter(Version.V_6_2_0)) { - out.writeString(refreshToken); + if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { // TODO change to V_6_5_0 after backport + out.writeOptionalString(refreshToken); + } else if (out.getVersion().onOrAfter(Version.V_6_2_0)) { + if (refreshToken == null) { + out.writeString(""); + } else { + out.writeString(refreshToken); + } } } @@ -70,7 +76,9 @@ public void readFrom(StreamInput in) throws IOException { tokenString = in.readString(); expiresIn = in.readTimeValue(); scope = in.readOptionalString(); - if (in.getVersion().onOrAfter(Version.V_6_2_0)) { + if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { // TODO change to V_6_5_0 after backport + refreshToken = in.readOptionalString(); + } else if (in.getVersion().onOrAfter(Version.V_6_2_0)) { refreshToken = in.readString(); } } @@ -90,4 +98,20 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } return builder.endObject(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CreateTokenResponse that = (CreateTokenResponse) o; + return Objects.equals(tokenString, that.tokenString) && + Objects.equals(expiresIn, that.expiresIn) && + Objects.equals(scope, that.scope) && + Objects.equals(refreshToken, that.refreshToken); + } + + @Override + public int hashCode() { + return Objects.hash(tokenString, expiresIn, scope, refreshToken); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/CreateTokenRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequestTests.java similarity index 78% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/CreateTokenRequestTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequestTests.java index 440452632844..bd23198e8eae 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/CreateTokenRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequestTests.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.security.action.token; +package org.elasticsearch.xpack.core.security.action.token; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.settings.SecureString; @@ -20,7 +20,7 @@ public void testRequestValidation() { ActionRequestValidationException ve = request.validate(); assertNotNull(ve); assertEquals(1, ve.validationErrors().size()); - assertThat(ve.validationErrors().get(0), containsString("[password, refresh_token]")); + assertThat(ve.validationErrors().get(0), containsString("[password, refresh_token, client_credentials]")); assertThat(ve.validationErrors().get(0), containsString("grant_type")); request.setGrantType("password"); @@ -72,5 +72,19 @@ public void testRequestValidation() { assertNotNull(ve); assertEquals(1, ve.validationErrors().size()); assertThat(ve.validationErrors(), hasItem("refresh_token is missing")); + + request.setGrantType("client_credentials"); + ve = request.validate(); + assertNull(ve); + + request.setUsername(randomAlphaOfLengthBetween(1, 32)); + request.setPassword(new SecureString(randomAlphaOfLengthBetween(1, 32).toCharArray())); + request.setRefreshToken(randomAlphaOfLengthBetween(1, 32)); + ve = request.validate(); + assertNotNull(ve); + assertEquals(3, ve.validationErrors().size()); + assertThat(ve.validationErrors(), hasItem(containsString("username is not supported"))); + assertThat(ve.validationErrors(), hasItem(containsString("password is not supported"))); + assertThat(ve.validationErrors(), hasItem(containsString("refresh_token is not supported"))); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponseTests.java new file mode 100644 index 000000000000..b784310fdb2a --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponseTests.java @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.security.action.token; + +import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; + +public class CreateTokenResponseTests extends ESTestCase { + + public void testSerialization() throws Exception { + CreateTokenResponse response = new CreateTokenResponse(randomAlphaOfLengthBetween(1, 10), TimeValue.timeValueMinutes(20L), + randomBoolean() ? null : "FULL", randomAlphaOfLengthBetween(1, 10)); + try (BytesStreamOutput output = new BytesStreamOutput()) { + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + CreateTokenResponse serialized = new CreateTokenResponse(); + serialized.readFrom(input); + assertEquals(response, serialized); + } + } + + response = new CreateTokenResponse(randomAlphaOfLengthBetween(1, 10), TimeValue.timeValueMinutes(20L), + randomBoolean() ? null : "FULL", null); + try (BytesStreamOutput output = new BytesStreamOutput()) { + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + CreateTokenResponse serialized = new CreateTokenResponse(); + serialized.readFrom(input); + assertEquals(response, serialized); + } + } + } + + public void testSerializationToPre62Version() throws Exception { + CreateTokenResponse response = new CreateTokenResponse(randomAlphaOfLengthBetween(1, 10), TimeValue.timeValueMinutes(20L), + randomBoolean() ? null : "FULL", randomBoolean() ? null : randomAlphaOfLengthBetween(1, 10)); + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.V_6_1_4); + try (BytesStreamOutput output = new BytesStreamOutput()) { + output.setVersion(version); + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + input.setVersion(version); + CreateTokenResponse serialized = new CreateTokenResponse(); + serialized.readFrom(input); + assertNull(serialized.getRefreshToken()); + assertEquals(response.getTokenString(), serialized.getTokenString()); + assertEquals(response.getExpiresIn(), serialized.getExpiresIn()); + assertEquals(response.getScope(), serialized.getScope()); + } + } + } + + public void testSerializationToPost62Pre65Version() throws Exception { + CreateTokenResponse response = new CreateTokenResponse(randomAlphaOfLengthBetween(1, 10), TimeValue.timeValueMinutes(20L), + randomBoolean() ? null : "FULL", randomAlphaOfLengthBetween(1, 10)); + final Version version = VersionUtils.randomVersionBetween(random(), Version.V_6_2_0, Version.V_6_4_0); + try (BytesStreamOutput output = new BytesStreamOutput()) { + output.setVersion(version); + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + input.setVersion(version); + CreateTokenResponse serialized = new CreateTokenResponse(); + serialized.readFrom(input); + assertEquals(response, serialized); + } + } + + // no refresh token + response = new CreateTokenResponse(randomAlphaOfLengthBetween(1, 10), TimeValue.timeValueMinutes(20L), + randomBoolean() ? null : "FULL", null); + try (BytesStreamOutput output = new BytesStreamOutput()) { + output.setVersion(version); + response.writeTo(output); + try (StreamInput input = output.bytes().streamInput()) { + input.setVersion(version); + CreateTokenResponse serialized = new CreateTokenResponse(); + serialized.readFrom(input); + assertEquals("", serialized.getRefreshToken()); + assertEquals(response.getTokenString(), serialized.getTokenString()); + assertEquals(response.getExpiresIn(), serialized.getExpiresIn()); + assertEquals(response.getScope(), serialized.getScope()); + } + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java index d2507d51d0e8..9dd18be510f5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlAuthenticateAction.java @@ -61,7 +61,7 @@ protected void doExecute(Task task, SamlAuthenticateRequest request, ActionListe final TimeValue expiresIn = tokenService.getExpirationDelay(); listener.onResponse( new SamlAuthenticateResponse(authentication.getUser().principal(), tokenString, tuple.v2(), expiresIn)); - }, listener::onFailure), tokenMeta); + }, listener::onFailure), tokenMeta, true); }, e -> { logger.debug(() -> new ParameterizedMessage("SamlToken [{}] could not be authenticated", saml), e); listener.onFailure(e); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java index 358f6aee712d..23aaa9e0d992 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenAction.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import java.io.IOException; import java.util.Collections; /** @@ -48,29 +49,52 @@ public TransportCreateTokenAction(Settings settings, ThreadPool threadPool, Tran @Override protected void doExecute(Task task, CreateTokenRequest request, ActionListener listener) { + CreateTokenRequest.GrantType type = CreateTokenRequest.GrantType.fromString(request.getGrantType()); + assert type != null : "type should have been validated in the action"; + switch (type) { + case PASSWORD: + authenticateAndCreateToken(request, listener); + break; + case CLIENT_CREDENTIALS: + Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext()); + createToken(request, authentication, authentication, false, listener); + break; + default: + listener.onFailure(new IllegalStateException("grant_type [" + request.getGrantType() + + "] is not supported by the create token action")); + break; + } + } + + private void authenticateAndCreateToken(CreateTokenRequest request, ActionListener listener) { Authentication originatingAuthentication = Authentication.getAuthentication(threadPool.getThreadContext()); try (ThreadContext.StoredContext ignore = threadPool.getThreadContext().stashContext()) { final UsernamePasswordToken authToken = new UsernamePasswordToken(request.getUsername(), request.getPassword()); authenticationService.authenticate(CreateTokenAction.NAME, request, authToken, - ActionListener.wrap(authentication -> { - request.getPassword().close(); - tokenService.createUserToken(authentication, originatingAuthentication, ActionListener.wrap(tuple -> { - final String tokenStr = tokenService.getUserTokenString(tuple.v1()); - final String scope = getResponseScopeValue(request.getScope()); + ActionListener.wrap(authentication -> { + request.getPassword().close(); + createToken(request, authentication, originatingAuthentication, true, listener); + }, e -> { + // clear the request password + request.getPassword().close(); + listener.onFailure(e); + })); + } + } + + private void createToken(CreateTokenRequest request, Authentication authentication, Authentication originatingAuth, + boolean includeRefreshToken, ActionListener listener) { + try { + tokenService.createUserToken(authentication, originatingAuth, ActionListener.wrap(tuple -> { + final String tokenStr = tokenService.getUserTokenString(tuple.v1()); + final String scope = getResponseScopeValue(request.getScope()); - final CreateTokenResponse response = - new CreateTokenResponse(tokenStr, tokenService.getExpirationDelay(), scope, tuple.v2()); - listener.onResponse(response); - }, e -> { - // clear the request password - request.getPassword().close(); - listener.onFailure(e); - }), Collections.emptyMap()); - }, e -> { - // clear the request password - request.getPassword().close(); - listener.onFailure(e); - })); + final CreateTokenResponse response = + new CreateTokenResponse(tokenStr, tokenService.getExpirationDelay(), scope, tuple.v2()); + listener.onResponse(response); + }, listener::onFailure), Collections.emptyMap(), includeRefreshToken); + } catch (IOException e) { + listener.onFailure(e); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index 8b6dd8295d39..937bd22d9820 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -212,7 +212,8 @@ public static Boolean isTokenServiceEnabled(Settings settings) { * The created token will be stored in the security index. */ public void createUserToken(Authentication authentication, Authentication originatingClientAuth, - ActionListener> listener, Map metadata) throws IOException { + ActionListener> listener, Map metadata, + boolean includeRefreshToken) throws IOException { ensureEnabled(); if (authentication == null) { listener.onFailure(new IllegalArgumentException("authentication must be provided")); @@ -226,13 +227,14 @@ public void createUserToken(Authentication authentication, Authentication origin new Authentication(authentication.getUser(), authentication.getAuthenticatedBy(), authentication.getLookedUpBy(), version); final UserToken userToken = new UserToken(version, matchingVersionAuth, expiration, metadata); - final String refreshToken = UUIDs.randomBase64UUID(); + final String refreshToken = includeRefreshToken ? UUIDs.randomBase64UUID() : null; try (XContentBuilder builder = XContentFactory.jsonBuilder()) { builder.startObject(); builder.field("doc_type", "token"); builder.field("creation_time", created.toEpochMilli()); - builder.startObject("refresh_token") + if (includeRefreshToken) { + builder.startObject("refresh_token") .field("token", refreshToken) .field("invalidated", false) .field("refreshed", false) @@ -242,6 +244,7 @@ public void createUserToken(Authentication authentication, Authentication origin .field("realm", originatingClientAuth.getAuthenticatedBy().getName()) .endObject() .endObject(); + } builder.startObject("access_token") .field("invalidated", false) .field("user_token", userToken) @@ -734,7 +737,7 @@ private void innerRefresh(String tokenDocId, Authentication userAuth, ActionList .request(); executeAsyncWithOrigin(client.threadPool().getThreadContext(), SECURITY_ORIGIN, updateRequest, ActionListener.wrap( - updateResponse -> createUserToken(authentication, userAuth, listener, metadata), + updateResponse -> createUserToken(authentication, userAuth, listener, metadata, true), e -> { Throwable cause = ExceptionsHelper.unwrapCause(e); if (cause instanceof VersionConflictEngineException || diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java index 3371b901647c..81b0b1a72911 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java @@ -316,7 +316,7 @@ private Tuple storeToken(SamlNameId nameId, String session) t new RealmRef("native", NativeRealmSettings.TYPE, "node01"), null); final Map metadata = samlRealm.createTokenMetadata(nameId, session); final PlainActionFuture> future = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, future, metadata); + tokenService.createUserToken(authentication, authentication, future, metadata, true); return future.actionGet(); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java index 1ce8b1aff139..c58a63d27ccb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java @@ -222,7 +222,7 @@ public void testLogoutInvalidatesToken() throws Exception { new SamlNameId(NameID.TRANSIENT, nameId, null, null, null), session); final PlainActionFuture> future = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, future, tokenMetaData); + tokenService.createUserToken(authentication, authentication, future, tokenMetaData, true); final UserToken userToken = future.actionGet().v1(); mockGetTokenFromId(userToken, client); final String tokenString = tokenService.getUserTokenString(userToken); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java new file mode 100644 index 000000000000..b9c89d8875ae --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java @@ -0,0 +1,195 @@ +/* + * 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.security.action.token; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetAction; +import org.elasticsearch.action.get.GetRequestBuilder; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.get.MultiGetAction; +import org.elasticsearch.action.get.MultiGetItemResponse; +import org.elasticsearch.action.get.MultiGetRequest; +import org.elasticsearch.action.get.MultiGetRequestBuilder; +import org.elasticsearch.action.get.MultiGetResponse; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.update.UpdateAction; +import org.elasticsearch.action.update.UpdateRequestBuilder; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.node.Node; +import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.test.ClusterServiceUtils; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; +import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequest; +import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.security.authc.AuthenticationService; +import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.junit.After; +import org.junit.Before; + +import java.time.Clock; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TransportCreateTokenActionTests extends ESTestCase { + + private static final Settings SETTINGS = Settings.builder().put(Node.NODE_NAME_SETTING.getKey(), "TokenServiceTests") + .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true).build(); + + private ThreadPool threadPool; + private Client client; + private SecurityIndexManager securityIndex; + private ClusterService clusterService; + private AtomicReference idxReqReference; + private AuthenticationService authenticationService; + + @Before + public void setupClient() { + threadPool = new TestThreadPool(getTestName()); + client = mock(Client.class); + idxReqReference = new AtomicReference<>(); + authenticationService = mock(AuthenticationService.class); + when(client.threadPool()).thenReturn(threadPool); + when(client.settings()).thenReturn(SETTINGS); + doAnswer(invocationOnMock -> { + GetRequestBuilder builder = new GetRequestBuilder(client, GetAction.INSTANCE); + builder.setIndex((String) invocationOnMock.getArguments()[0]) + .setType((String) invocationOnMock.getArguments()[1]) + .setId((String) invocationOnMock.getArguments()[2]); + return builder; + }).when(client).prepareGet(anyString(), anyString(), anyString()); + when(client.prepareMultiGet()).thenReturn(new MultiGetRequestBuilder(client, MultiGetAction.INSTANCE)); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; + MultiGetResponse response = mock(MultiGetResponse.class); + MultiGetItemResponse[] responses = new MultiGetItemResponse[2]; + when(response.getResponses()).thenReturn(responses); + + GetResponse oldGetResponse = mock(GetResponse.class); + when(oldGetResponse.isExists()).thenReturn(false); + responses[0] = new MultiGetItemResponse(oldGetResponse, null); + + GetResponse getResponse = mock(GetResponse.class); + responses[1] = new MultiGetItemResponse(getResponse, null); + when(getResponse.isExists()).thenReturn(false); + listener.onResponse(response); + return Void.TYPE; + }).when(client).multiGet(any(MultiGetRequest.class), any(ActionListener.class)); + when(client.prepareIndex(any(String.class), any(String.class), any(String.class))) + .thenReturn(new IndexRequestBuilder(client, IndexAction.INSTANCE)); + when(client.prepareUpdate(any(String.class), any(String.class), any(String.class))) + .thenReturn(new UpdateRequestBuilder(client, UpdateAction.INSTANCE)); + doAnswer(invocationOnMock -> { + idxReqReference.set((IndexRequest) invocationOnMock.getArguments()[1]); + ActionListener responseActionListener = (ActionListener) invocationOnMock.getArguments()[2]; + responseActionListener.onResponse(new IndexResponse()); + return null; + }).when(client).execute(eq(IndexAction.INSTANCE), any(IndexRequest.class), any(ActionListener.class)); + + // setup lifecycle service + securityIndex = mock(SecurityIndexManager.class); + doAnswer(invocationOnMock -> { + Runnable runnable = (Runnable) invocationOnMock.getArguments()[1]; + runnable.run(); + return null; + }).when(securityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); + + doAnswer(invocationOnMock -> { + UsernamePasswordToken token = (UsernamePasswordToken) invocationOnMock.getArguments()[2]; + User user = new User(token.principal()); + Authentication authentication = new Authentication(user, new Authentication.RealmRef("fake", "mock", "n1"), null); + authentication.writeToContext(threadPool.getThreadContext()); + ActionListener authListener = (ActionListener) invocationOnMock.getArguments()[3]; + authListener.onResponse(authentication); + return Void.TYPE; + }).when(authenticationService).authenticate(eq(CreateTokenAction.NAME), any(CreateTokenRequest.class), + any(UsernamePasswordToken.class), any(ActionListener.class)); + + this.clusterService = ClusterServiceUtils.createClusterService(threadPool); + } + + @After + public void stopThreadPool() throws Exception { + if (threadPool != null) { + terminate(threadPool); + } + } + + public void testClientCredentialsCreatesWithoutRefreshToken() throws Exception { + final TokenService tokenService = new TokenService(SETTINGS, Clock.systemUTC(), client, securityIndex, clusterService); + Authentication authentication = new Authentication(new User("joe"), new Authentication.RealmRef("realm", "type", "node"), null); + authentication.writeToContext(threadPool.getThreadContext()); + + final TransportCreateTokenAction action = new TransportCreateTokenAction(SETTINGS, threadPool, + mock(TransportService.class), new ActionFilters(Collections.emptySet()), tokenService, + authenticationService); + final CreateTokenRequest createTokenRequest = new CreateTokenRequest(); + createTokenRequest.setGrantType("client_credentials"); + + PlainActionFuture tokenResponseFuture = new PlainActionFuture<>(); + action.doExecute(null, createTokenRequest, tokenResponseFuture); + CreateTokenResponse createTokenResponse = tokenResponseFuture.get(); + assertNull(createTokenResponse.getRefreshToken()); + assertNotNull(createTokenResponse.getTokenString()); + + assertNotNull(idxReqReference.get()); + Map sourceMap = idxReqReference.get().sourceAsMap(); + assertNotNull(sourceMap); + assertNotNull(sourceMap.get("access_token")); + assertNull(sourceMap.get("refresh_token")); + } + + public void testPasswordGrantTypeCreatesWithRefreshToken() throws Exception { + final TokenService tokenService = new TokenService(SETTINGS, Clock.systemUTC(), client, securityIndex, clusterService); + Authentication authentication = new Authentication(new User("joe"), new Authentication.RealmRef("realm", "type", "node"), null); + authentication.writeToContext(threadPool.getThreadContext()); + + final TransportCreateTokenAction action = new TransportCreateTokenAction(SETTINGS, threadPool, + mock(TransportService.class), new ActionFilters(Collections.emptySet()), tokenService, + authenticationService); + final CreateTokenRequest createTokenRequest = new CreateTokenRequest(); + createTokenRequest.setGrantType("password"); + createTokenRequest.setUsername("user"); + createTokenRequest.setPassword(new SecureString("password".toCharArray())); + + PlainActionFuture tokenResponseFuture = new PlainActionFuture<>(); + action.doExecute(null, createTokenRequest, tokenResponseFuture); + CreateTokenResponse createTokenResponse = tokenResponseFuture.get(); + assertNotNull(createTokenResponse.getRefreshToken()); + assertNotNull(createTokenResponse.getTokenString()); + + assertNotNull(idxReqReference.get()); + Map sourceMap = idxReqReference.get().sourceAsMap(); + assertNotNull(sourceMap); + assertNotNull(sourceMap.get("access_token")); + assertNotNull(sourceMap.get("refresh_token")); + } +} 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 4a40e0d543bc..a07bc734361d 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 @@ -896,7 +896,7 @@ public void testAuthenticateWithToken() throws Exception { PlainActionFuture> tokenFuture = new PlainActionFuture<>(); try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { Authentication originatingAuth = new Authentication(new User("creator"), new RealmRef("test", "test", "test"), null); - tokenService.createUserToken(expected, originatingAuth, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(expected, originatingAuth, tokenFuture, Collections.emptyMap(), true); } String token = tokenService.getUserTokenString(tokenFuture.get().v1()); mockGetTokenFromId(tokenFuture.get().v1(), client); @@ -975,7 +975,7 @@ public void testExpiredToken() throws Exception { PlainActionFuture> tokenFuture = new PlainActionFuture<>(); try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { Authentication originatingAuth = new Authentication(new User("creator"), new RealmRef("test", "test", "test"), null); - tokenService.createUserToken(expected, originatingAuth, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(expected, originatingAuth, tokenFuture, Collections.emptyMap(), true); } String token = tokenService.getUserTokenString(tokenFuture.get().v1()); mockGetTokenFromId(tokenFuture.get().v1(), client); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java index ec4a97b7f392..e6cc2dcccdfa 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java @@ -341,6 +341,39 @@ public void testCreateThenRefreshAsDifferentUser() { assertEquals(SecuritySettingsSource.TEST_USER_NAME, response.user().principal()); } + public void testClientCredentialsGrant() throws Exception { + Client client = client().filterWithHeader(Collections.singletonMap("Authorization", + UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, + SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + SecurityClient securityClient = new SecurityClient(client); + CreateTokenResponse createTokenResponse = securityClient.prepareCreateToken() + .setGrantType("client_credentials") + .get(); + assertNull(createTokenResponse.getRefreshToken()); + + AuthenticateRequest request = new AuthenticateRequest(); + request.username(SecuritySettingsSource.TEST_SUPERUSER); + PlainActionFuture authFuture = new PlainActionFuture<>(); + client.filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + createTokenResponse.getTokenString())) + .execute(AuthenticateAction.INSTANCE, request, authFuture); + AuthenticateResponse response = authFuture.get(); + assertEquals(SecuritySettingsSource.TEST_SUPERUSER, response.user().principal()); + + // invalidate + PlainActionFuture invalidateResponseFuture = new PlainActionFuture<>(); + InvalidateTokenRequest invalidateTokenRequest = + new InvalidateTokenRequest(createTokenResponse.getTokenString(), InvalidateTokenRequest.Type.ACCESS_TOKEN); + securityClient.invalidateToken(invalidateTokenRequest, invalidateResponseFuture); + assertTrue(invalidateResponseFuture.get().isCreated()); + + ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> { + PlainActionFuture responseFuture = new PlainActionFuture<>(); + client.filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + createTokenResponse.getTokenString())) + .execute(AuthenticateAction.INSTANCE, request, responseFuture); + responseFuture.actionGet(); + }); + } + @Before public void waitForSecurityIndexWritable() throws Exception { assertSecurityIndexActive(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index c529ea8747b1..2d5b5707cd26 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -157,7 +157,7 @@ public void testAttachAndGetToken() throws Exception { TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); assertNotNull(token); mockGetTokenFromId(token); @@ -203,7 +203,7 @@ public void testRotateKey() throws Exception { TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); assertNotNull(token); mockGetTokenFromId(token); @@ -227,7 +227,7 @@ public void testRotateKey() throws Exception { } PlainActionFuture> newTokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, newTokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, newTokenFuture, Collections.emptyMap(), true); final UserToken newToken = newTokenFuture.get().v1(); assertNotNull(newToken); assertNotEquals(tokenService.getUserTokenString(newToken), tokenService.getUserTokenString(token)); @@ -262,7 +262,7 @@ public void testKeyExchange() throws Exception { otherTokenService.refreshMetaData(tokenService.getTokenMetaData()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); assertNotNull(token); mockGetTokenFromId(token); @@ -292,7 +292,7 @@ public void testPruneKeys() throws Exception { TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); assertNotNull(token); mockGetTokenFromId(token); @@ -322,7 +322,7 @@ public void testPruneKeys() throws Exception { } PlainActionFuture> newTokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, newTokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, newTokenFuture, Collections.emptyMap(), true); final UserToken newToken = newTokenFuture.get().v1(); assertNotNull(newToken); assertNotEquals(tokenService.getUserTokenString(newToken), tokenService.getUserTokenString(token)); @@ -353,7 +353,7 @@ public void testPassphraseWorks() throws Exception { TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); assertNotNull(token); mockGetTokenFromId(token); @@ -383,7 +383,7 @@ public void testGetTokenWhenKeyCacheHasExpired() throws Exception { Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); UserToken token = tokenFuture.get().v1(); assertThat(tokenService.getUserTokenString(token), notNullValue()); @@ -397,7 +397,7 @@ public void testInvalidatedToken() throws Exception { new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); assertNotNull(token); doAnswer(invocationOnMock -> { @@ -451,7 +451,7 @@ public void testTokenExpiry() throws Exception { TokenService tokenService = new TokenService(tokenServiceEnabledSettings, clock, client, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); mockGetTokenFromId(token); @@ -501,7 +501,8 @@ public void testTokenServiceDisabled() throws Exception { .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), false) .build(), Clock.systemUTC(), client, securityIndex, clusterService); - IllegalStateException e = expectThrows(IllegalStateException.class, () -> tokenService.createUserToken(null, null, null, null)); + IllegalStateException e = expectThrows(IllegalStateException.class, + () -> tokenService.createUserToken(null, null, null, null, true)); assertEquals("tokens are not enabled", e.getMessage()); PlainActionFuture future = new PlainActionFuture<>(); @@ -559,7 +560,7 @@ public void testIndexNotAvailable() throws Exception { new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); - tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap()); + tokenService.createUserToken(authentication, authentication, tokenFuture, Collections.emptyMap(), true); final UserToken token = tokenFuture.get().v1(); assertNotNull(token); mockGetTokenFromId(token); diff --git a/x-pack/qa/rolling-upgrade/build.gradle b/x-pack/qa/rolling-upgrade/build.gradle index 548081a89388..90da6cf4e58b 100644 --- a/x-pack/qa/rolling-upgrade/build.gradle +++ b/x-pack/qa/rolling-upgrade/build.gradle @@ -158,6 +158,7 @@ subprojects { } else { String systemKeyFile = version.before('6.3.0') ? 'x-pack/system_key' : 'system_key' extraConfigFile systemKeyFile, "${mainProject.projectDir}/src/test/resources/system_key" + keystoreSetting 'xpack.security.authc.token.passphrase', 'token passphrase' } setting 'xpack.watcher.encrypt_sensitive_data', 'true' } @@ -199,6 +200,9 @@ subprojects { setting 'xpack.watcher.encrypt_sensitive_data', 'true' keystoreFile 'xpack.watcher.encryption_key', "${mainProject.projectDir}/src/test/resources/system_key" } + if (version.before('6.0.0')) { + keystoreSetting 'xpack.security.authc.token.passphrase', 'token passphrase' + } } } From 318df2a107fb2e77a035bd18ce0a65cf54a88772 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 27 Aug 2018 12:27:23 -0400 Subject: [PATCH 172/283] Adjust BWC version on mapping version The introduction of mapping version on index metadata has been backported to 6.x. This commit adjusts the BWC version around mapping version to account for this backport. --- .../cluster/metadata/IndexMetaData.java | 12 ++++++------ .../elasticsearch/index/mapper/MapperService.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index 31bf260e9013..11c489f63abc 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -684,7 +684,7 @@ private static class IndexMetaDataDiff implements Diff { index = in.readString(); routingNumShards = in.readInt(); version = in.readLong(); - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (in.getVersion().onOrAfter(Version.V_6_5_0)) { mappingVersion = in.readVLong(); } else { mappingVersion = 1; @@ -724,7 +724,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(index); out.writeInt(routingNumShards); out.writeLong(version); - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (out.getVersion().onOrAfter(Version.V_6_5_0)) { out.writeVLong(mappingVersion); } out.writeByte(state.id); @@ -760,7 +760,7 @@ public IndexMetaData apply(IndexMetaData part) { public static IndexMetaData readFrom(StreamInput in) throws IOException { Builder builder = new Builder(in.readString()); builder.version(in.readLong()); - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (in.getVersion().onOrAfter(Version.V_6_5_0)) { builder.mappingVersion(in.readVLong()); } else { builder.mappingVersion(1); @@ -804,7 +804,7 @@ public static IndexMetaData readFrom(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { out.writeString(index.getName()); // uuid will come as part of settings out.writeLong(version); - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (out.getVersion().onOrAfter(Version.V_6_5_0)) { out.writeVLong(mappingVersion); } out.writeInt(routingNumShards); @@ -1370,8 +1370,8 @@ public static IndexMetaData fromXContent(XContentParser parser) throws IOExcepti throw new IllegalArgumentException("Unexpected token " + token); } } - if (Assertions.ENABLED && Version.indexCreated(builder.settings).onOrAfter(Version.V_7_0_0_alpha1)) { - assert mappingVersion : "mapping version should be present for indices created on or after 7.0.0"; + if (Assertions.ENABLED && Version.indexCreated(builder.settings).onOrAfter(Version.V_6_5_0)) { + assert mappingVersion : "mapping version should be present for indices created on or after 6.5.0"; } return builder.build(); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 5ebfc5bb51e7..d06374d5d891 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -249,7 +249,7 @@ private void assertMappingVersion( final Map updatedEntries) { if (Assertions.ENABLED && currentIndexMetaData != null - && currentIndexMetaData.getCreationVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + && currentIndexMetaData.getCreationVersion().onOrAfter(Version.V_6_5_0)) { if (currentIndexMetaData.getMappingVersion() == newIndexMetaData.getMappingVersion()) { // if the mapping version is unchanged, then there should not be any updates and all mappings should be the same assert updatedEntries.isEmpty() : updatedEntries; From ed0571e16caac689a77362bfe4c45c79dcba4d82 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Mon, 27 Aug 2018 20:31:27 +0200 Subject: [PATCH 173/283] ShardSearchFailure#readFrom to set index and shardId (#33161) As part of recent changes made to `ShardOperationFailedException` we introduced `index` and `shardId` members to the base class, but the subclasses are entirely responsible for the serialization of such fields. In the case of `ShardSearchFailure`, we have an additional `SearchShardTarget` instance member which also holds the index and the shardId, hence they get serialized as part of `SearchShardTarget` itself. When de-serializing a `ShardSearchFailure` though, we need to remember to also set the parent class `index` and `shardId` fields otherwise they get lost Relates to #32640 --- .../action/ShardOperationFailedException.java | 2 +- .../action/search/ShardSearchFailure.java | 5 +++-- .../replication/ReplicationResponse.java | 2 +- .../snapshots/SnapshotShardFailure.java | 2 +- .../action/search/SearchResponseTests.java | 3 ++- .../action/search/ShardSearchFailureTests.java | 18 +++++++++++++++--- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/ShardOperationFailedException.java b/server/src/main/java/org/elasticsearch/action/ShardOperationFailedException.java index 08a97d4d993a..490a1760abea 100644 --- a/server/src/main/java/org/elasticsearch/action/ShardOperationFailedException.java +++ b/server/src/main/java/org/elasticsearch/action/ShardOperationFailedException.java @@ -33,7 +33,7 @@ public abstract class ShardOperationFailedException implements Streamable, ToXContent { protected String index; - protected int shardId; + protected int shardId = -1; protected String reason; protected RestStatus status; protected Throwable cause; diff --git a/server/src/main/java/org/elasticsearch/action/search/ShardSearchFailure.java b/server/src/main/java/org/elasticsearch/action/search/ShardSearchFailure.java index 98418153d501..ddfadfa57e31 100644 --- a/server/src/main/java/org/elasticsearch/action/search/ShardSearchFailure.java +++ b/server/src/main/java/org/elasticsearch/action/search/ShardSearchFailure.java @@ -54,8 +54,7 @@ public class ShardSearchFailure extends ShardOperationFailedException { private SearchShardTarget shardTarget; - private ShardSearchFailure() { - + ShardSearchFailure() { } public ShardSearchFailure(Exception e) { @@ -101,6 +100,8 @@ public static ShardSearchFailure readShardSearchFailure(StreamInput in) throws I public void readFrom(StreamInput in) throws IOException { if (in.readBoolean()) { shardTarget = new SearchShardTarget(in); + index = shardTarget.getFullyQualifiedIndexName(); + shardId = shardTarget.getShardId().getId(); } reason = in.readString(); status = RestStatus.readFrom(in); diff --git a/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationResponse.java b/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationResponse.java index bc5c696894a6..3e0c1a6d1e41 100644 --- a/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationResponse.java +++ b/server/src/main/java/org/elasticsearch/action/support/replication/ReplicationResponse.java @@ -271,7 +271,7 @@ public boolean primary() { public void readFrom(StreamInput in) throws IOException { shardId = ShardId.readShardId(in); super.shardId = shardId.getId(); - super.index = shardId.getIndexName(); + index = shardId.getIndexName(); nodeId = in.readOptionalString(); cause = in.readException(); status = RestStatus.readFrom(in); diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java index f2bdc2ba5df4..67bf9c6069fe 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardFailure.java @@ -102,7 +102,7 @@ public void readFrom(StreamInput in) throws IOException { nodeId = in.readOptionalString(); shardId = ShardId.readShardId(in); super.shardId = shardId.getId(); - super.index = shardId.getIndexName(); + index = shardId.getIndexName(); reason = in.readString(); status = RestStatus.readFrom(in); } diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java index feb5ef50795d..d6fbf59d941b 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchResponseTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.action.search; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -180,7 +181,7 @@ public void testFromXContentWithFailures() throws IOException { int numFailures = randomIntBetween(1, 5); ShardSearchFailure[] failures = new ShardSearchFailure[numFailures]; for (int i = 0; i < failures.length; i++) { - failures[i] = ShardSearchFailureTests.createTestItem(); + failures[i] = ShardSearchFailureTests.createTestItem(IndexMetaData.INDEX_UUID_NA_VALUE); } SearchResponse response = createTestItem(failures); XContentType xcontentType = randomFrom(XContentType.values()); diff --git a/server/src/test/java/org/elasticsearch/action/search/ShardSearchFailureTests.java b/server/src/test/java/org/elasticsearch/action/search/ShardSearchFailureTests.java index bd892829c954..f62f874c9e2c 100644 --- a/server/src/test/java/org/elasticsearch/action/search/ShardSearchFailureTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/ShardSearchFailureTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; import java.io.IOException; @@ -38,7 +39,7 @@ public class ShardSearchFailureTests extends ESTestCase { - public static ShardSearchFailure createTestItem() { + public static ShardSearchFailure createTestItem(String indexUuid) { String randomMessage = randomAlphaOfLengthBetween(3, 20); Exception ex = new ParsingException(0, 0, randomMessage , new IllegalArgumentException("some bad argument")); SearchShardTarget searchShardTarget = null; @@ -47,7 +48,7 @@ public static ShardSearchFailure createTestItem() { String indexName = randomAlphaOfLengthBetween(5, 10); String clusterAlias = randomBoolean() ? randomAlphaOfLengthBetween(5, 10) : null; searchShardTarget = new SearchShardTarget(nodeId, - new ShardId(new Index(indexName, IndexMetaData.INDEX_UUID_NA_VALUE), randomInt()), clusterAlias, OriginalIndices.NONE); + new ShardId(new Index(indexName, indexUuid), randomInt()), clusterAlias, OriginalIndices.NONE); } return new ShardSearchFailure(ex, searchShardTarget); } @@ -66,7 +67,7 @@ public void testFromXContentWithRandomFields() throws IOException { } private void doFromXContentTestWithRandomFields(boolean addRandomFields) throws IOException { - ShardSearchFailure response = createTestItem(); + ShardSearchFailure response = createTestItem(IndexMetaData.INDEX_UUID_NA_VALUE); XContentType xContentType = randomFrom(XContentType.values()); boolean humanReadable = randomBoolean(); BytesReference originalBytes = toShuffledXContent(response, xContentType, ToXContent.EMPTY_PARAMS, humanReadable); @@ -134,4 +135,15 @@ public void testToXContentWithClusterAlias() throws IOException { + "}", xContent.utf8ToString()); } + + public void testSerialization() throws IOException { + ShardSearchFailure testItem = createTestItem(randomAlphaOfLength(12)); + ShardSearchFailure deserializedInstance = copyStreamable(testItem, writableRegistry(), + ShardSearchFailure::new, VersionUtils.randomVersion(random())); + assertEquals(testItem.index(), deserializedInstance.index()); + assertEquals(testItem.shard(), deserializedInstance.shard()); + assertEquals(testItem.shardId(), deserializedInstance.shardId()); + assertEquals(testItem.reason(), deserializedInstance.reason()); + assertEquals(testItem.status(), deserializedInstance.status()); + } } From 014b3236dcb6f2942945c360b343dccac605b466 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 27 Aug 2018 15:59:42 -0400 Subject: [PATCH 174/283] Ensure to generate identical NoOp for the same failure (#33141) We generate slightly different NoOps in InternalEngine and TransportShardBulkAction for the same failure. 1. InternalEngine uses Exception#getFailure to generate a message without the class name: newOp [NoOp{seqNo=1, primaryTerm=1, reason='Contexts are mandatory in context enabled completion field [suggest_context]'}]. 2. TransportShardBulkAction uses Exception#toString to generate a message with the class name: NoOp{seqNo=1, primaryTerm=1, reason='java.lang.IllegalArgumentException: Contexts are mandatory in context enabled completion field [suggest_context]'}. If a write operation fails while a replica is recovering, that replica will possibly receive two different NoOps: one from recovery and one from replication. These two different NoOps will trip TranslogWriter#assertNoSeqNumberConflict assertion. This commit ensures that we generate the same Noop for the same failure. Closes #32986 --- .../index/engine/InternalEngine.java | 4 +- .../IndexLevelReplicationTests.java | 125 +++++++++--------- .../ESIndexLevelReplicationTestCase.java | 24 +++- 3 files changed, 78 insertions(+), 75 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 c4c6792bf46a..023e659ffabe 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -802,7 +802,7 @@ public IndexResult index(Index index) throws IOException { location = translog.add(new Translog.Index(index, indexResult)); } else if (indexResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { // if we have document failure, record it as a no-op in the translog with the generated seq_no - location = translog.add(new Translog.NoOp(indexResult.getSeqNo(), index.primaryTerm(), indexResult.getFailure().getMessage())); + location = translog.add(new Translog.NoOp(indexResult.getSeqNo(), index.primaryTerm(), indexResult.getFailure().toString())); } else { location = null; } @@ -1111,7 +1111,7 @@ public DeleteResult delete(Delete delete) throws IOException { location = translog.add(new Translog.Delete(delete, deleteResult)); } else if (deleteResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { location = translog.add(new Translog.NoOp(deleteResult.getSeqNo(), - delete.primaryTerm(), deleteResult.getFailure().getMessage())); + delete.primaryTerm(), deleteResult.getFailure().toString())); } else { location = null; } 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 f38550d70413..1d1e423afc1b 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java @@ -36,7 +36,6 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.engine.Engine; -import org.elasticsearch.index.engine.EngineConfig; import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.InternalEngine; import org.elasticsearch.index.engine.InternalEngineTests; @@ -47,6 +46,7 @@ import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTests; import org.elasticsearch.index.store.Store; +import org.elasticsearch.index.translog.SnapshotMatchers; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.indices.recovery.RecoveryTarget; import org.elasticsearch.threadpool.TestThreadPool; @@ -54,6 +54,7 @@ import org.hamcrest.Matcher; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -338,38 +339,73 @@ public void testReplicaOperationWithConcurrentPrimaryPromotion() throws Exceptio * for primary and replica shards */ public void testDocumentFailureReplication() throws Exception { - final String failureMessage = "simulated document failure"; - final ThrowingDocumentFailureEngineFactory throwingDocumentFailureEngineFactory = - new ThrowingDocumentFailureEngineFactory(failureMessage); + final IOException indexException = new IOException("simulated indexing failure"); + final IOException deleteException = new IOException("simulated deleting failure"); + final EngineFactory engineFactory = config -> InternalEngineTests.createInternalEngine((dir, iwc) -> + new IndexWriter(dir, iwc) { + final AtomicBoolean throwAfterIndexedOneDoc = new AtomicBoolean(); // need one document to trigger delete in IW. + @Override + public long addDocument(Iterable doc) throws IOException { + if (throwAfterIndexedOneDoc.getAndSet(true)) { + throw indexException; + } else { + return super.addDocument(doc); + } + } + @Override + public long deleteDocuments(Term... terms) throws IOException { + throw deleteException; + } + }, null, null, config); try (ReplicationGroup shards = new ReplicationGroup(buildIndexMetaData(0)) { @Override - protected EngineFactory getEngineFactory(ShardRouting routing) { - return throwingDocumentFailureEngineFactory; - }}) { + protected EngineFactory getEngineFactory(ShardRouting routing) { return engineFactory; }}) { - // test only primary + // start with the primary only so two first failures are replicated to replicas via recovery from the translog of the primary. shards.startPrimary(); - BulkItemResponse response = shards.index( - new IndexRequest(index.getName(), "type", "1") - .source("{}", XContentType.JSON) - ); - assertTrue(response.isFailed()); - assertNoOpTranslogOperationForDocumentFailure(shards, 1, shards.getPrimary().getPendingPrimaryTerm(), failureMessage); - shards.assertAllEqual(0); + long primaryTerm = shards.getPrimary().getPendingPrimaryTerm(); + List expectedTranslogOps = new ArrayList<>(); + BulkItemResponse indexResp = shards.index(new IndexRequest(index.getName(), "type", "1").source("{}", XContentType.JSON)); + assertThat(indexResp.isFailed(), equalTo(false)); + expectedTranslogOps.add(new Translog.Index("type", "1", 0, primaryTerm, 1, "{}".getBytes(StandardCharsets.UTF_8), null, -1)); + try (Translog.Snapshot snapshot = getTranslog(shards.getPrimary()).newSnapshot()) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } + + indexResp = shards.index(new IndexRequest(index.getName(), "type", "any").source("{}", XContentType.JSON)); + assertThat(indexResp.getFailure().getCause(), equalTo(indexException)); + expectedTranslogOps.add(new Translog.NoOp(1, primaryTerm, indexException.toString())); + + BulkItemResponse deleteResp = shards.delete(new DeleteRequest(index.getName(), "type", "1")); + assertThat(deleteResp.getFailure().getCause(), equalTo(deleteException)); + expectedTranslogOps.add(new Translog.NoOp(2, primaryTerm, deleteException.toString())); + shards.assertAllEqual(1); - // add some replicas int nReplica = randomIntBetween(1, 3); for (int i = 0; i < nReplica; i++) { shards.addReplica(); } shards.startReplicas(nReplica); - response = shards.index( - new IndexRequest(index.getName(), "type", "1") - .source("{}", XContentType.JSON) - ); - assertTrue(response.isFailed()); - assertNoOpTranslogOperationForDocumentFailure(shards, 2, shards.getPrimary().getPendingPrimaryTerm(), failureMessage); - shards.assertAllEqual(0); + for (IndexShard shard : shards) { + try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } + } + // unlike previous failures, these two failures replicated directly from the replication channel. + indexResp = shards.index(new IndexRequest(index.getName(), "type", "any").source("{}", XContentType.JSON)); + assertThat(indexResp.getFailure().getCause(), equalTo(indexException)); + expectedTranslogOps.add(new Translog.NoOp(3, primaryTerm, indexException.toString())); + + deleteResp = shards.delete(new DeleteRequest(index.getName(), "type", "1")); + assertThat(deleteResp.getFailure().getCause(), equalTo(deleteException)); + expectedTranslogOps.add(new Translog.NoOp(4, primaryTerm, deleteException.toString())); + + for (IndexShard shard : shards) { + try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } + } + shards.assertAllEqual(1); } } @@ -541,47 +577,4 @@ public void testOutOfOrderDeliveryForAppendOnlyOperations() throws Exception { shards.assertAllEqual(0); } } - - /** Throws documentFailure on every indexing operation */ - static class ThrowingDocumentFailureEngineFactory implements EngineFactory { - final String documentFailureMessage; - - ThrowingDocumentFailureEngineFactory(String documentFailureMessage) { - this.documentFailureMessage = documentFailureMessage; - } - - @Override - public Engine newReadWriteEngine(EngineConfig config) { - return InternalEngineTests.createInternalEngine((directory, writerConfig) -> - new IndexWriter(directory, writerConfig) { - @Override - public long addDocument(Iterable doc) throws IOException { - assert documentFailureMessage != null; - throw new IOException(documentFailureMessage); - } - }, null, null, config); - } - } - - private static void assertNoOpTranslogOperationForDocumentFailure( - Iterable replicationGroup, - int expectedOperation, - long expectedPrimaryTerm, - String failureMessage) throws IOException { - for (IndexShard indexShard : replicationGroup) { - try(Translog.Snapshot snapshot = getTranslog(indexShard).newSnapshot()) { - assertThat(snapshot.totalOperations(), equalTo(expectedOperation)); - long expectedSeqNo = 0L; - Translog.Operation op = snapshot.next(); - do { - assertThat(op.opType(), equalTo(Translog.Operation.Type.NO_OP)); - assertThat(op.seqNo(), equalTo(expectedSeqNo)); - assertThat(op.primaryTerm(), equalTo(expectedPrimaryTerm)); - assertThat(((Translog.NoOp) op).reason(), containsString(failureMessage)); - op = snapshot.next(); - expectedSeqNo++; - } while (op != null); - } - } - } } 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 9b63f0d233e0..3f1f5daf5148 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 @@ -37,6 +37,7 @@ import org.elasticsearch.action.resync.ResyncReplicationResponse; import org.elasticsearch.action.resync.TransportResyncReplicationAction; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.replication.ReplicatedWriteRequest; import org.elasticsearch.action.support.replication.ReplicationOperation; import org.elasticsearch.action.support.replication.ReplicationRequest; @@ -193,14 +194,23 @@ public int appendDocs(final int numOfDoc) throws Exception { } public BulkItemResponse index(IndexRequest indexRequest) throws Exception { + return executeWriteRequest(indexRequest, indexRequest.getRefreshPolicy()); + } + + public BulkItemResponse delete(DeleteRequest deleteRequest) throws Exception { + return executeWriteRequest(deleteRequest, deleteRequest.getRefreshPolicy()); + } + + private BulkItemResponse executeWriteRequest( + DocWriteRequest writeRequest, WriteRequest.RefreshPolicy refreshPolicy) throws Exception { PlainActionFuture listener = new PlainActionFuture<>(); final ActionListener wrapBulkListener = ActionListener.wrap( - bulkShardResponse -> listener.onResponse(bulkShardResponse.getResponses()[0]), - listener::onFailure); + bulkShardResponse -> listener.onResponse(bulkShardResponse.getResponses()[0]), + listener::onFailure); BulkItemRequest[] items = new BulkItemRequest[1]; - items[0] = new BulkItemRequest(0, indexRequest); - BulkShardRequest request = new BulkShardRequest(shardId, indexRequest.getRefreshPolicy(), items); - new IndexingAction(request, wrapBulkListener, this).execute(); + items[0] = new BulkItemRequest(0, writeRequest); + BulkShardRequest request = new BulkShardRequest(shardId, refreshPolicy, items); + new WriteReplicationAction(request, wrapBulkListener, this).execute(); return listener.get(); } @@ -598,9 +608,9 @@ public void respond(ActionListener listener) { } - class IndexingAction extends ReplicationAction { + class WriteReplicationAction extends ReplicationAction { - IndexingAction(BulkShardRequest request, ActionListener listener, ReplicationGroup replicationGroup) { + WriteReplicationAction(BulkShardRequest request, ActionListener listener, ReplicationGroup replicationGroup) { super(request, listener, replicationGroup, "indexing"); } From 5937e499e1b7f2dd15a1ce7bc21f4ffa7d9f2cbc Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Mon, 27 Aug 2018 16:41:32 -0400 Subject: [PATCH 175/283] Build analysis-icu client JAR (#33184) This plugin needs to be able to be installed client side because it contains doc values formats that can be returned to the transport client. To keep this simple for developers, we publish the client JAR to Maven so that they can depend on the plugin in their POM and install the plugin there. --- plugins/analysis-icu/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/analysis-icu/build.gradle b/plugins/analysis-icu/build.gradle index 676fd4481315..a42a28cad4e2 100644 --- a/plugins/analysis-icu/build.gradle +++ b/plugins/analysis-icu/build.gradle @@ -22,6 +22,7 @@ import org.elasticsearch.gradle.precommit.ForbiddenApisCliTask esplugin { description 'The ICU Analysis plugin integrates Lucene ICU module into elasticsearch, adding ICU relates analysis components.' classname 'org.elasticsearch.plugin.analysis.icu.AnalysisICUPlugin' + hasClientJar = true } tasks.withType(ForbiddenApisCliTask) { From 5b11df9c351dc7b0fc2124c9f7fced8d8d52dfca Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 27 Aug 2018 18:33:57 -0400 Subject: [PATCH 176/283] DOCS: Make ellipsis optional in /cat/thread_pool (#33186) The fix proposed in #31442 fails with the oss distro because the added 3dots does not match anything with the default oss while a 3dots expression requires matching at least one thread pool. This change makes an ellipsis optional so the thread_pool list can match both the oss distro (without ccr) and default distro (with ccr). Relates #31442 --- docs/reference/cat/thread_pool.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/cat/thread_pool.asciidoc b/docs/reference/cat/thread_pool.asciidoc index eab74f1f9f0d..d1ea1fad8851 100644 --- a/docs/reference/cat/thread_pool.asciidoc +++ b/docs/reference/cat/thread_pool.asciidoc @@ -22,7 +22,7 @@ node-0 flush 0 0 0 ... node-0 write 0 0 0 -------------------------------------------------- -// TESTRESPONSE[s/\.\.\./(node-0 \\S+ 0 0 0\n)+/] +// TESTRESPONSE[s/\.\.\./(node-0 \\S+ 0 0 0\n)*/] // TESTRESPONSE[s/\d+/\\d+/ _cat] // The substitutions do two things: // 1. Expect any number of extra thread pools. This allows us to only list a From 29ba143e2cac93e18a576e9501898303f256b910 Mon Sep 17 00:00:00 2001 From: Jonathan Little Date: Mon, 27 Aug 2018 16:29:50 -0700 Subject: [PATCH 177/283] Remove old unused test script files (#32970) --- .../src/test/cluster/config/scripts/calculate_score.painless | 1 - .../test/cluster/config/scripts/my_combine_script.painless | 5 ----- docs/src/test/cluster/config/scripts/my_init_script.painless | 1 - docs/src/test/cluster/config/scripts/my_map_script.painless | 1 - .../test/cluster/config/scripts/my_reduce_script.painless | 5 ----- docs/src/test/cluster/config/scripts/my_script.painless | 2 -- 6 files changed, 15 deletions(-) delete mode 100644 docs/src/test/cluster/config/scripts/calculate_score.painless delete mode 100644 docs/src/test/cluster/config/scripts/my_combine_script.painless delete mode 100644 docs/src/test/cluster/config/scripts/my_init_script.painless delete mode 100644 docs/src/test/cluster/config/scripts/my_map_script.painless delete mode 100644 docs/src/test/cluster/config/scripts/my_reduce_script.painless delete mode 100644 docs/src/test/cluster/config/scripts/my_script.painless diff --git a/docs/src/test/cluster/config/scripts/calculate_score.painless b/docs/src/test/cluster/config/scripts/calculate_score.painless deleted file mode 100644 index 0fad3fc59f95..000000000000 --- a/docs/src/test/cluster/config/scripts/calculate_score.painless +++ /dev/null @@ -1 +0,0 @@ -Math.log(_score * 2) + params.my_modifier diff --git a/docs/src/test/cluster/config/scripts/my_combine_script.painless b/docs/src/test/cluster/config/scripts/my_combine_script.painless deleted file mode 100644 index 106ef08d91fd..000000000000 --- a/docs/src/test/cluster/config/scripts/my_combine_script.painless +++ /dev/null @@ -1,5 +0,0 @@ -double profit = 0; -for (t in params._agg.transactions) { - profit += t -} -return profit diff --git a/docs/src/test/cluster/config/scripts/my_init_script.painless b/docs/src/test/cluster/config/scripts/my_init_script.painless deleted file mode 100644 index fb6aa11723cf..000000000000 --- a/docs/src/test/cluster/config/scripts/my_init_script.painless +++ /dev/null @@ -1 +0,0 @@ -params._agg.transactions = [] diff --git a/docs/src/test/cluster/config/scripts/my_map_script.painless b/docs/src/test/cluster/config/scripts/my_map_script.painless deleted file mode 100644 index f4700482d558..000000000000 --- a/docs/src/test/cluster/config/scripts/my_map_script.painless +++ /dev/null @@ -1 +0,0 @@ -params._agg.transactions.add(doc.type.value == 'sale' ? doc.amount.value : -1 * doc.amount.value) diff --git a/docs/src/test/cluster/config/scripts/my_reduce_script.painless b/docs/src/test/cluster/config/scripts/my_reduce_script.painless deleted file mode 100644 index ca4f67ca2db0..000000000000 --- a/docs/src/test/cluster/config/scripts/my_reduce_script.painless +++ /dev/null @@ -1,5 +0,0 @@ -double profit = 0; -for (a in params._aggs) { - profit += a -} -return profit diff --git a/docs/src/test/cluster/config/scripts/my_script.painless b/docs/src/test/cluster/config/scripts/my_script.painless deleted file mode 100644 index 55d0e99ed0f0..000000000000 --- a/docs/src/test/cluster/config/scripts/my_script.painless +++ /dev/null @@ -1,2 +0,0 @@ -// Simple script to load a field. Not really a good example, but a simple one. -doc[params.field].value From daee8bd13375a79994d48350535cc90dff7f23b1 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 28 Aug 2018 07:44:01 +0200 Subject: [PATCH 178/283] HLRC+MINOR: Remove Unused Private Method (#33165) * This one seems to be unused since 92eb32477 --- .../org/elasticsearch/client/RestHighLevelClient.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 7376f74839ce..b6784060e24e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -19,7 +19,6 @@ package org.elasticsearch.client; -import org.apache.http.Header; import org.apache.http.HttpEntity; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchStatusException; @@ -1218,15 +1217,6 @@ protected final Resp parseEntity(final HttpEntity entity, } } - private static RequestOptions optionsForHeaders(Header[] headers) { - RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); - for (Header header : headers) { - Objects.requireNonNull(header, "header cannot be null"); - options.addHeader(header.getName(), header.getValue()); - } - return options.build(); - } - static boolean convertExistsResponse(Response response) { return response.getStatusLine().getStatusCode() == 200; } From 71d5c66fd384aaebfb83e0b24920df984eb9f09b Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Tue, 28 Aug 2018 10:00:11 +0300 Subject: [PATCH 179/283] Fix plugin build test on Windows (#33078) Fix plugin build test on Windows --- .../org/elasticsearch/gradle/BuildExamplePluginsIT.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java index 9b63d6f45e06..3e18b0b80af3 100644 --- a/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java @@ -158,7 +158,12 @@ private String getLocalTestRepoPath() { Objects.requireNonNull(property, "test.local-test-repo-path not passed to tests"); File file = new File(property); assertTrue("Expected " + property + " to exist, but it did not!", file.exists()); - return file.getAbsolutePath(); + if (File.separator.equals("\\")) { + // Use / on Windows too, the build script is not happy with \ + return file.getAbsolutePath().replace(File.separator, "/"); + } else { + return file.getAbsolutePath(); + } } } From 2cc611604f16cbedf94fb218da9fe29da7727868 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Tue, 28 Aug 2018 10:03:30 +0300 Subject: [PATCH 180/283] Run Third party audit with forbidden APIs CLI (part3/3) (#33052) The new implementation is functional equivalent with the old, ant based one. It parses task standard error to get the missing classes and violations in the same way. I considered re-using ForbiddenApisCliTask but Gradle makes it hard to build inheritance with tasks that have task actions , since the order of the task actions can't be controlled. This inheritance isn't dully desired either as the third party audit task is much more opinionated and we don't want to expose some of the configuration. We could probably extract a common base class without any task actions, but probably more trouble than it's worth. Closes #31715 --- buildSrc/build.gradle | 1 - .../gradle/precommit/PrecommitTasks.groovy | 39 ++- .../precommit/ThirdPartyAuditTask.groovy | 297 ------------------ .../test/StandaloneRestTestPlugin.groovy | 2 + .../elasticsearch/gradle/JdkJarHellCheck.java | 81 +++++ .../precommit/ForbiddenApisCliTask.java | 49 ++- .../gradle/precommit/ThirdPartyAuditTask.java | 288 +++++++++++++++++ plugins/discovery-azure-classic/build.gradle | 2 +- plugins/discovery-ec2/build.gradle | 2 +- plugins/ingest-attachment/build.gradle | 22 +- plugins/repository-hdfs/build.gradle | 21 +- plugins/repository-s3/build.gradle | 2 +- server/build.gradle | 19 +- test/logger-usage/build.gradle | 11 +- x-pack/plugin/security/build.gradle | 4 +- x-pack/plugin/sql/sql-action/build.gradle | 11 +- x-pack/plugin/watcher/build.gradle | 2 +- 17 files changed, 505 insertions(+), 348 deletions(-) delete mode 100644 buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/JdkJarHellCheck.java create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 967c2e27ee8d..9918d54d7073 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -102,7 +102,6 @@ dependencies { compile 'com.netflix.nebula:gradle-info-plugin:3.0.3' compile 'org.eclipse.jgit:org.eclipse.jgit:3.2.0.201312181205-r' compile 'com.perforce:p4java:2012.3.551082' // THIS IS SUPPOSED TO BE OPTIONAL IN THE FUTURE.... - compile 'de.thetaphi:forbiddenapis:2.5' compile 'org.apache.rat:apache-rat:0.11' compile "org.elasticsearch:jna:4.5.1" compile 'com.github.jengelman.gradle.plugins:shadow:2.0.4' diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index b63b1f40d804..d82302c84742 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -31,6 +31,11 @@ class PrecommitTasks { /** Adds a precommit task, which depends on non-test verification tasks. */ public static Task create(Project project, boolean includeDependencyLicenses) { + Configuration forbiddenApisConfiguration = project.configurations.create("forbiddenApisCliJar") + project.dependencies { + forbiddenApisCliJar ('de.thetaphi:forbiddenapis:2.5') + } + List precommitTasks = [ configureCheckstyle(project), configureForbiddenApisCli(project), @@ -39,7 +44,7 @@ class PrecommitTasks { project.tasks.create('licenseHeaders', LicenseHeadersTask.class), project.tasks.create('filepermissions', FilePermissionsTask.class), project.tasks.create('jarHell', JarHellTask.class), - project.tasks.create('thirdPartyAudit', ThirdPartyAuditTask.class) + configureThirdPartyAudit(project) ] // tasks with just tests don't need dependency licenses, so this flag makes adding @@ -75,32 +80,26 @@ class PrecommitTasks { return project.tasks.create(precommitOptions) } - private static Task configureForbiddenApisCli(Project project) { - Configuration forbiddenApisConfiguration = project.configurations.create("forbiddenApisCliJar") - project.dependencies { - forbiddenApisCliJar ('de.thetaphi:forbiddenapis:2.5') + private static Task configureThirdPartyAudit(Project project) { + ThirdPartyAuditTask thirdPartyAuditTask = project.tasks.create('thirdPartyAudit', ThirdPartyAuditTask.class) + ExportElasticsearchBuildResourcesTask buildResources = project.tasks.getByName('buildResources') + thirdPartyAuditTask.configure { + dependsOn(buildResources) + signatureFile = buildResources.copy("forbidden/third-party-audit.txt") + javaHome = project.runtimeJavaHome } - Task forbiddenApisCli = project.tasks.create('forbiddenApis') + return thirdPartyAuditTask + } + private static Task configureForbiddenApisCli(Project project) { + Task forbiddenApisCli = project.tasks.create('forbiddenApis') project.sourceSets.forEach { sourceSet -> forbiddenApisCli.dependsOn( project.tasks.create(sourceSet.getTaskName('forbiddenApis', null), ForbiddenApisCliTask) { ExportElasticsearchBuildResourcesTask buildResources = project.tasks.getByName('buildResources') dependsOn(buildResources) - execAction = { spec -> - spec.classpath = project.files( - project.configurations.forbiddenApisCliJar, - sourceSet.compileClasspath, - sourceSet.runtimeClasspath - ) - spec.executable = "${project.runtimeJavaHome}/bin/java" - } - inputs.files( - forbiddenApisConfiguration, - sourceSet.compileClasspath, - sourceSet.runtimeClasspath - ) - + it.sourceSet = sourceSet + javaHome = project.runtimeJavaHome targetCompatibility = project.compilerJavaVersion bundledSignatures = [ "jdk-unsafe", "jdk-deprecated", "jdk-non-portable", "jdk-system-out" diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy deleted file mode 100644 index 52b13a566442..000000000000 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.groovy +++ /dev/null @@ -1,297 +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.gradle.precommit; - -import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin -import org.apache.tools.ant.BuildEvent; -import org.apache.tools.ant.BuildException; -import org.apache.tools.ant.BuildListener; -import org.apache.tools.ant.BuildLogger; -import org.apache.tools.ant.DefaultLogger; -import org.apache.tools.ant.Project; -import org.elasticsearch.gradle.AntTask; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.file.FileCollection; -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputFile - -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Basic static checking to keep tabs on third party JARs - */ -public class ThirdPartyAuditTask extends AntTask { - - // patterns for classes to exclude, because we understand their issues - private List excludes = []; - - /** - * Input for the task. Set javadoc for {#link getJars} for more. Protected - * so the afterEvaluate closure in the constructor can write it. - */ - protected FileCollection jars; - - /** - * Classpath against which to run the third patty audit. Protected so the - * afterEvaluate closure in the constructor can write it. - */ - protected FileCollection classpath; - - /** - * We use a simple "marker" file that we touch when the task succeeds - * as the task output. This is compared against the modified time of the - * inputs (ie the jars/class files). - */ - @OutputFile - File successMarker = new File(project.buildDir, 'markers/thirdPartyAudit') - - ThirdPartyAuditTask() { - // we depend on this because its the only reliable configuration - // this probably makes the build slower: gradle you suck here when it comes to configurations, you pay the price. - dependsOn(project.configurations.testCompile); - description = "Checks third party JAR bytecode for missing classes, use of internal APIs, and other horrors'"; - - project.afterEvaluate { - Configuration configuration = project.configurations.findByName('runtime') - Configuration compileOnly = project.configurations.findByName('compileOnly') - if (configuration == null) { - // some projects apparently do not have 'runtime'? what a nice inconsistency, - // basically only serves to waste time in build logic! - configuration = project.configurations.findByName('testCompile') - } - assert configuration != null - if (project.plugins.hasPlugin(ShadowPlugin)) { - Configuration original = configuration - configuration = project.configurations.create('thirdPartyAudit') - configuration.extendsFrom(original, project.configurations.bundle) - } - if (compileOnly == null) { - classpath = configuration - } else { - classpath = project.files(configuration, compileOnly) - } - - // we only want third party dependencies. - jars = configuration.fileCollection({ dependency -> - dependency.group.startsWith("org.elasticsearch") == false - }); - - // we don't want provided dependencies, which we have already scanned. e.g. don't - // scan ES core's dependencies for every single plugin - if (compileOnly != null) { - jars -= compileOnly - } - inputs.files(jars) - onlyIf { jars.isEmpty() == false } - } - } - - /** - * classes that should be excluded from the scan, - * e.g. because we know what sheisty stuff those particular classes are up to. - */ - public void setExcludes(String[] classes) { - for (String s : classes) { - if (s.indexOf('*') != -1) { - throw new IllegalArgumentException("illegal third party audit exclusion: '" + s + "', wildcards are not permitted!"); - } - } - excludes = classes.sort(); - } - - /** - * Returns current list of exclusions. - */ - @Input - public List getExcludes() { - return excludes; - } - - // yes, we parse Uwe Schindler's errors to find missing classes, and to keep a continuous audit. Just don't let him know! - static final Pattern MISSING_CLASS_PATTERN = - Pattern.compile(/WARNING: The referenced class '(.*)' cannot be loaded\. Please fix the classpath\!/); - - static final Pattern VIOLATION_PATTERN = - Pattern.compile(/\s\sin ([a-zA-Z0-9\$\.]+) \(.*\)/); - - // we log everything and capture errors and handle them with our whitelist - // this is important, as we detect stale whitelist entries, workaround forbidden apis bugs, - // and it also allows whitelisting missing classes! - static class EvilLogger extends DefaultLogger { - final Set missingClasses = new TreeSet<>(); - final Map> violations = new TreeMap<>(); - String previousLine = null; - - @Override - public void messageLogged(BuildEvent event) { - if (event.getTask().getClass() == de.thetaphi.forbiddenapis.ant.AntTask.class) { - if (event.getPriority() == Project.MSG_WARN) { - Matcher m = MISSING_CLASS_PATTERN.matcher(event.getMessage()); - if (m.matches()) { - missingClasses.add(m.group(1).replace('.', '/') + ".class"); - } - - // Reset the priority of the event to DEBUG, so it doesn't - // pollute the build output - event.setMessage(event.getMessage(), Project.MSG_DEBUG); - } else if (event.getPriority() == Project.MSG_ERR) { - Matcher m = VIOLATION_PATTERN.matcher(event.getMessage()); - if (m.matches()) { - String violation = previousLine + '\n' + event.getMessage(); - String clazz = m.group(1).replace('.', '/') + ".class"; - List current = violations.get(clazz); - if (current == null) { - current = new ArrayList<>(); - violations.put(clazz, current); - } - current.add(violation); - } - previousLine = event.getMessage(); - } - } - super.messageLogged(event); - } - } - - @Override - protected BuildLogger makeLogger(PrintStream stream, int outputLevel) { - DefaultLogger log = new EvilLogger(); - log.errorPrintStream = stream; - log.outputPrintStream = stream; - log.messageOutputLevel = outputLevel; - return log; - } - - @Override - protected void runAnt(AntBuilder ant) { - ant.project.addTaskDefinition('thirdPartyAudit', de.thetaphi.forbiddenapis.ant.AntTask); - - // print which jars we are going to scan, always - // this is not the time to try to be succinct! Forbidden will print plenty on its own! - Set names = new TreeSet<>(); - for (File jar : jars) { - names.add(jar.getName()); - } - - // TODO: forbidden-apis + zipfileset gives O(n^2) behavior unless we dump to a tmpdir first, - // and then remove our temp dir afterwards. don't complain: try it yourself. - // we don't use gradle temp dir handling, just google it, or try it yourself. - - File tmpDir = new File(project.buildDir, 'tmp/thirdPartyAudit'); - - // clean up any previous mess (if we failed), then unzip everything to one directory - ant.delete(dir: tmpDir.getAbsolutePath()); - tmpDir.mkdirs(); - for (File jar : jars) { - ant.unzip(src: jar.getAbsolutePath(), dest: tmpDir.getAbsolutePath()); - } - - // convert exclusion class names to binary file names - List excludedFiles = excludes.collect {it.replace('.', '/') + ".class"} - Set excludedSet = new TreeSet<>(excludedFiles); - - // jarHellReprise - Set sheistySet = getSheistyClasses(tmpDir.toPath()); - - try { - ant.thirdPartyAudit(failOnUnsupportedJava: false, - failOnMissingClasses: false, - classpath: classpath.asPath) { - fileset(dir: tmpDir) - signatures { - string(value: getClass().getResourceAsStream('/forbidden/third-party-audit.txt').getText('UTF-8')) - } - } - } catch (BuildException ignore) {} - - EvilLogger evilLogger = null; - for (BuildListener listener : ant.project.getBuildListeners()) { - if (listener instanceof EvilLogger) { - evilLogger = (EvilLogger) listener; - break; - } - } - assert evilLogger != null; - - // keep our whitelist up to date - Set bogusExclusions = new TreeSet<>(excludedSet); - bogusExclusions.removeAll(sheistySet); - bogusExclusions.removeAll(evilLogger.missingClasses); - bogusExclusions.removeAll(evilLogger.violations.keySet()); - if (!bogusExclusions.isEmpty()) { - throw new IllegalStateException("Invalid exclusions, nothing is wrong with these classes: " + bogusExclusions); - } - - // don't duplicate classes with the JDK - sheistySet.removeAll(excludedSet); - if (!sheistySet.isEmpty()) { - throw new IllegalStateException("JAR HELL WITH JDK! " + sheistySet); - } - - // don't allow a broken classpath - evilLogger.missingClasses.removeAll(excludedSet); - if (!evilLogger.missingClasses.isEmpty()) { - throw new IllegalStateException("CLASSES ARE MISSING! " + evilLogger.missingClasses); - } - - // don't use internal classes - evilLogger.violations.keySet().removeAll(excludedSet); - if (!evilLogger.violations.isEmpty()) { - throw new IllegalStateException("VIOLATIONS WERE FOUND! " + evilLogger.violations); - } - - // clean up our mess (if we succeed) - ant.delete(dir: tmpDir.getAbsolutePath()); - - successMarker.setText("", 'UTF-8') - } - - /** - * check for sheisty classes: if they also exist in the extensions classloader, its jar hell with the jdk! - */ - private Set getSheistyClasses(Path root) { - // system.parent = extensions loader. - // note: for jigsaw, this evilness will need modifications (e.g. use jrt filesystem!). - // but groovy/gradle needs to work at all first! - ClassLoader ext = ClassLoader.getSystemClassLoader().getParent(); - assert ext != null; - - Set sheistySet = new TreeSet<>(); - Files.walkFileTree(root, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String entry = root.relativize(file).toString().replace('\\', '/'); - if (entry.endsWith(".class")) { - if (ext.getResource(entry) != null) { - sheistySet.add(entry); - } - } - return FileVisitResult.CONTINUE; - } - }); - return sheistySet; - } -} diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/StandaloneRestTestPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/StandaloneRestTestPlugin.groovy index a2484e9c5fce..a5d3b41339db 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/StandaloneRestTestPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/test/StandaloneRestTestPlugin.groovy @@ -53,6 +53,8 @@ public class StandaloneRestTestPlugin implements Plugin { // only setup tests to build project.sourceSets.create('test') + // create a compileOnly configuration as others might expect it + project.configurations.create("compileOnly") project.dependencies.add('testCompile', "org.elasticsearch.test:framework:${VersionProperties.elasticsearch}") project.eclipse.classpath.sourceSets = [project.sourceSets.test] diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/JdkJarHellCheck.java b/buildSrc/src/main/java/org/elasticsearch/gradle/JdkJarHellCheck.java new file mode 100644 index 000000000000..60de1981f982 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/JdkJarHellCheck.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.gradle; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class JdkJarHellCheck { + + private Set detected = new HashSet<>(); + + private void scanForJDKJarHell(Path root) throws IOException { + // system.parent = extensions loader. + // note: for jigsaw, this evilness will need modifications (e.g. use jrt filesystem!) + ClassLoader ext = ClassLoader.getSystemClassLoader().getParent(); + assert ext != null; + + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + String entry = root.relativize(file).toString().replace('\\', '/'); + if (entry.endsWith(".class")) { + if (ext.getResource(entry) != null) { + detected.add( + entry + .replace("/", ".") + .replace(".class","") + ); + } + } + return FileVisitResult.CONTINUE; + } + }); + } + + public Set getDetected() { + return Collections.unmodifiableSet(detected); + } + + public static void main(String[] argv) throws IOException { + JdkJarHellCheck checker = new JdkJarHellCheck(); + for (String location : argv) { + Path path = Paths.get(location); + if (Files.exists(path) == false) { + throw new IllegalArgumentException("Path does not exist: " + path); + } + checker.scanForJDKJarHell(path); + } + if (checker.getDetected().isEmpty()) { + System.exit(0); + } else { + checker.getDetected().forEach(System.out::println); + System.exit(1); + } + } + +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java index 21a0597b38af..46e5d84a2f28 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java @@ -18,10 +18,9 @@ */ package org.elasticsearch.gradle.precommit; -import de.thetaphi.forbiddenapis.cli.CliMain; -import org.gradle.api.Action; import org.gradle.api.DefaultTask; import org.gradle.api.JavaVersion; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.FileCollection; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; @@ -29,6 +28,7 @@ import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.SkipWhenEmpty; +import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskAction; import org.gradle.process.JavaExecSpec; @@ -50,7 +50,8 @@ public class ForbiddenApisCliTask extends DefaultTask { private Set suppressAnnotations = new LinkedHashSet<>(); private JavaVersion targetCompatibility; private FileCollection classesDirs; - private Action execAction; + private SourceSet sourceSet; + private String javaHome; @Input public JavaVersion getTargetCompatibility() { @@ -69,14 +70,6 @@ public void setTargetCompatibility(JavaVersion targetCompatibility) { } } - public Action getExecAction() { - return execAction; - } - - public void setExecAction(Action execAction) { - this.execAction = execAction; - } - @OutputFile public File getMarkerFile() { return new File( @@ -131,11 +124,41 @@ public void setSuppressAnnotations(Set suppressAnnotations) { this.suppressAnnotations = suppressAnnotations; } + @InputFiles + public FileCollection getClassPathFromSourceSet() { + return getProject().files( + sourceSet.getCompileClasspath(), + sourceSet.getRuntimeClasspath() + ); + } + + public void setSourceSet(SourceSet sourceSet) { + this.sourceSet = sourceSet; + } + + @InputFiles + public Configuration getForbiddenAPIsConfiguration() { + return getProject().getConfigurations().getByName("forbiddenApisCliJar"); + } + + @Input + public String getJavaHome() { + return javaHome; + } + + public void setJavaHome(String javaHome) { + this.javaHome = javaHome; + } + @TaskAction public void runForbiddenApisAndWriteMarker() throws IOException { getProject().javaexec((JavaExecSpec spec) -> { - execAction.execute(spec); - spec.setMain(CliMain.class.getName()); + spec.classpath( + getForbiddenAPIsConfiguration(), + getClassPathFromSourceSet() + ); + spec.setExecutable(getJavaHome() + "/bin/java"); + spec.setMain("de.thetaphi.forbiddenapis.cli.CliMain"); // build the command line getSignaturesFiles().forEach(file -> spec.args("-f", file.getAbsolutePath())); getSuppressAnnotations().forEach(annotation -> spec.args("--suppressannotation", annotation)); diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java new file mode 100644 index 000000000000..d1939d5c6526 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java @@ -0,0 +1,288 @@ +/* + * 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.gradle.precommit; + +import org.apache.commons.io.output.NullOutputStream; +import org.elasticsearch.gradle.JdkJarHellCheck; +import org.elasticsearch.test.NamingConventionsCheck; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.StopExecutionException; +import org.gradle.api.tasks.TaskAction; +import org.gradle.process.ExecResult; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class ThirdPartyAuditTask extends DefaultTask { + + private static final Pattern MISSING_CLASS_PATTERN = Pattern.compile( + "WARNING: The referenced class '(.*)' cannot be loaded\\. Please fix the classpath!" + ); + + private static final Pattern VIOLATION_PATTERN = Pattern.compile( + "\\s\\sin ([a-zA-Z0-9$.]+) \\(.*\\)" + ); + + /** + * patterns for classes to exclude, because we understand their issues + */ + private Set excludes = new TreeSet<>(); + + private File signatureFile; + + private String javaHome; + + @InputFiles + public Configuration getForbiddenAPIsConfiguration() { + return getProject().getConfigurations().getByName("forbiddenApisCliJar"); + } + + @InputFile + public File getSignatureFile() { + return signatureFile; + } + + public void setSignatureFile(File signatureFile) { + this.signatureFile = signatureFile; + } + + @InputFiles + public Configuration getRuntimeConfiguration() { + Configuration runtime = getProject().getConfigurations().findByName("runtime"); + if (runtime == null) { + return getProject().getConfigurations().getByName("testCompile"); + } + return runtime; + } + + @Input + public String getJavaHome() { + return javaHome; + } + + public void setJavaHome(String javaHome) { + this.javaHome = javaHome; + } + + @InputFiles + public Configuration getCompileOnlyConfiguration() { + return getProject().getConfigurations().getByName("compileOnly"); + } + + @OutputDirectory + public File getJarExpandDir() { + return new File( + new File(getProject().getBuildDir(), "precommit/thirdPartyAudit"), + getName() + ); + } + + public void setExcludes(String... classes) { + excludes.clear(); + for (String each : classes) { + if (each.indexOf('*') != -1) { + throw new IllegalArgumentException("illegal third party audit exclusion: '" + each + "', wildcards are not permitted!"); + } + excludes.add(each); + } + } + + @Input + public Set getExcludes() { + return Collections.unmodifiableSet(excludes); + } + + @TaskAction + public void runThirdPartyAudit() throws IOException { + FileCollection jars = getJarsToScan(); + + extractJars(jars); + + final String forbiddenApisOutput = runForbiddenAPIsCli(); + + final Set missingClasses = new TreeSet<>(); + Matcher missingMatcher = MISSING_CLASS_PATTERN.matcher(forbiddenApisOutput); + while (missingMatcher.find()) { + missingClasses.add(missingMatcher.group(1)); + } + + final Set violationsClasses = new TreeSet<>(); + Matcher violationMatcher = VIOLATION_PATTERN.matcher(forbiddenApisOutput); + while (violationMatcher.find()) { + violationsClasses.add(violationMatcher.group(1)); + } + + Set jdkJarHellClasses = runJdkJarHellCheck(); + + assertNoPointlessExclusions(missingClasses, violationsClasses, jdkJarHellClasses); + + assertNoMissingAndViolations(missingClasses, violationsClasses); + + assertNoJarHell(jdkJarHellClasses); + } + + private void extractJars(FileCollection jars) { + File jarExpandDir = getJarExpandDir(); + jars.forEach(jar -> + getProject().copy(spec -> { + spec.from(getProject().zipTree(jar)); + spec.into(jarExpandDir); + }) + ); + } + + private void assertNoJarHell(Set jdkJarHellClasses) { + jdkJarHellClasses.removeAll(excludes); + if (jdkJarHellClasses.isEmpty() == false) { + throw new IllegalStateException("Jar Hell with the JDK:" + formatClassList(jdkJarHellClasses)); + } + } + + private void assertNoMissingAndViolations(Set missingClasses, Set violationsClasses) { + missingClasses.removeAll(excludes); + violationsClasses.removeAll(excludes); + String missingText = formatClassList(missingClasses); + String violationsText = formatClassList(violationsClasses); + if (missingText.isEmpty() && violationsText.isEmpty()) { + getLogger().info("Third party audit passed successfully"); + } else { + throw new IllegalStateException( + "Audit of third party dependencies failed:\n" + + (missingText.isEmpty() ? "" : "Missing classes:\n" + missingText) + + (violationsText.isEmpty() ? "" : "Classes with violations:\n" + violationsText) + ); + } + } + + private void assertNoPointlessExclusions(Set missingClasses, Set violationsClasses, Set jdkJarHellClasses) { + // keep our whitelist up to date + Set bogusExclusions = new TreeSet<>(excludes); + bogusExclusions.removeAll(missingClasses); + bogusExclusions.removeAll(jdkJarHellClasses); + bogusExclusions.removeAll(violationsClasses); + if (bogusExclusions.isEmpty() == false) { + throw new IllegalStateException( + "Invalid exclusions, nothing is wrong with these classes: " + formatClassList(bogusExclusions) + ); + } + } + + private String runForbiddenAPIsCli() throws IOException { + ByteArrayOutputStream errorOut = new ByteArrayOutputStream(); + getProject().javaexec(spec -> { + spec.setExecutable(javaHome + "/bin/java"); + spec.classpath( + getForbiddenAPIsConfiguration(), + getRuntimeConfiguration(), + getCompileOnlyConfiguration() + ); + spec.setMain("de.thetaphi.forbiddenapis.cli.CliMain"); + spec.args( + "-f", getSignatureFile().getAbsolutePath(), + "-d", getJarExpandDir(), + "--allowmissingclasses" + ); + spec.setErrorOutput(errorOut); + if (getLogger().isInfoEnabled() == false) { + spec.setStandardOutput(new NullOutputStream()); + } + spec.setIgnoreExitValue(true); + }); + final String forbiddenApisOutput; + try (ByteArrayOutputStream outputStream = errorOut) { + forbiddenApisOutput = outputStream.toString(StandardCharsets.UTF_8.name()); + } + if (getLogger().isInfoEnabled()) { + getLogger().info(forbiddenApisOutput); + } + return forbiddenApisOutput; + } + + private FileCollection getJarsToScan() { + FileCollection jars = getRuntimeConfiguration() + .fileCollection(dep -> dep.getGroup().startsWith("org.elasticsearch") == false); + Configuration compileOnlyConfiguration = getCompileOnlyConfiguration(); + // don't scan provided dependencies that we already scanned, e.x. don't scan cores dependencies for every plugin + if (compileOnlyConfiguration != null) { + jars.minus(compileOnlyConfiguration); + } + if (jars.isEmpty()) { + throw new StopExecutionException("No jars to scan"); + } + return jars; + } + + private String formatClassList(Set classList) { + return classList.stream() + .map(name -> " * " + name) + .collect(Collectors.joining("\n")); + } + + private Set runJdkJarHellCheck() throws IOException { + ByteArrayOutputStream standardOut = new ByteArrayOutputStream(); + ExecResult execResult = getProject().javaexec(spec -> { + URL location = NamingConventionsCheck.class.getProtectionDomain().getCodeSource().getLocation(); + if (location.getProtocol().equals("file") == false) { + throw new GradleException("Unexpected location for NamingConventionCheck class: " + location); + } + try { + spec.classpath( + location.toURI().getPath(), + getRuntimeConfiguration(), + getCompileOnlyConfiguration() + ); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + spec.setMain(JdkJarHellCheck.class.getName()); + spec.args(getJarExpandDir()); + spec.setIgnoreExitValue(true); + spec.setExecutable(javaHome + "/bin/java"); + spec.setStandardOutput(standardOut); + }); + if (execResult.getExitValue() == 0) { + return Collections.emptySet(); + } + final String jdkJarHellCheckList; + try (ByteArrayOutputStream outputStream = standardOut) { + jdkJarHellCheckList = outputStream.toString(StandardCharsets.UTF_8.name()); + } + return new TreeSet<>(Arrays.asList(jdkJarHellCheckList.split("\\r?\\n"))); + } + + +} diff --git a/plugins/discovery-azure-classic/build.gradle b/plugins/discovery-azure-classic/build.gradle index 6f177f7b7f5b..3dae3d3642c5 100644 --- a/plugins/discovery-azure-classic/build.gradle +++ b/plugins/discovery-azure-classic/build.gradle @@ -128,7 +128,7 @@ thirdPartyAudit.excludes = [ ] // jarhell with jdk (intentionally, because jaxb was removed from default modules in java 9) -if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += [ 'javax.xml.bind.Binder', 'javax.xml.bind.ContextFinder$1', diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index b1c3b62fd6ed..e32ba6948d62 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -87,7 +87,7 @@ thirdPartyAudit.excludes = [ 'org.apache.log.Logger', ] -if (JavaVersion.current() > JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += [ 'javax.xml.bind.DatatypeConverter', 'javax.xml.bind.JAXBContext' diff --git a/plugins/ingest-attachment/build.gradle b/plugins/ingest-attachment/build.gradle index 1a6aa809de04..6cd55f682c8b 100644 --- a/plugins/ingest-attachment/build.gradle +++ b/plugins/ingest-attachment/build.gradle @@ -2106,7 +2106,27 @@ thirdPartyAudit.excludes = [ 'ucar.nc2.dataset.NetcdfDataset' ] -if (JavaVersion.current() > JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion == JavaVersion.VERSION_1_8) { + thirdPartyAudit.excludes += [ + // TODO: Why is this needed ? + 'com.sun.javadoc.ClassDoc', + 'com.sun.javadoc.Doc', + 'com.sun.javadoc.Doclet', + 'com.sun.javadoc.ExecutableMemberDoc', + 'com.sun.javadoc.FieldDoc', + 'com.sun.javadoc.MethodDoc', + 'com.sun.javadoc.PackageDoc', + 'com.sun.javadoc.Parameter', + 'com.sun.javadoc.ProgramElementDoc', + 'com.sun.javadoc.RootDoc', + 'com.sun.javadoc.SourcePosition', + 'com.sun.javadoc.Tag', + 'com.sun.javadoc.Type', + 'com.sun.tools.javadoc.Main' + ] +} + +if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += [ 'javax.activation.ActivationDataFlavor', 'javax.activation.CommandMap', diff --git a/plugins/repository-hdfs/build.gradle b/plugins/repository-hdfs/build.gradle index 6debaf5282fc..557dcaa5faed 100644 --- a/plugins/repository-hdfs/build.gradle +++ b/plugins/repository-hdfs/build.gradle @@ -582,6 +582,25 @@ thirdPartyAudit.excludes = [ 'com.squareup.okhttp.ResponseBody' ] -if (JavaVersion.current() > JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += ['javax.xml.bind.annotation.adapters.HexBinaryAdapter'] } + +if (project.runtimeJavaVersion == JavaVersion.VERSION_1_8) { + thirdPartyAudit.excludes += [ + // TODO: Why is this needed ? + 'com.sun.javadoc.AnnotationDesc', + 'com.sun.javadoc.AnnotationTypeDoc', + 'com.sun.javadoc.ClassDoc', + 'com.sun.javadoc.ConstructorDoc', + 'com.sun.javadoc.Doc', + 'com.sun.javadoc.DocErrorReporter', + 'com.sun.javadoc.FieldDoc', + 'com.sun.javadoc.LanguageVersion', + 'com.sun.javadoc.MethodDoc', + 'com.sun.javadoc.PackageDoc', + 'com.sun.javadoc.ProgramElementDoc', + 'com.sun.javadoc.RootDoc', + 'com.sun.tools.doclets.standard.Standard' + ] +} diff --git a/plugins/repository-s3/build.gradle b/plugins/repository-s3/build.gradle index 7f0ca209db79..5d248b22caf1 100644 --- a/plugins/repository-s3/build.gradle +++ b/plugins/repository-s3/build.gradle @@ -447,7 +447,7 @@ thirdPartyAudit.excludes = [ ] // jarhell with jdk (intentionally, because jaxb was removed from default modules in java 9) -if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += [ 'javax.xml.bind.Binder', 'javax.xml.bind.ContextFinder$1', diff --git a/server/build.gradle b/server/build.gradle index b22a93a702c2..f8a604941f7e 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -304,17 +304,22 @@ thirdPartyAudit.excludes = [ 'com.google.common.geometry.S2LatLng', ] -if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { - // Used by Log4J 2.11.1 +if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += [ - 'java.io.ObjectInputFilter', - 'java.io.ObjectInputFilter$Config', - 'java.io.ObjectInputFilter$FilterInfo', - 'java.io.ObjectInputFilter$Status' + // Used by Log4J 2.11.1 + 'java.io.ObjectInputFilter', + 'java.io.ObjectInputFilter$Config', + 'java.io.ObjectInputFilter$FilterInfo', + 'java.io.ObjectInputFilter$Status', + // added in 9 + 'java.lang.ProcessHandle', + 'java.lang.StackWalker', + 'java.lang.StackWalker$Option', + 'java.lang.StackWalker$StackFrame' ] } -if (JavaVersion.current() > JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += ['javax.xml.bind.DatatypeConverter'] } diff --git a/test/logger-usage/build.gradle b/test/logger-usage/build.gradle index 0f02283e5373..2da906564143 100644 --- a/test/logger-usage/build.gradle +++ b/test/logger-usage/build.gradle @@ -44,7 +44,7 @@ thirdPartyAudit.excludes = [ 'org.osgi.framework.wiring.BundleWiring' ] -if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { // Used by Log4J 2.11.1 thirdPartyAudit.excludes += [ 'java.io.ObjectInputFilter', @@ -52,4 +52,13 @@ if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { 'java.io.ObjectInputFilter$FilterInfo', 'java.io.ObjectInputFilter$Status' ] +} + +if (project.runtimeJavaVersion == JavaVersion.VERSION_1_8) { + thirdPartyAudit.excludes += [ + 'java.lang.ProcessHandle', + 'java.lang.StackWalker', + 'java.lang.StackWalker$Option', + 'java.lang.StackWalker$StackFrame' + ] } \ No newline at end of file diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index f2c78e122584..71b22531ccab 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -242,7 +242,7 @@ thirdPartyAudit.excludes = [ 'javax.persistence.EntityManagerFactory', 'javax.persistence.EntityTransaction', 'javax.persistence.LockModeType', - 'javax/persistence/Query', + 'javax.persistence.Query', // [missing classes] OpenSAML storage and HttpClient cache have optional memcache support 'net.spy.memcached.CASResponse', 'net.spy.memcached.CASValue', @@ -266,7 +266,7 @@ thirdPartyAudit.excludes = [ 'com.google.common.util.concurrent.AbstractFuture$UnsafeAtomicHelper$1', ] -if (JavaVersion.current() > JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += [ 'javax.xml.bind.JAXBContext', 'javax.xml.bind.JAXBElement', diff --git a/x-pack/plugin/sql/sql-action/build.gradle b/x-pack/plugin/sql/sql-action/build.gradle index 345318d20b80..ee99c36b9062 100644 --- a/x-pack/plugin/sql/sql-action/build.gradle +++ b/x-pack/plugin/sql/sql-action/build.gradle @@ -140,7 +140,7 @@ thirdPartyAudit.excludes = [ 'org.zeromq.ZMQ' ] -if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { // Used by Log4J 2.11.1 thirdPartyAudit.excludes += [ 'java.io.ObjectInputFilter', @@ -148,4 +148,13 @@ if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { 'java.io.ObjectInputFilter$FilterInfo', 'java.io.ObjectInputFilter$Status' ] +} + +if (project.runtimeJavaVersion == JavaVersion.VERSION_1_8) { + thirdPartyAudit.excludes += [ + 'java.lang.ProcessHandle', + 'java.lang.StackWalker', + 'java.lang.StackWalker$Option', + 'java.lang.StackWalker$StackFrame' + ] } \ No newline at end of file diff --git a/x-pack/plugin/watcher/build.gradle b/x-pack/plugin/watcher/build.gradle index 3a9d759c46d1..3412cafc4f4c 100644 --- a/x-pack/plugin/watcher/build.gradle +++ b/x-pack/plugin/watcher/build.gradle @@ -68,7 +68,7 @@ thirdPartyAudit.excludes = [ ] // pulled in as external dependency to work on java 9 -if (JavaVersion.current() <= JavaVersion.VERSION_1_8) { +if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += [ 'com.sun.activation.registries.MailcapParseException', 'javax.activation.ActivationDataFlavor', From 19ef41ee82a3e801e6380eaf35f79b004321f5eb Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Tue, 28 Aug 2018 09:06:43 +0200 Subject: [PATCH 181/283] Watcher: Simplify finding next date in cron schedule (#33015) The code introduced in 3fa36807f87ad90af593e86f9ed843ced3260973 to fix an issue with crons always returning -1 was not very readable. This implementation uses streams to improve readability. --- .../trigger/schedule/CronnableSchedule.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java index 695c9b192eaa..1e6285f71d78 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/CronnableSchedule.java @@ -30,19 +30,14 @@ private CronnableSchedule(Cron... crons) { @Override public long nextScheduledTimeAfter(long startTime, long time) { assert time >= startTime; - long nextTime = Long.MAX_VALUE; - for (Cron cron : crons) { - long nextValidTimeAfter = cron.getNextValidTimeAfter(time); - - boolean previousCronExpired = nextTime == -1; - boolean currentCronValid = nextValidTimeAfter > -1; - if (previousCronExpired && currentCronValid) { - nextTime = nextValidTimeAfter; - } else { - nextTime = Math.min(nextTime, nextValidTimeAfter); - } - } - return nextTime; + return Arrays.stream(crons) + .map(cron -> cron.getNextValidTimeAfter(time)) + // filter out expired dates before sorting + .filter(nextValidTime -> nextValidTime > -1) + .sorted() + .findFirst() + // no date in the future found, return -1 to the caller + .orElse(-1L); } public Cron[] crons() { From 9d92a87ae6579c00cbf1bf958549599a2a3cc454 Mon Sep 17 00:00:00 2001 From: Jonathan Little Date: Tue, 28 Aug 2018 01:27:43 -0700 Subject: [PATCH 182/283] Remove support for deprecated params._agg/_aggs for scripted metric aggregations (#32979) --- .../elasticsearch/gradle/BuildPlugin.groovy | 2 - docs/build.gradle | 3 - .../migrate_7_0/aggregations.asciidoc | 2 - server/build.gradle | 13 -- .../script/ScriptedMetricAggContexts.java | 37 +--- .../scripted/InternalScriptedMetric.java | 5 - .../scripted/ScriptedMetricAggregator.java | 4 +- .../ScriptedMetricAggregatorFactory.java | 15 +- ...alScriptedMetricAggStateV6CompatTests.java | 109 ----------- ...MetricAggregatorAggStateV6CompatTests.java | 180 ------------------ .../script/MockScriptEngine.java | 16 +- 11 files changed, 20 insertions(+), 366 deletions(-) delete mode 100644 server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java delete mode 100644 server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index bce00ae8f6d3..4c4a8cbe8810 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -802,8 +802,6 @@ class BuildPlugin implements Plugin { systemProperty 'tests.task', path systemProperty 'tests.security.manager', 'true' systemProperty 'jna.nosys', 'true' - // TODO: remove this deprecation compatibility setting for 7.0 - systemProperty 'es.aggregations.enable_scripted_metric_agg_param', 'false' systemProperty 'compiler.java', project.ext.compilerJavaVersion.getMajorVersion() if (project.ext.inFipsJvm) { systemProperty 'runtime.java', project.ext.runtimeJavaVersion.getMajorVersion() + "FIPS" diff --git a/docs/build.gradle b/docs/build.gradle index 8ee5c8a8e539..980c99baf832 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -41,9 +41,6 @@ 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 deprecation compatibility setting for 7.0 - systemProperty 'es.aggregations.enable_scripted_metric_agg_param', 'false' } // remove when https://github.com/elastic/elasticsearch/issues/31305 is fixed diff --git a/docs/reference/migration/migrate_7_0/aggregations.asciidoc b/docs/reference/migration/migrate_7_0/aggregations.asciidoc index b4f29935be9a..08f181b2919e 100644 --- a/docs/reference/migration/migrate_7_0/aggregations.asciidoc +++ b/docs/reference/migration/migrate_7_0/aggregations.asciidoc @@ -21,5 +21,3 @@ has been removed. `missing_bucket` should be used instead. The object used to share aggregation state between the scripts in a Scripted Metric Aggregation is now a variable called `state` available in the script context, rather than being provided via the `params` object as `params._agg`. - -The old `params._agg` variable is still available as well. diff --git a/server/build.gradle b/server/build.gradle index f8a604941f7e..aaef6d87e617 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -346,16 +346,3 @@ if (isEclipse == false || project.path == ":server-tests") { check.dependsOn integTest integTest.mustRunAfter test } - -// TODO: remove these compatibility tests in 7.0 -additionalTest('testScriptedMetricAggParamsV6Compatibility') { - include '**/ScriptedMetricAggregatorAggStateV6CompatTests.class' - include '**/InternalScriptedMetricAggStateV6CompatTests.class' - systemProperty 'es.aggregations.enable_scripted_metric_agg_param', 'true' -} - -test { - // these are tested explicitly in separate test tasks - exclude '**/ScriptedMetricAggregatorAggStateV6CompatTests.class' - exclude '**/InternalScriptedMetricAggStateV6CompatTests.class' -} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java b/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java index 0c34c59b7be5..9f6ea999a930 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptedMetricAggContexts.java @@ -22,8 +22,6 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.Scorer; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.common.logging.DeprecationLogger; -import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.search.lookup.LeafSearchLookup; import org.elasticsearch.search.lookup.SearchLookup; @@ -33,30 +31,11 @@ import java.util.Map; public class ScriptedMetricAggContexts { - private static final DeprecationLogger DEPRECATION_LOGGER = - new DeprecationLogger(Loggers.getLogger(ScriptedMetricAggContexts.class)); - - // Public for access from tests - public static final String AGG_PARAM_DEPRECATION_WARNING = - "params._agg/_aggs for scripted metric aggregations are deprecated, use state/states (not in params) instead. " + - "Use -Des.aggregations.enable_scripted_metric_agg_param=false to disable."; - - public static boolean deprecatedAggParamEnabled() { - boolean enabled = Boolean.parseBoolean( - System.getProperty("es.aggregations.enable_scripted_metric_agg_param", "true")); - - if (enabled) { - DEPRECATION_LOGGER.deprecatedAndMaybeLog("enable_scripted_metric_agg_param", AGG_PARAM_DEPRECATION_WARNING); - } - - return enabled; - } - private abstract static class ParamsAndStateBase { private final Map params; - private final Object state; + private final Map state; - ParamsAndStateBase(Map params, Object state) { + ParamsAndStateBase(Map params, Map state) { this.params = params; this.state = state; } @@ -71,14 +50,14 @@ public Object getState() { } public abstract static class InitScript extends ParamsAndStateBase { - public InitScript(Map params, Object state) { + public InitScript(Map params, Map state) { super(params, state); } public abstract void execute(); public interface Factory { - InitScript newInstance(Map params, Object state); + InitScript newInstance(Map params, Map state); } public static String[] PARAMETERS = {}; @@ -89,7 +68,7 @@ public abstract static class MapScript extends ParamsAndStateBase { private final LeafSearchLookup leafLookup; private Scorer scorer; - public MapScript(Map params, Object state, SearchLookup lookup, LeafReaderContext leafContext) { + public MapScript(Map params, Map state, SearchLookup lookup, LeafReaderContext leafContext) { super(params, state); this.leafLookup = leafContext == null ? null : lookup.getLeafSearchLookup(leafContext); @@ -131,7 +110,7 @@ public interface LeafFactory { } public interface Factory { - LeafFactory newFactory(Map params, Object state, SearchLookup lookup); + LeafFactory newFactory(Map params, Map state, SearchLookup lookup); } public static String[] PARAMETERS = new String[] {}; @@ -139,14 +118,14 @@ public interface Factory { } public abstract static class CombineScript extends ParamsAndStateBase { - public CombineScript(Map params, Object state) { + public CombineScript(Map params, Map state) { super(params, state); } public abstract Object execute(); public interface Factory { - CombineScript newInstance(Map params, Object state); + CombineScript newInstance(Map params, Map state); } public static String[] PARAMETERS = {}; diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java index 4124a8eeb76a..db0993d12967 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java @@ -95,11 +95,6 @@ public InternalAggregation doReduce(List aggregations, Redu params.putAll(firstAggregation.reduceScript.getParams()); } - // Add _aggs to params map for backwards compatibility (redundant with a context variable on the ReduceScript created below). - if (ScriptedMetricAggContexts.deprecatedAggParamEnabled()) { - params.put("_aggs", aggregationObjects); - } - ScriptedMetricAggContexts.ReduceScript.Factory factory = reduceContext.scriptService().compile( firstAggregation.reduceScript, ScriptedMetricAggContexts.ReduceScript.CONTEXT); ScriptedMetricAggContexts.ReduceScript script = factory.newInstance(params, aggregationObjects); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java index ffdff44b783b..ea7bf270b8b6 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java @@ -41,10 +41,10 @@ public class ScriptedMetricAggregator extends MetricsAggregator { private final ScriptedMetricAggContexts.MapScript.LeafFactory mapScript; private final ScriptedMetricAggContexts.CombineScript combineScript; private final Script reduceScript; - private Object aggState; + private Map aggState; protected ScriptedMetricAggregator(String name, ScriptedMetricAggContexts.MapScript.LeafFactory mapScript, ScriptedMetricAggContexts.CombineScript combineScript, - Script reduceScript, Object aggState, SearchContext context, Aggregator parent, + Script reduceScript, Map aggState, SearchContext context, Aggregator parent, List pipelineAggregators, Map metaData) throws IOException { super(name, context, parent, pipelineAggregators, metaData); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java index 076c29feceae..3b8f8321deaa 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorFactory.java @@ -80,20 +80,7 @@ public Aggregator createInternal(Aggregator parent, boolean collectsFromSingleBu aggParams = new HashMap<>(); } - // Add _agg to params map for backwards compatibility (redundant with context variables on the scripts created below). - // When this is removed, aggState (as passed to ScriptedMetricAggregator) can be changed to Map, since - // it won't be possible to completely replace it with another type as is possible when it's an entry in params. - Object aggState = new HashMap(); - if (ScriptedMetricAggContexts.deprecatedAggParamEnabled()) { - if (aggParams.containsKey("_agg") == false) { - // Add _agg if it wasn't added manually - aggParams.put("_agg", aggState); - } else { - // If it was added manually, also use it for the agg context variable to reduce the likelihood of - // weird behavior due to multiple different variables. - aggState = aggParams.get("_agg"); - } - } + Map aggState = new HashMap(); final ScriptedMetricAggContexts.InitScript initScript = this.initScript.newInstance( mergeParams(aggParams, initScriptParams), aggState); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java deleted file mode 100644 index 4abf68a960b1..000000000000 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetricAggStateV6CompatTests.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.search.aggregations.metrics.scripted; - -import org.elasticsearch.common.io.stream.Writeable.Reader; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.script.MockScriptEngine; -import org.elasticsearch.script.Script; -import org.elasticsearch.script.ScriptedMetricAggContexts; -import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.script.ScriptModule; -import org.elasticsearch.script.ScriptService; -import org.elasticsearch.script.ScriptType; -import org.elasticsearch.search.aggregations.Aggregation.CommonFields; -import org.elasticsearch.search.aggregations.ParsedAggregation; -import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; -import org.elasticsearch.test.InternalAggregationTestCase; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Predicate; - -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.sameInstance; - -/** - * This test verifies that the _aggs param is added correctly when the system property - * "es.aggregations.enable_scripted_metric_agg_param" is set to true. - */ -public class InternalScriptedMetricAggStateV6CompatTests extends InternalAggregationTestCase { - - private static final String REDUCE_SCRIPT_NAME = "reduceScript"; - - @Override - protected InternalScriptedMetric createTestInstance(String name, List pipelineAggregators, - Map metaData) { - Script reduceScript = new Script(ScriptType.INLINE, MockScriptEngine.NAME, REDUCE_SCRIPT_NAME, Collections.emptyMap()); - return new InternalScriptedMetric(name, "agg value", reduceScript, pipelineAggregators, metaData); - } - - /** - * Mock of the script service. The script that is run looks at the - * "_aggs" parameter to verify that it was put in place by InternalScriptedMetric. - */ - @Override - protected ScriptService mockScriptService() { - Function, Object> script = params -> { - Object aggs = params.get("_aggs"); - Object states = params.get("states"); - assertThat(aggs, instanceOf(List.class)); - assertThat(aggs, sameInstance(states)); - return aggs; - }; - - @SuppressWarnings("unchecked") - MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, - Collections.singletonMap(REDUCE_SCRIPT_NAME, script)); - Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); - return new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); - } - - @Override - protected void assertReduced(InternalScriptedMetric reduced, List inputs) { - assertWarnings(ScriptedMetricAggContexts.AGG_PARAM_DEPRECATION_WARNING); - } - - @Override - protected Reader instanceReader() { - return InternalScriptedMetric::new; - } - - @Override - protected void assertFromXContent(InternalScriptedMetric aggregation, ParsedAggregation parsedAggregation) {} - - @Override - protected Predicate excludePathsFromXContentInsertion() { - return path -> path.contains(CommonFields.VALUE.getPreferredName()); - } - - @Override - protected InternalScriptedMetric mutateInstance(InternalScriptedMetric instance) { - String name = instance.getName(); - Object value = instance.aggregation(); - Script reduceScript = instance.reduceScript; - List pipelineAggregators = instance.pipelineAggregators(); - Map metaData = instance.getMetaData(); - return new InternalScriptedMetric(name + randomAlphaOfLength(5), value, reduceScript, pipelineAggregators, - metaData); - } -} diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java deleted file mode 100644 index bf78cae711b9..000000000000 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregatorAggStateV6CompatTests.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.search.aggregations.metrics.scripted; - -import org.apache.lucene.document.SortedNumericDocValuesField; -import org.apache.lucene.index.DirectoryReader; -import org.apache.lucene.index.IndexReader; -import org.apache.lucene.index.RandomIndexWriter; -import org.apache.lucene.search.MatchAllDocsQuery; -import org.apache.lucene.store.Directory; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.query.QueryShardContext; -import org.elasticsearch.script.MockScriptEngine; -import org.elasticsearch.script.Script; -import org.elasticsearch.script.ScriptedMetricAggContexts; -import org.elasticsearch.script.ScriptEngine; -import org.elasticsearch.script.ScriptModule; -import org.elasticsearch.script.ScriptService; -import org.elasticsearch.script.ScriptType; -import org.elasticsearch.search.aggregations.AggregatorTestCase; -import org.junit.BeforeClass; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import static java.util.Collections.singleton; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.sameInstance; - -/** - * This test verifies that the _agg param is added correctly when the system property - * "es.aggregations.enable_scripted_metric_agg_param" is set to true. - */ -public class ScriptedMetricAggregatorAggStateV6CompatTests extends AggregatorTestCase { - - private static final String AGG_NAME = "scriptedMetric"; - private static final Script INIT_SCRIPT = new Script(ScriptType.INLINE, MockScriptEngine.NAME, "initScript", Collections.emptyMap()); - private static final Script MAP_SCRIPT = new Script(ScriptType.INLINE, MockScriptEngine.NAME, "mapScript", Collections.emptyMap()); - private static final Script COMBINE_SCRIPT = new Script(ScriptType.INLINE, MockScriptEngine.NAME, "combineScript", - Collections.emptyMap()); - - private static final Script INIT_SCRIPT_EXPLICIT_AGG = new Script(ScriptType.INLINE, MockScriptEngine.NAME, - "initScriptExplicitAgg", Collections.emptyMap()); - private static final Script MAP_SCRIPT_EXPLICIT_AGG = new Script(ScriptType.INLINE, MockScriptEngine.NAME, - "mapScriptExplicitAgg", Collections.emptyMap()); - private static final Script COMBINE_SCRIPT_EXPLICIT_AGG = new Script(ScriptType.INLINE, MockScriptEngine.NAME, - "combineScriptExplicitAgg", Collections.emptyMap()); - private static final String EXPLICIT_AGG_OBJECT = "Explicit agg object"; - - private static final Map, Object>> SCRIPTS = new HashMap<>(); - - @BeforeClass - @SuppressWarnings("unchecked") - public static void initMockScripts() { - // If _agg is provided implicitly, it should be the same objects as "state" from the context. - SCRIPTS.put("initScript", params -> { - Object agg = params.get("_agg"); - Object state = params.get("state"); - assertThat(agg, instanceOf(Map.class)); - assertThat(agg, sameInstance(state)); - return agg; - }); - SCRIPTS.put("mapScript", params -> { - Object agg = params.get("_agg"); - Object state = params.get("state"); - assertThat(agg, instanceOf(Map.class)); - assertThat(agg, sameInstance(state)); - return agg; - }); - SCRIPTS.put("combineScript", params -> { - Object agg = params.get("_agg"); - Object state = params.get("state"); - assertThat(agg, instanceOf(Map.class)); - assertThat(agg, sameInstance(state)); - return agg; - }); - - SCRIPTS.put("initScriptExplicitAgg", params -> { - Object agg = params.get("_agg"); - assertThat(agg, equalTo(EXPLICIT_AGG_OBJECT)); - return agg; - }); - SCRIPTS.put("mapScriptExplicitAgg", params -> { - Object agg = params.get("_agg"); - assertThat(agg, equalTo(EXPLICIT_AGG_OBJECT)); - return agg; - }); - SCRIPTS.put("combineScriptExplicitAgg", params -> { - Object agg = params.get("_agg"); - assertThat(agg, equalTo(EXPLICIT_AGG_OBJECT)); - return agg; - }); - } - - /** - * Test that the _agg param is implicitly added - */ - public void testWithImplicitAggParam() throws IOException { - try (Directory directory = newDirectory()) { - Integer numDocs = 10; - try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) { - for (int i = 0; i < numDocs; i++) { - indexWriter.addDocument(singleton(new SortedNumericDocValuesField("number", i))); - } - } - try (IndexReader indexReader = DirectoryReader.open(directory)) { - ScriptedMetricAggregationBuilder aggregationBuilder = new ScriptedMetricAggregationBuilder(AGG_NAME); - aggregationBuilder.initScript(INIT_SCRIPT).mapScript(MAP_SCRIPT).combineScript(COMBINE_SCRIPT); - search(newSearcher(indexReader, true, true), new MatchAllDocsQuery(), aggregationBuilder); - } - } - - assertWarnings(ScriptedMetricAggContexts.AGG_PARAM_DEPRECATION_WARNING); - } - - /** - * Test that an explicitly added _agg param is honored - */ - public void testWithExplicitAggParam() throws IOException { - try (Directory directory = newDirectory()) { - Integer numDocs = 10; - try (RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) { - for (int i = 0; i < numDocs; i++) { - indexWriter.addDocument(singleton(new SortedNumericDocValuesField("number", i))); - } - } - - Map aggParams = new HashMap<>(); - aggParams.put("_agg", EXPLICIT_AGG_OBJECT); - - try (IndexReader indexReader = DirectoryReader.open(directory)) { - ScriptedMetricAggregationBuilder aggregationBuilder = new ScriptedMetricAggregationBuilder(AGG_NAME); - aggregationBuilder - .params(aggParams) - .initScript(INIT_SCRIPT_EXPLICIT_AGG) - .mapScript(MAP_SCRIPT_EXPLICIT_AGG) - .combineScript(COMBINE_SCRIPT_EXPLICIT_AGG); - search(newSearcher(indexReader, true, true), new MatchAllDocsQuery(), aggregationBuilder); - } - } - - assertWarnings(ScriptedMetricAggContexts.AGG_PARAM_DEPRECATION_WARNING); - } - - /** - * We cannot use Mockito for mocking QueryShardContext in this case because - * script-related methods (e.g. QueryShardContext#getLazyExecutableScript) - * is final and cannot be mocked - */ - @Override - protected QueryShardContext queryShardContextMock(MapperService mapperService) { - MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME, SCRIPTS); - Map engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine); - ScriptService scriptService = new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS); - return new QueryShardContext(0, mapperService.getIndexSettings(), null, null, mapperService, null, scriptService, - xContentRegistry(), writableRegistry(), null, null, System::currentTimeMillis, null); - } -} 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 0d340a91d4ce..4e2b8259e6fb 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -242,16 +242,18 @@ public MovingFunctionScript createMovingFunctionScript() { return new MockMovingFunctionScript(); } - public ScriptedMetricAggContexts.InitScript createMetricAggInitScript(Map params, Object state) { + public ScriptedMetricAggContexts.InitScript createMetricAggInitScript(Map params, Map state) { return new MockMetricAggInitScript(params, state, script != null ? script : ctx -> 42d); } - public ScriptedMetricAggContexts.MapScript.LeafFactory createMetricAggMapScript(Map params, Object state, + public ScriptedMetricAggContexts.MapScript.LeafFactory createMetricAggMapScript(Map params, + Map state, SearchLookup lookup) { return new MockMetricAggMapScript(params, state, lookup, script != null ? script : ctx -> 42d); } - public ScriptedMetricAggContexts.CombineScript createMetricAggCombineScript(Map params, Object state) { + public ScriptedMetricAggContexts.CombineScript createMetricAggCombineScript(Map params, + Map state) { return new MockMetricAggCombineScript(params, state, script != null ? script : ctx -> 42d); } @@ -415,7 +417,7 @@ public double execute(Query query, Field field, Term term) throws IOException { public static class MockMetricAggInitScript extends ScriptedMetricAggContexts.InitScript { private final Function, Object> script; - MockMetricAggInitScript(Map params, Object state, + MockMetricAggInitScript(Map params, Map state, Function, Object> script) { super(params, state); this.script = script; @@ -436,11 +438,11 @@ public void execute() { public static class MockMetricAggMapScript implements ScriptedMetricAggContexts.MapScript.LeafFactory { private final Map params; - private final Object state; + private final Map state; private final SearchLookup lookup; private final Function, Object> script; - MockMetricAggMapScript(Map params, Object state, SearchLookup lookup, + MockMetricAggMapScript(Map params, Map state, SearchLookup lookup, Function, Object> script) { this.params = params; this.state = state; @@ -473,7 +475,7 @@ public void execute() { public static class MockMetricAggCombineScript extends ScriptedMetricAggContexts.CombineScript { private final Function, Object> script; - MockMetricAggCombineScript(Map params, Object state, + MockMetricAggCombineScript(Map params, Map state, Function, Object> script) { super(params, state); this.script = script; From cd91992c89f98ed89d6d5a8d8605f697b3d4014c Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 28 Aug 2018 06:06:22 -0400 Subject: [PATCH 183/283] Only fetch mapping updates when necessary (#33182) Today we fetch the mapping from the leader and apply it as a mapping update whenever the index metadata version on the leader changes. Yet, the index metadata can change for many reasons other than a mapping update (e.g., settings updates, adding an alias, or a replica being promoted to a primary among many other reasons). This commit builds on the addition of a mapping version to the index metadata to only fetch mapping updates when the mapping version increases. This reduces the number of these fetches and application of mappings on the follower to the bare minimum. --- .../xpack/ccr/action/ShardChangesAction.java | 22 +++---- .../xpack/ccr/action/ShardFollowNodeTask.java | 57 +++++++++---------- .../ccr/action/ShardFollowTasksExecutor.java | 2 +- .../ccr/action/ShardChangesResponseTests.java | 4 +- .../ShardFollowNodeTaskRandomTests.java | 48 ++++++++-------- .../ShardFollowNodeTaskStatusTests.java | 2 +- .../ccr/action/ShardFollowNodeTaskTests.java | 36 ++++++------ .../ShardFollowTaskReplicationTests.java | 2 +- .../rest-api-spec/test/ccr/stats.yml | 2 +- 9 files changed, 87 insertions(+), 88 deletions(-) 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 4eaf71f9c689..cfddb88b87d9 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 @@ -161,10 +161,10 @@ public String toString() { public static final class Response extends ActionResponse { - private long indexMetadataVersion; + private long mappingVersion; - public long getIndexMetadataVersion() { - return indexMetadataVersion; + public long getMappingVersion() { + return mappingVersion; } private long globalCheckpoint; @@ -188,8 +188,8 @@ public Translog.Operation[] getOperations() { Response() { } - Response(final long indexMetadataVersion, final long globalCheckpoint, final long maxSeqNo, final Translog.Operation[] operations) { - this.indexMetadataVersion = indexMetadataVersion; + Response(final long mappingVersion, final long globalCheckpoint, final long maxSeqNo, final Translog.Operation[] operations) { + this.mappingVersion = mappingVersion; this.globalCheckpoint = globalCheckpoint; this.maxSeqNo = maxSeqNo; this.operations = operations; @@ -198,7 +198,7 @@ public Translog.Operation[] getOperations() { @Override public void readFrom(final StreamInput in) throws IOException { super.readFrom(in); - indexMetadataVersion = in.readVLong(); + mappingVersion = in.readVLong(); globalCheckpoint = in.readZLong(); maxSeqNo = in.readZLong(); operations = in.readArray(Translog.Operation::readOperation, Translog.Operation[]::new); @@ -207,7 +207,7 @@ public void readFrom(final StreamInput in) throws IOException { @Override public void writeTo(final StreamOutput out) throws IOException { super.writeTo(out); - out.writeVLong(indexMetadataVersion); + out.writeVLong(mappingVersion); out.writeZLong(globalCheckpoint); out.writeZLong(maxSeqNo); out.writeArray(Translog.Operation::writeOperation, operations); @@ -218,7 +218,7 @@ public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final Response that = (Response) o; - return indexMetadataVersion == that.indexMetadataVersion && + return mappingVersion == that.mappingVersion && globalCheckpoint == that.globalCheckpoint && maxSeqNo == that.maxSeqNo && Arrays.equals(operations, that.operations); @@ -226,7 +226,7 @@ public boolean equals(final Object o) { @Override public int hashCode() { - return Objects.hash(indexMetadataVersion, globalCheckpoint, maxSeqNo, Arrays.hashCode(operations)); + return Objects.hash(mappingVersion, globalCheckpoint, maxSeqNo, Arrays.hashCode(operations)); } } @@ -252,7 +252,7 @@ protected Response shardOperation(Request request, ShardId shardId) throws IOExc IndexService indexService = indicesService.indexServiceSafe(request.getShard().getIndex()); IndexShard indexShard = indexService.getShard(request.getShard().id()); final SeqNoStats seqNoStats = indexShard.seqNoStats(); - final long indexMetaDataVersion = clusterService.state().metaData().index(shardId.getIndex()).getVersion(); + final long mappingVersion = clusterService.state().metaData().index(shardId.getIndex()).getMappingVersion(); final Translog.Operation[] operations = getOperations( indexShard, @@ -260,7 +260,7 @@ protected Response shardOperation(Request request, ShardId shardId) throws IOExc request.fromSeqNo, request.maxOperationCount, request.maxOperationSizeInBytes); - return new Response(indexMetaDataVersion, seqNoStats.getGlobalCheckpoint(), seqNoStats.getMaxSeqNo(), operations); + return new Response(mappingVersion, seqNoStats.getGlobalCheckpoint(), seqNoStats.getMaxSeqNo(), operations); } @Override 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 f2b5b7b3772d..6854a9f5741b 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 @@ -80,7 +80,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { private long followerMaxSeqNo = 0; private int numConcurrentReads = 0; private int numConcurrentWrites = 0; - private long currentIndexMetadataVersion = 0; + private long currentMappingVersion = 0; private long totalFetchTimeMillis = 0; private long numberOfSuccessfulFetches = 0; private long numberOfFailedFetches = 0; @@ -131,14 +131,13 @@ void start( this.lastRequestedSeqNo = followerGlobalCheckpoint; } - // Forcefully updates follower mapping, this gets us the leader imd version and - // makes sure that leader and follower mapping are identical. - updateMapping(imdVersion -> { + // updates follower mapping, this gets us the leader mapping version and makes sure that leader and follower mapping are identical + updateMapping(mappingVersion -> { synchronized (ShardFollowNodeTask.this) { - currentIndexMetadataVersion = imdVersion; + currentMappingVersion = mappingVersion; } - LOGGER.info("{} Started to follow leader shard {}, followGlobalCheckPoint={}, indexMetaDataVersion={}", - params.getFollowShardId(), params.getLeaderShardId(), followerGlobalCheckpoint, imdVersion); + LOGGER.info("{} Started to follow leader shard {}, followGlobalCheckPoint={}, mappingVersion={}", + params.getFollowShardId(), params.getLeaderShardId(), followerGlobalCheckpoint, mappingVersion); coordinateReads(); }); } @@ -258,7 +257,7 @@ private void sendShardChangesRequest(long from, int maxOperationCount, long maxR } void handleReadResponse(long from, long maxRequiredSeqNo, ShardChangesAction.Response response) { - maybeUpdateMapping(response.getIndexMetadataVersion(), () -> innerHandleReadResponse(from, maxRequiredSeqNo, response)); + maybeUpdateMapping(response.getMappingVersion(), () -> innerHandleReadResponse(from, maxRequiredSeqNo, response)); } /** Called when some operations are fetched from the leading */ @@ -344,16 +343,16 @@ private synchronized void handleWriteResponse(final BulkShardOperationsResponse coordinateReads(); } - private synchronized void maybeUpdateMapping(Long minimumRequiredIndexMetadataVersion, Runnable task) { - if (currentIndexMetadataVersion >= minimumRequiredIndexMetadataVersion) { - LOGGER.trace("{} index metadata version [{}] is higher or equal than minimum required index metadata version [{}]", - params.getFollowShardId(), currentIndexMetadataVersion, minimumRequiredIndexMetadataVersion); + private synchronized void maybeUpdateMapping(Long minimumRequiredMappingVersion, Runnable task) { + if (currentMappingVersion >= minimumRequiredMappingVersion) { + LOGGER.trace("{} mapping version [{}] is higher or equal than minimum required mapping version [{}]", + params.getFollowShardId(), currentMappingVersion, minimumRequiredMappingVersion); task.run(); } else { - LOGGER.trace("{} updating mapping, index metadata version [{}] is lower than minimum required index metadata version [{}]", - params.getFollowShardId(), currentIndexMetadataVersion, minimumRequiredIndexMetadataVersion); - updateMapping(imdVersion -> { - currentIndexMetadataVersion = imdVersion; + LOGGER.trace("{} updating mapping, mapping version [{}] is lower than minimum required mapping version [{}]", + params.getFollowShardId(), currentMappingVersion, minimumRequiredMappingVersion); + updateMapping(mappingVersion -> { + currentMappingVersion = mappingVersion; task.run(); }); } @@ -422,7 +421,7 @@ public synchronized Status getStatus() { numConcurrentReads, numConcurrentWrites, buffer.size(), - currentIndexMetadataVersion, + currentMappingVersion, totalFetchTimeMillis, numberOfSuccessfulFetches, numberOfFailedFetches, @@ -448,7 +447,7 @@ public static class Status implements Task.Status { 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 INDEX_METADATA_VERSION_FIELD = new ParseField("index_metadata_version"); + 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"); @@ -504,7 +503,7 @@ public static class Status implements Task.Status { 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(), INDEX_METADATA_VERSION_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); @@ -582,10 +581,10 @@ public int numberOfQueuedWrites() { return numberOfQueuedWrites; } - private final long indexMetadataVersion; + private final long mappingVersion; - public long indexMetadataVersion() { - return indexMetadataVersion; + public long mappingVersion() { + return mappingVersion; } private final long totalFetchTimeMillis; @@ -658,7 +657,7 @@ public NavigableMap fetchExceptions() { final int numberOfConcurrentReads, final int numberOfConcurrentWrites, final int numberOfQueuedWrites, - final long indexMetadataVersion, + final long mappingVersion, final long totalFetchTimeMillis, final long numberOfSuccessfulFetches, final long numberOfFailedFetches, @@ -678,7 +677,7 @@ public NavigableMap fetchExceptions() { this.numberOfConcurrentReads = numberOfConcurrentReads; this.numberOfConcurrentWrites = numberOfConcurrentWrites; this.numberOfQueuedWrites = numberOfQueuedWrites; - this.indexMetadataVersion = indexMetadataVersion; + this.mappingVersion = mappingVersion; this.totalFetchTimeMillis = totalFetchTimeMillis; this.numberOfSuccessfulFetches = numberOfSuccessfulFetches; this.numberOfFailedFetches = numberOfFailedFetches; @@ -701,7 +700,7 @@ public Status(final StreamInput in) throws IOException { this.numberOfConcurrentReads = in.readVInt(); this.numberOfConcurrentWrites = in.readVInt(); this.numberOfQueuedWrites = in.readVInt(); - this.indexMetadataVersion = in.readVLong(); + this.mappingVersion = in.readVLong(); this.totalFetchTimeMillis = in.readVLong(); this.numberOfSuccessfulFetches = in.readVLong(); this.numberOfFailedFetches = in.readVLong(); @@ -730,7 +729,7 @@ public void writeTo(final StreamOutput out) throws IOException { out.writeVInt(numberOfConcurrentReads); out.writeVInt(numberOfConcurrentWrites); out.writeVInt(numberOfQueuedWrites); - out.writeVLong(indexMetadataVersion); + out.writeVLong(mappingVersion); out.writeVLong(totalFetchTimeMillis); out.writeVLong(numberOfSuccessfulFetches); out.writeVLong(numberOfFailedFetches); @@ -756,7 +755,7 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa 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(INDEX_METADATA_VERSION_FIELD.getPreferredName(), indexMetadataVersion); + builder.field(MAPPING_VERSION_FIELD.getPreferredName(), mappingVersion); builder.humanReadableField( TOTAL_FETCH_TIME_MILLIS_FIELD.getPreferredName(), "total_fetch_time", @@ -815,7 +814,7 @@ public boolean equals(final Object o) { numberOfConcurrentReads == that.numberOfConcurrentReads && numberOfConcurrentWrites == that.numberOfConcurrentWrites && numberOfQueuedWrites == that.numberOfQueuedWrites && - indexMetadataVersion == that.indexMetadataVersion && + mappingVersion == that.mappingVersion && totalFetchTimeMillis == that.totalFetchTimeMillis && numberOfSuccessfulFetches == that.numberOfSuccessfulFetches && numberOfFailedFetches == that.numberOfFailedFetches && @@ -837,7 +836,7 @@ public int hashCode() { numberOfConcurrentReads, numberOfConcurrentWrites, numberOfQueuedWrites, - indexMetadataVersion, + mappingVersion, totalFetchTimeMillis, numberOfSuccessfulFetches, numberOfFailedFetches, 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 34ba3a2e5c69..83e3e4806e18 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 @@ -115,7 +115,7 @@ protected void innerUpdateMapping(LongConsumer handler, Consumer erro putMappingRequest.type(mappingMetaData.type()); putMappingRequest.source(mappingMetaData.source().string(), XContentType.JSON); followerClient.admin().indices().putMapping(putMappingRequest, ActionListener.wrap( - putMappingResponse -> handler.accept(indexMetaData.getVersion()), + putMappingResponse -> handler.accept(indexMetaData.getMappingVersion()), errorHandler)); }, errorHandler)); } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesResponseTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesResponseTests.java index 8e150b8f934e..e9c67097d72b 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesResponseTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardChangesResponseTests.java @@ -12,7 +12,7 @@ public class ShardChangesResponseTests extends AbstractStreamableTestCase fromToSlot = new HashMap<>(); @Override protected void innerUpdateMapping(LongConsumer handler, Consumer errorHandler) { - handler.accept(indexMetadataVersion); + handler.accept(mappingVersion); } @Override @@ -134,7 +134,7 @@ protected void innerSendShardChangesRequest(long from, int maxOperationCount, Co fromToSlot.put(from, ++slot); // if too many invocations occur with the same from then AOBE occurs, this ok and then something is wrong. } - indexMetadataVersion = testResponse.indexMetadataVersion; + mappingVersion = testResponse.mappingVersion; if (testResponse.exception != null) { errorHandler.accept(testResponse.exception); } else { @@ -187,15 +187,15 @@ private void tearDown() { }; } - private static TestRun createTestRun(long startSeqNo, long startIndexMetadataVersion, int maxOperationCount) { + private static TestRun createTestRun(long startSeqNo, long startMappingVersion, int maxOperationCount) { long prevGlobalCheckpoint = startSeqNo; - long indexMetaDataVersion = startIndexMetadataVersion; + long mappingVersion = startMappingVersion; int numResponses = randomIntBetween(16, 256); Map> responses = new HashMap<>(numResponses); for (int i = 0; i < numResponses; i++) { long nextGlobalCheckPoint = prevGlobalCheckpoint + maxOperationCount; if (sometimes()) { - indexMetaDataVersion++; + mappingVersion++; } if (sometimes()) { @@ -203,7 +203,7 @@ private static TestRun createTestRun(long startSeqNo, long startIndexMetadataVer // Sometimes add a random retryable error if (sometimes()) { Exception error = new UnavailableShardsException(new ShardId("test", "test", 0), ""); - item.add(new TestResponse(error, indexMetaDataVersion, null)); + item.add(new TestResponse(error, mappingVersion, null)); } List ops = new ArrayList<>(); for (long seqNo = prevGlobalCheckpoint; seqNo <= nextGlobalCheckPoint; seqNo++) { @@ -211,8 +211,8 @@ private static TestRun createTestRun(long startSeqNo, long startIndexMetadataVer byte[] source = "{}".getBytes(StandardCharsets.UTF_8); ops.add(new Translog.Index("doc", id, seqNo, 0, source)); } - item.add(new TestResponse(null, indexMetaDataVersion, - new ShardChangesAction.Response(indexMetaDataVersion, 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 @@ -224,13 +224,13 @@ private static TestRun createTestRun(long startSeqNo, long startIndexMetadataVer // Sometimes add a random retryable error if (sometimes()) { Exception error = new UnavailableShardsException(new ShardId("test", "test", 0), ""); - item.add(new TestResponse(error, indexMetaDataVersion, null)); + item.add(new TestResponse(error, mappingVersion, null)); } // Sometimes add an empty shard changes response to also simulate a leader shard lagging behind if (sometimes()) { ShardChangesAction.Response response = - new ShardChangesAction.Response(indexMetaDataVersion, prevGlobalCheckpoint, prevGlobalCheckpoint, EMPTY); - item.add(new TestResponse(null, indexMetaDataVersion, response)); + new ShardChangesAction.Response(mappingVersion, prevGlobalCheckpoint, prevGlobalCheckpoint, EMPTY); + item.add(new TestResponse(null, mappingVersion, response)); } List ops = new ArrayList<>(); for (long seqNo = fromSeqNo; seqNo <= toSeqNo; seqNo++) { @@ -241,14 +241,14 @@ private static TestRun createTestRun(long startSeqNo, long startIndexMetadataVer // 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(indexMetaDataVersion, localLeaderGCP, localLeaderGCP, ops.toArray(EMPTY)); - item.add(new TestResponse(null, indexMetaDataVersion, response)); + new ShardChangesAction.Response(mappingVersion, localLeaderGCP, localLeaderGCP, ops.toArray(EMPTY)); + item.add(new TestResponse(null, mappingVersion, response)); responses.put(fromSeqNo, Collections.unmodifiableList(item)); } } prevGlobalCheckpoint = nextGlobalCheckPoint + 1; } - return new TestRun(maxOperationCount, startSeqNo, startIndexMetadataVersion, indexMetaDataVersion, + return new TestRun(maxOperationCount, startSeqNo, startMappingVersion, mappingVersion, prevGlobalCheckpoint - 1, responses); } @@ -261,18 +261,18 @@ private static class TestRun { final int maxOperationCount; final long startSeqNo; - final long startIndexMetadataVersion; + final long startMappingVersion; - final long finalIndexMetaDataVerion; + final long finalMappingVersion; final long finalExpectedGlobalCheckpoint; final Map> responses; - private TestRun(int maxOperationCount, long startSeqNo, long startIndexMetadataVersion, long finalIndexMetaDataVerion, + private TestRun(int maxOperationCount, long startSeqNo, long startMappingVersion, long finalMappingVersion, long finalExpectedGlobalCheckpoint, Map> responses) { this.maxOperationCount = maxOperationCount; this.startSeqNo = startSeqNo; - this.startIndexMetadataVersion = startIndexMetadataVersion; - this.finalIndexMetaDataVerion = finalIndexMetaDataVerion; + this.startMappingVersion = startMappingVersion; + this.finalMappingVersion = finalMappingVersion; this.finalExpectedGlobalCheckpoint = finalExpectedGlobalCheckpoint; this.responses = Collections.unmodifiableMap(responses); } @@ -281,12 +281,12 @@ private TestRun(int maxOperationCount, long startSeqNo, long startIndexMetadataV private static class TestResponse { final Exception exception; - final long indexMetadataVersion; + final long mappingVersion; final ShardChangesAction.Response response; - private TestResponse(Exception exception, long indexMetadataVersion, ShardChangesAction.Response response) { + private TestResponse(Exception exception, long mappingVersion, ShardChangesAction.Response response) { this.exception = exception; - this.indexMetadataVersion = indexMetadataVersion; + this.mappingVersion = mappingVersion; this.response = response; } } 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 4eb428309195..234b7334e64f 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 @@ -65,7 +65,7 @@ protected void assertEqualInstances(final ShardFollowNodeTask.Status expectedIns assertThat(newInstance.numberOfConcurrentReads(), equalTo(expectedInstance.numberOfConcurrentReads())); assertThat(newInstance.numberOfConcurrentWrites(), equalTo(expectedInstance.numberOfConcurrentWrites())); assertThat(newInstance.numberOfQueuedWrites(), equalTo(expectedInstance.numberOfQueuedWrites())); - assertThat(newInstance.indexMetadataVersion(), equalTo(expectedInstance.indexMetadataVersion())); + assertThat(newInstance.mappingVersion(), equalTo(expectedInstance.mappingVersion())); assertThat(newInstance.totalFetchTimeMillis(), equalTo(expectedInstance.totalFetchTimeMillis())); assertThat(newInstance.numberOfSuccessfulFetches(), equalTo(expectedInstance.numberOfSuccessfulFetches())); assertThat(newInstance.numberOfFailedFetches(), equalTo(expectedInstance.numberOfFailedFetches())); 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 54aef6bd3d11..4f7c0bf16645 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 @@ -51,7 +51,7 @@ public class ShardFollowNodeTaskTests extends ESTestCase { private Queue readFailures; private Queue writeFailures; private Queue mappingUpdateFailures; - private Queue imdVersions; + private Queue mappingVersions; private Queue leaderGlobalCheckpoints; private Queue followerGlobalCheckpoints; private Queue maxSeqNos; @@ -180,7 +180,7 @@ public void testReceiveRetryableError() { for (int i = 0; i < max; i++) { readFailures.add(new ShardNotFoundException(new ShardId("leader_index", "", 0))); } - imdVersions.add(1L); + mappingVersions.add(1L); leaderGlobalCheckpoints.add(63L); maxSeqNos.add(63L); simulateResponse.set(true); @@ -327,7 +327,7 @@ public void testHandleReadResponse() { assertThat(bulkShardOperationRequests.get(0), equalTo(Arrays.asList(response.getOperations()))); ShardFollowNodeTask.Status status = task.getStatus(); - assertThat(status.indexMetadataVersion(), equalTo(0L)); + assertThat(status.mappingVersion(), equalTo(0L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); @@ -433,7 +433,7 @@ public void testMappingUpdate() { ShardFollowNodeTask task = createShardFollowTask(64, 1, 1, Integer.MAX_VALUE, Long.MAX_VALUE); startTask(task, 63, -1); - imdVersions.add(1L); + mappingVersions.add(1L); task.coordinateReads(); ShardChangesAction.Response response = generateShardChangesResponse(0, 63, 1L, 63L); task.handleReadResponse(0L, 63L, response); @@ -442,7 +442,7 @@ public void testMappingUpdate() { assertThat(bulkShardOperationRequests.get(0), equalTo(Arrays.asList(response.getOperations()))); ShardFollowNodeTask.Status status = task.getStatus(); - assertThat(status.indexMetadataVersion(), equalTo(1L)); + assertThat(status.mappingVersion(), equalTo(1L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -458,7 +458,7 @@ public void testMappingUpdateRetryableError() { for (int i = 0; i < max; i++) { mappingUpdateFailures.add(new ConnectException()); } - imdVersions.add(1L); + mappingVersions.add(1L); task.coordinateReads(); ShardChangesAction.Response response = generateShardChangesResponse(0, 63, 1L, 63L); task.handleReadResponse(0L, 63L, response); @@ -467,7 +467,7 @@ public void testMappingUpdateRetryableError() { assertThat(bulkShardOperationRequests.size(), equalTo(1)); assertThat(task.isStopped(), equalTo(false)); ShardFollowNodeTask.Status status = task.getStatus(); - assertThat(status.indexMetadataVersion(), equalTo(1L)); + assertThat(status.mappingVersion(), equalTo(1L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(1)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -483,17 +483,17 @@ public void testMappingUpdateRetryableErrorRetriedTooManyTimes() { for (int i = 0; i < max; i++) { mappingUpdateFailures.add(new ConnectException()); } - imdVersions.add(1L); + 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(imdVersions.size(), equalTo(1)); + assertThat(mappingVersions.size(), equalTo(1)); assertThat(bulkShardOperationRequests.size(), equalTo(0)); assertThat(task.isStopped(), equalTo(true)); ShardFollowNodeTask.Status status = task.getStatus(); - assertThat(status.indexMetadataVersion(), equalTo(0L)); + assertThat(status.mappingVersion(), equalTo(0L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -512,7 +512,7 @@ public void testMappingUpdateNonRetryableError() { assertThat(bulkShardOperationRequests.size(), equalTo(0)); assertThat(task.isStopped(), equalTo(true)); ShardFollowNodeTask.Status status = task.getStatus(); - assertThat(status.indexMetadataVersion(), equalTo(0L)); + assertThat(status.mappingVersion(), equalTo(0L)); assertThat(status.numberOfConcurrentReads(), equalTo(1)); assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); @@ -723,7 +723,7 @@ ShardFollowNodeTask createShardFollowTask(int maxBatchOperationCount, int maxCon readFailures = new LinkedList<>(); writeFailures = new LinkedList<>(); mappingUpdateFailures = new LinkedList<>(); - imdVersions = new LinkedList<>(); + mappingVersions = new LinkedList<>(); leaderGlobalCheckpoints = new LinkedList<>(); followerGlobalCheckpoints = new LinkedList<>(); maxSeqNos = new LinkedList<>(); @@ -738,9 +738,9 @@ protected void innerUpdateMapping(LongConsumer handler, Consumer erro return; } - Long imdVersion = imdVersions.poll(); - if (imdVersion != null) { - handler.accept(imdVersion); + final Long mappingVersion = mappingVersions.poll(); + if (mappingVersion != null) { + handler.accept(mappingVersion); } } @@ -779,7 +779,7 @@ protected void innerSendShardChangesRequest(long from, int requestBatchSize, Con } final ShardChangesAction.Response response = new ShardChangesAction.Response( - imdVersions.poll(), + mappingVersions.poll(), leaderGlobalCheckpoints.poll(), maxSeqNos.poll(), operations); @@ -805,7 +805,7 @@ public void markAsFailed(Exception e) { }; } - private static ShardChangesAction.Response generateShardChangesResponse(long fromSeqNo, long toSeqNo, long imdVersion, + private static ShardChangesAction.Response generateShardChangesResponse(long fromSeqNo, long toSeqNo, long mappingVersion, long leaderGlobalCheckPoint) { List ops = new ArrayList<>(); for (long seqNo = fromSeqNo; seqNo <= toSeqNo; seqNo++) { @@ -814,7 +814,7 @@ private static ShardChangesAction.Response generateShardChangesResponse(long fro ops.add(new Translog.Index("doc", id, seqNo, 0, source)); } return new ShardChangesAction.Response( - imdVersion, 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 32cc87612570..ec180943a3b5 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 @@ -209,7 +209,7 @@ protected void innerSendShardChangesRequest(long from, int maxOperationCount, Co try { Translog.Operation[] ops = ShardChangesAction.getOperations(indexShard, seqNoStats.getGlobalCheckpoint(), from, maxOperationCount, params.getMaxBatchSizeInBytes()); - // Hard code index metadata version, this is ok, as mapping updates are not tested here. + // 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); handler.accept(response); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml index a38698a45be4..c64cbe7690f6 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml @@ -33,7 +33,7 @@ - gte: { bar.0.number_of_concurrent_reads: 0 } - match: { bar.0.number_of_concurrent_writes: 0 } - match: { bar.0.number_of_queued_writes: 0 } - - gte: { bar.0.index_metadata_version: 0 } + - gte: { bar.0.mapping_version: 0 } - gte: { bar.0.total_fetch_time_millis: 0 } - gte: { bar.0.number_of_successful_fetches: 0 } - gte: { bar.0.number_of_failed_fetches: 0 } From 525cda03314b4b37f805b7b8522bb72b7fded13d Mon Sep 17 00:00:00 2001 From: Dominik Date: Tue, 28 Aug 2018 20:48:47 +1000 Subject: [PATCH 184/283] Minor spelling and grammar fix (#32931) --- docs/reference/sql/overview.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/sql/overview.asciidoc b/docs/reference/sql/overview.asciidoc index a72f5ca61feb..3da070a447ad 100644 --- a/docs/reference/sql/overview.asciidoc +++ b/docs/reference/sql/overview.asciidoc @@ -28,7 +28,7 @@ No need for additional hardware, processes, runtimes or libraries to query {es}; Lightweight and efficient:: -{es-sql} does not abstract {es} and its search capabilities - on the contrary, it embrases and exposes to SQL to allow proper full-text search, in real-time, in the same declarative, succint fashion. +{es-sql} does not abstract {es} and its search capabilities - on the contrary, it embraces and exposes SQL to allow proper full-text search, in real-time, in the same declarative, succint fashion. From b7c0d2830ab4f5e49de1b3803bbe5bfb09594f6a Mon Sep 17 00:00:00 2001 From: lipsill <39668292+lipsill@users.noreply.github.com> Date: Tue, 28 Aug 2018 13:16:43 +0200 Subject: [PATCH 185/283] [Docs] Remove repeating words (#33087) --- docs/java-rest/high-level/licensing/put-license.asciidoc | 2 +- docs/java-rest/high-level/migration.asciidoc | 2 +- docs/java-rest/high-level/miscellaneous/x-pack-info.asciidoc | 2 +- docs/painless/painless-debugging.asciidoc | 2 +- docs/painless/painless-operators-array.asciidoc | 2 +- .../aggregations/bucket/significantterms-aggregation.asciidoc | 4 ++-- .../aggregations/pipeline/movfn-aggregation.asciidoc | 2 +- docs/reference/cluster/remote-info.asciidoc | 2 +- docs/reference/cluster/reroute.asciidoc | 2 +- docs/reference/cluster/tasks.asciidoc | 2 +- docs/reference/modules/gateway.asciidoc | 2 +- docs/reference/monitoring/http-export.asciidoc | 2 +- docs/reference/query-dsl/span-multi-term-query.asciidoc | 2 +- docs/reference/search/request/collapse.asciidoc | 2 +- docs/reference/settings/security-settings.asciidoc | 4 ++-- docs/reference/sql/concepts.asciidoc | 2 +- docs/reference/testing/testing-framework.asciidoc | 2 +- docs/resiliency/index.asciidoc | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/java-rest/high-level/licensing/put-license.asciidoc b/docs/java-rest/high-level/licensing/put-license.asciidoc index a270d658dddb..945d447317bc 100644 --- a/docs/java-rest/high-level/licensing/put-license.asciidoc +++ b/docs/java-rest/high-level/licensing/put-license.asciidoc @@ -10,7 +10,7 @@ The license can be added or updated using the `putLicense()` method: -------------------------------------------------- include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute] -------------------------------------------------- -<1> Set the categories of information to retrieve. The the default is to +<1> Set the categories of information to retrieve. The default is to return no information which is useful for checking if {xpack} is installed but not much else. <2> A JSON document containing the license information. diff --git a/docs/java-rest/high-level/migration.asciidoc b/docs/java-rest/high-level/migration.asciidoc index ad4e0613fc14..662df0f56403 100644 --- a/docs/java-rest/high-level/migration.asciidoc +++ b/docs/java-rest/high-level/migration.asciidoc @@ -270,7 +270,7 @@ include-tagged::{doc-tests}/MigrationDocumentationIT.java[migration-cluster-heal helper requires the content type of the response to be passed as an argument and returns a `Map` of objects. Values in the map can be of any type, including inner `Map` that are used to represent the JSON object hierarchy. -<5> Retrieve the value of the `status` field in the response map, casts it as a a `String` +<5> Retrieve the value of the `status` field in the response map, casts it as a `String` object and use the `ClusterHealthStatus.fromString()` method to convert it as a `ClusterHealthStatus` object. This method throws an exception if the value does not corresponds to a valid cluster health status. diff --git a/docs/java-rest/high-level/miscellaneous/x-pack-info.asciidoc b/docs/java-rest/high-level/miscellaneous/x-pack-info.asciidoc index f877ed720db6..b432b10d3b8b 100644 --- a/docs/java-rest/high-level/miscellaneous/x-pack-info.asciidoc +++ b/docs/java-rest/high-level/miscellaneous/x-pack-info.asciidoc @@ -13,7 +13,7 @@ include-tagged::{doc-tests}/MiscellaneousDocumentationIT.java[x-pack-info-execut -------------------------------------------------- <1> Enable verbose mode. The default is `false` but `true` will return more information. -<2> Set the categories of information to retrieve. The the default is to +<2> Set the categories of information to retrieve. The default is to return no information which is useful for checking if {xpack} is installed but not much else. diff --git a/docs/painless/painless-debugging.asciidoc b/docs/painless/painless-debugging.asciidoc index 8523116616d1..c141cbc53252 100644 --- a/docs/painless/painless-debugging.asciidoc +++ b/docs/painless/painless-debugging.asciidoc @@ -5,7 +5,7 @@ Painless doesn't have a https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop[REPL] -and while it'd be nice for it to have one one day, it wouldn't tell you the +and while it'd be nice for it to have one day, it wouldn't tell you the whole story around debugging painless scripts embedded in Elasticsearch because the data that the scripts have access to or "context" is so important. For now the best way to debug embedded scripts is by throwing exceptions at choice diff --git a/docs/painless/painless-operators-array.asciidoc b/docs/painless/painless-operators-array.asciidoc index acfb87d30af1..e80a863df274 100644 --- a/docs/painless/painless-operators-array.asciidoc +++ b/docs/painless/painless-operators-array.asciidoc @@ -254,7 +254,7 @@ and `]` tokens. *Errors* * If a value other than an `int` type value or a value that is castable to an - `int` type value is specified for for a dimension's size. + `int` type value is specified for a dimension's size. *Grammar* diff --git a/docs/reference/aggregations/bucket/significantterms-aggregation.asciidoc b/docs/reference/aggregations/bucket/significantterms-aggregation.asciidoc index b6595c0d05cc..0a8a46a0b67b 100644 --- a/docs/reference/aggregations/bucket/significantterms-aggregation.asciidoc +++ b/docs/reference/aggregations/bucket/significantterms-aggregation.asciidoc @@ -433,8 +433,8 @@ Scripts can be inline (as in above example), indexed or stored on disk. For deta Available parameters in the script are [horizontal] -`_subset_freq`:: Number of documents the term appears in in the subset. -`_superset_freq`:: Number of documents the term appears in in the superset. +`_subset_freq`:: Number of documents the term appears in the subset. +`_superset_freq`:: Number of documents the term appears in the superset. `_subset_size`:: Number of documents in the subset. `_superset_size`:: Number of documents in the superset. diff --git a/docs/reference/aggregations/pipeline/movfn-aggregation.asciidoc b/docs/reference/aggregations/pipeline/movfn-aggregation.asciidoc index b05c56b88056..09fa707c5db3 100644 --- a/docs/reference/aggregations/pipeline/movfn-aggregation.asciidoc +++ b/docs/reference/aggregations/pipeline/movfn-aggregation.asciidoc @@ -307,7 +307,7 @@ POST /_search ===== stdDev Function -This function accepts a collection of doubles and and average, then returns the standard deviation of the values in that window. +This function accepts a collection of doubles and average, then returns the standard deviation of the values in that window. `null` and `NaN` values are ignored; the sum is only calculated over the real values. If the window is empty, or all values are `null`/`NaN`, `0.0` is returned as the result. diff --git a/docs/reference/cluster/remote-info.asciidoc b/docs/reference/cluster/remote-info.asciidoc index 2866d798b281..a53a26873ce9 100644 --- a/docs/reference/cluster/remote-info.asciidoc +++ b/docs/reference/cluster/remote-info.asciidoc @@ -10,7 +10,7 @@ GET /_remote/info ---------------------------------- // CONSOLE -This command returns returns connection and endpoint information keyed by +This command returns connection and endpoint information keyed by the configured remote cluster alias. [float] diff --git a/docs/reference/cluster/reroute.asciidoc b/docs/reference/cluster/reroute.asciidoc index f076a7b83585..276a43f660d8 100644 --- a/docs/reference/cluster/reroute.asciidoc +++ b/docs/reference/cluster/reroute.asciidoc @@ -31,7 +31,7 @@ POST /_cluster/reroute // CONSOLE // TEST[skip:doc tests run with only a single node] -It is important to note that that after processing any reroute commands +It is important to note that after processing any reroute commands Elasticsearch will perform rebalancing as normal (respecting the values of settings such as `cluster.routing.rebalance.enable`) in order to remain in a balanced state. For example, if the requested allocation includes moving a diff --git a/docs/reference/cluster/tasks.asciidoc b/docs/reference/cluster/tasks.asciidoc index 2e59da422243..d6dfa71b76b6 100644 --- a/docs/reference/cluster/tasks.asciidoc +++ b/docs/reference/cluster/tasks.asciidoc @@ -127,7 +127,7 @@ might look like: The new `description` field contains human readable text that identifies the particular request that the task is performing such as identifying the search request being performed by a search task like the example above. Other kinds of -task have have different descriptions, like <> which +task have different descriptions, like <> which has the search and the destination, or <> which just has the number of requests and the destination indices. Many requests will only have an empty description because more detailed information about the request is not diff --git a/docs/reference/modules/gateway.asciidoc b/docs/reference/modules/gateway.asciidoc index 76e084079399..038a4b24a853 100644 --- a/docs/reference/modules/gateway.asciidoc +++ b/docs/reference/modules/gateway.asciidoc @@ -51,7 +51,7 @@ NOTE: These settings only take effect on a full cluster restart. === Dangling indices -When a node joins the cluster, any shards stored in its local data directory +When a node joins the cluster, any shards stored in its local data directory which do not already exist in the cluster will be imported into the cluster. This functionality is intended as a best effort to help users who lose all master nodes. If a new master node is started which is unaware of diff --git a/docs/reference/monitoring/http-export.asciidoc b/docs/reference/monitoring/http-export.asciidoc index 4dfe1a0c537e..4ba93f326370 100644 --- a/docs/reference/monitoring/http-export.asciidoc +++ b/docs/reference/monitoring/http-export.asciidoc @@ -96,7 +96,7 @@ see <>. [[http-exporter-dns]] ==== Using DNS Hosts in HTTP Exporters -{monitoring} runs inside of the the JVM security manager. When the JVM has the +{monitoring} runs inside of the JVM security manager. When the JVM has the security manager enabled, the JVM changes the duration so that it caches DNS lookups indefinitely (for example, the mapping of a DNS hostname to an IP address). For this reason, if you are in an environment where the DNS response diff --git a/docs/reference/query-dsl/span-multi-term-query.asciidoc b/docs/reference/query-dsl/span-multi-term-query.asciidoc index 40bd15532984..de78d80284ed 100644 --- a/docs/reference/query-dsl/span-multi-term-query.asciidoc +++ b/docs/reference/query-dsl/span-multi-term-query.asciidoc @@ -41,5 +41,5 @@ WARNING: `span_multi` queries will hit too many clauses failure if the number of boolean query limit (defaults to 1024).To avoid an unbounded expansion you can set the <> of the multi term query to `top_terms_*` rewrite. Or, if you use `span_multi` on `prefix` query only, you can activate the <> field option of the `text` field instead. This will -rewrite any prefix query on the field to a a single term query that matches the indexed prefix. +rewrite any prefix query on the field to a single term query that matches the indexed prefix. diff --git a/docs/reference/search/request/collapse.asciidoc b/docs/reference/search/request/collapse.asciidoc index 192495e5d6d0..1ab79e36c7e9 100644 --- a/docs/reference/search/request/collapse.asciidoc +++ b/docs/reference/search/request/collapse.asciidoc @@ -217,4 +217,4 @@ Response: -------------------------------------------------- // NOTCONSOLE -NOTE: Second level of of collapsing doesn't allow `inner_hits`. \ No newline at end of file +NOTE: Second level of collapsing doesn't allow `inner_hits`. \ No newline at end of file diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index f1d8b555d562..bcc00ce30c5f 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -334,7 +334,7 @@ the filter. If not set, the user DN is passed into the filter. Defaults to Empt `unmapped_groups_as_roles`:: If set to `true`, the names of any unmapped LDAP groups are used as role names and assigned to the user. A group is considered to be _unmapped_ if it is not -not referenced in a +referenced in a {xpack-ref}/mapping-roles.html#mapping-roles-file[role-mapping file]. API-based role mappings are not considered. Defaults to `false`. @@ -479,7 +479,7 @@ this setting controls the amount of time to cache DNS lookups. Defaults to `1h`. `domain_name`:: -The domain name of Active Directory. If the the `url` and `user_search_dn` +The domain name of Active Directory. If the `url` and the `user_search_dn` settings are not specified, the cluster can derive those values from this setting. Required. diff --git a/docs/reference/sql/concepts.asciidoc b/docs/reference/sql/concepts.asciidoc index 1dc23e391fab..6098ebe91578 100644 --- a/docs/reference/sql/concepts.asciidoc +++ b/docs/reference/sql/concepts.asciidoc @@ -25,7 +25,7 @@ So let's start from the bottom; these roughly are: |`column` |`field` -|In both cases, at the lowest level, data is stored in in _named_ entries, of a variety of <>, containing _one_ value. SQL calls such an entry a _column_ while {es} a _field_. +|In both cases, at the lowest level, data is stored in _named_ entries, of a variety of <>, containing _one_ value. SQL calls such an entry a _column_ while {es} a _field_. Notice that in {es} a field can contain _multiple_ values of the same type (esentially a list) while in SQL, a _column_ can contain _exactly_ one value of said type. {es-sql} will do its best to preserve the SQL semantic and, depending on the query, reject those that return fields with more than one value. diff --git a/docs/reference/testing/testing-framework.asciidoc b/docs/reference/testing/testing-framework.asciidoc index dfc7371dd375..321122d81f50 100644 --- a/docs/reference/testing/testing-framework.asciidoc +++ b/docs/reference/testing/testing-framework.asciidoc @@ -230,7 +230,7 @@ As many Elasticsearch tests are checking for a similar output, like the amount o `assertMatchCount()`:: Asserts a matching count from a percolation response `assertFirstHit()`:: Asserts the first hit hits the specified matcher `assertSecondHit()`:: Asserts the second hit hits the specified matcher -`assertThirdHit()`:: Asserts the third hits hits the specified matcher +`assertThirdHit()`:: Asserts the third hit hits the specified matcher `assertSearchHit()`:: Assert a certain element in a search response hits the specified matcher `assertNoFailures()`:: Asserts that no shard failures have occurred in the response `assertFailures()`:: Asserts that shard failures have happened during a search request diff --git a/docs/resiliency/index.asciidoc b/docs/resiliency/index.asciidoc index aac8c192372c..8157e51e5e0c 100644 --- a/docs/resiliency/index.asciidoc +++ b/docs/resiliency/index.asciidoc @@ -459,7 +459,7 @@ Upgrading indices create with Lucene 3.x (Elasticsearch v0.20 and before) to Luc [float] === Improve error handling when deleting files (STATUS: DONE, v1.4.0.Beta1) -Lucene uses reference counting to prevent files that are still in use from being deleted. Lucene testing discovered a bug ({JIRA}5919[LUCENE-5919]) when decrementing the ref count on a batch of files. If deleting some of the files resulted in an exception (e.g. due to interference from a virus scanner), the files that had had their ref counts decremented successfully could later have their ref counts deleted again, incorrectly, resulting in files being physically deleted before their time. This is fixed in Lucene 4.10. +Lucene uses reference counting to prevent files that are still in use from being deleted. Lucene testing discovered a bug ({JIRA}5919[LUCENE-5919]) when decrementing the ref count on a batch of files. If deleting some of the files resulted in an exception (e.g. due to interference from a virus scanner), the files that had their ref counts decremented successfully could later have their ref counts deleted again, incorrectly, resulting in files being physically deleted before their time. This is fixed in Lucene 4.10. [float] === Using Lucene Checksums to verify shards during snapshot/restore (STATUS:DONE, v1.3.3) From d7e4a493554037559bbee48a71a97c14ec793dda Mon Sep 17 00:00:00 2001 From: Jilles van Gurp Date: Tue, 28 Aug 2018 14:25:55 +0200 Subject: [PATCH 186/283] [Docs] Add link to es-kotlin-wrapper-client (#32618) ES Kotlin Wrapper client is a library that wraps the official Highlevel Elasticsearch HTTP client for Java. --- docs/community-clients/index.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/community-clients/index.asciidoc b/docs/community-clients/index.asciidoc index e28ec84f0879..3266d3e365c5 100644 --- a/docs/community-clients/index.asciidoc +++ b/docs/community-clients/index.asciidoc @@ -132,6 +132,9 @@ The following project appears to be abandoned: * https://github.com/mbuhot/eskotlin[ES Kotlin]: Elasticsearch Query DSL for kotlin based on the {client}/java-api/current/index.html[official Elasticsearch Java client]. + +* https://github.com/jillesvangurp/es-kotlin-wrapper-client[ES Kotlin Wrapper Client]: + Kotlin extension functions and abstractions for the {client}/java-api/current/index.html[official Elasticsearch Highlevel Client]. Aims to reduce the amount of boilerplate needed to do searches, bulk indexing and other common things users do with the client. [[lua]] == Lua From d8a1b7cb177aa8b8732e5ef072cb139695dc6d1c Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 28 Aug 2018 08:48:42 -0400 Subject: [PATCH 187/283] Make soft-deletes settings final (#33172) For now, we do not support changing the soft-deletes setting even with closed indices. Therefore we should make it a final setting. Relates #29530 --- .../main/java/org/elasticsearch/index/IndexSettings.java | 2 +- .../java/org/elasticsearch/index/IndexSettingsTests.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index f6cb58a0676f..4e1176c902e8 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -242,7 +242,7 @@ public final class IndexSettings { * Specifies if the index should use soft-delete instead of hard-delete for update/delete operations. */ public static final Setting INDEX_SOFT_DELETES_SETTING = - Setting.boolSetting("index.soft_deletes.enabled", true, Property.IndexScope); + Setting.boolSetting("index.soft_deletes.enabled", true, Property.IndexScope, Property.Final); /** * Controls how many soft-deleted documents will be kept around before being merged away. Keeping more deleted diff --git a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java index b7da5add2acf..64a2fa69bcbd 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java @@ -553,4 +553,12 @@ public void testQueryDefaultField() { ); assertThat(index.getDefaultFields(), equalTo(Arrays.asList("body", "title"))); } + + public void testUpdateSoftDeletesFails() { + IndexScopedSettings settings = new IndexScopedSettings(Settings.EMPTY, IndexScopedSettings.BUILT_IN_INDEX_SETTINGS); + IllegalArgumentException error = expectThrows(IllegalArgumentException.class, () -> + settings.updateSettings(Settings.builder().put("index.soft_deletes.enabled", randomBoolean()).build(), + Settings.builder(), Settings.builder(), "index")); + assertThat(error.getMessage(), equalTo("final index setting [index.soft_deletes.enabled], not updateable")); + } } From 5954354e6200f8fa1d3304e5bff304adf2b17aa8 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 28 Aug 2018 08:53:45 -0400 Subject: [PATCH 188/283] Fix ShardFollowNodeTask.Status equals and hash code (#33189) These were broken when fetch exceptions were introduced to the status object but equals and hash code were not updated then. This commit addresses that. --- .../xpack/ccr/action/ShardFollowNodeTask.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) 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 6854a9f5741b..80d6ed4cb4ab 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 @@ -687,7 +687,7 @@ public NavigableMap fetchExceptions() { this.numberOfSuccessfulBulkOperations = numberOfSuccessfulBulkOperations; this.numberOfFailedBulkOperations = numberOfFailedBulkOperations; this.numberOfOperationsIndexed = numberOfOperationsIndexed; - this.fetchExceptions = fetchExceptions; + this.fetchExceptions = Objects.requireNonNull(fetchExceptions); } public Status(final StreamInput in) throws IOException { @@ -821,7 +821,15 @@ public boolean equals(final Object o) { operationsReceived == that.operationsReceived && totalTransferredBytes == that.totalTransferredBytes && numberOfSuccessfulBulkOperations == that.numberOfSuccessfulBulkOperations && - numberOfFailedBulkOperations == that.numberOfFailedBulkOperations; + 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)); } @Override @@ -843,8 +851,18 @@ public int hashCode() { operationsReceived, totalTransferredBytes, numberOfSuccessfulBulkOperations, - numberOfFailedBulkOperations); - + 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)); + } + + private static List getFetchExceptionMessages(final Status status) { + return status.fetchExceptions().values().stream().map(ElasticsearchException::getMessage).collect(Collectors.toList()); } public String toString() { From 79b507dbf58051aad2e4ffea00d443e25595088d Mon Sep 17 00:00:00 2001 From: Jake Landis Date: Tue, 28 Aug 2018 07:11:20 -0700 Subject: [PATCH 189/283] ingest: Introduce the dissect processor (#32884) * ingest: Introduce the dissect processor The ingest node dissect processor is an alternative to Grok to split a string based on a pattern. Dissect differs from Grok such that regular expressions are not used to split the string. Dissect can be used to parse a source text field with a simpler pattern, and is often faster the Grok for basic string parsing. This processor uses the dissect library which does most of the work. --- docs/reference/ingest/ingest-node.asciidoc | 193 ++++++++++++++++++ modules/ingest-common/build.gradle | 1 + .../ingest/common/DissectProcessor.java | 76 +++++++ .../ingest/common/IngestCommonPlugin.java | 1 + .../common/DissectProcessorFactoryTests.java | 92 +++++++++ .../ingest/common/DissectProcessorTests.java | 114 +++++++++++ .../test/ingest/200_dissect_processor.yml | 90 ++++++++ 7 files changed, 567 insertions(+) create mode 100644 modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DissectProcessor.java create mode 100644 modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java create mode 100644 modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java create mode 100644 modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/200_dissect_processor.yml diff --git a/docs/reference/ingest/ingest-node.asciidoc b/docs/reference/ingest/ingest-node.asciidoc index 37c616b23499..0241751a4df3 100644 --- a/docs/reference/ingest/ingest-node.asciidoc +++ b/docs/reference/ingest/ingest-node.asciidoc @@ -1049,6 +1049,199 @@ understands this to mean `2016-04-01` as is explained in the <>, dissect also extracts structured fields out of a single text field +within a document. However unlike the <>, dissect does not use +https://en.wikipedia.org/wiki/Regular_expression[Regular Expressions]. This allows dissect's syntax to be simple and for +some cases faster than the <>. + +Dissect matches a single text field against a defined pattern. + +For example the following pattern: +[source,txt] +-------------------------------------------------- +%{clientip} %{ident} %{auth} [%{@timestamp}] \"%{verb} %{request} HTTP/%{httpversion}\" %{status} %{size} +-------------------------------------------------- +will match a log line of this format: +[source,txt] +-------------------------------------------------- +1.2.3.4 - - [30/Apr/1998:22:00:52 +0000] \"GET /english/venues/cities/images/montpellier/18.gif HTTP/1.0\" 200 3171 +-------------------------------------------------- +and result in a document with the following fields: +[source,js] +-------------------------------------------------- +"doc": { + "_index": "_index", + "_type": "_type", + "_id": "_id", + "_source": { + "request": "/english/venues/cities/images/montpellier/18.gif", + "auth": "-", + "ident": "-", + "verb": "GET", + "@timestamp": "30/Apr/1998:22:00:52 +0000", + "size": "3171", + "clientip": "1.2.3.4", + "httpversion": "1.0", + "status": "200" + } +} +-------------------------------------------------- +// NOTCONSOLE + +A dissect pattern is defined by the parts of the string that will be discarded. In the example above the first part +to be discarded is a single space. Dissect finds this space, then assigns the value of `clientip` is everything up +until that space. +Later dissect matches the `[` and then `]` and then assigns `@timestamp` to everything in-between `[` and `]`. +Paying special attention the parts of the string to discard will help build successful dissect patterns. + +Successful matches require all keys in a pattern to have a value. If any of the `%{keyname}` defined in the pattern do +not have a value, then an exception is thrown and may be handled by the <> directive. +An empty key `%{}` or a <> can be used to match values, but exclude the value from +the final document. All matched values are represented as string data types. The <> +may be used to convert to expected data type. + +Dissect also supports <> that can change dissect's default +behavior. For example you can instruct dissect to ignore certain fields, append fields, skip over padding, etc. +See <> for more information. + +[[dissect-options]] +.Dissect Options +[options="header"] +|====== +| Name | Required | Default | Description +| `field` | yes | - | The field to dissect +| `pattern` | yes | - | The pattern to apply to the field +| `append_separator`| no | "" (empty string) | The character(s) that separate the appended fields. +| `ignore_missing` | no | false | If `true` and `field` does not exist or is `null`, the processor quietly exits without modifying the document +| ` +|====== + +[source,js] +-------------------------------------------------- +{ + "dissect": { + "field": "message", + "pattern" : "%{clientip} %{ident} %{auth} [%{@timestamp}] \"%{verb} %{request} HTTP/%{httpversion}\" %{status} %{size}" + } +} +-------------------------------------------------- +// NOTCONSOLE +[[dissect-key-modifiers]] +==== Dissect key modifiers +Key modifiers can change the default behavior for dissection. Key modifiers may be found on the left or right +of the `%{keyname}` always inside the `%{` and `}`. For example `%{+keyname ->}` has the append and right padding +modifiers. + +.Dissect Key Modifiers +[options="header"] +|====== +| Modifier | Name | Position | Example | Description | Details +| `->` | Skip right padding | (far) right | `%{keyname1->}` | Skips any repeated characters to the right | <> +| `+` | Append | left | `%{+keyname} %{+keyname}` | Appends two or more fields together | <> +| `+` with `/n` | Append with order | left and right | `%{+keyname/2} %{+keyname/1}` | Appends two or more fields together in the order specified | <> +| `?` | Named skip key | left | `%{?ignoreme}` | Skips the matched value in the output. Same behavior as `%{}`| <> +| `*` and `&` | Reference keys | left | `%{*r1} %{&r1}` | Sets the output key as value of `*` and output value of `&` | <> +| ` +|====== + +[[dissect-modifier-skip-right-padding]] +===== Right padding modifier (`->`) + +The algorithm that performs the dissection is very strict in that it requires all characters in the pattern to match +the source string. For example, the pattern `%{fookey} %{barkey}` (1 space), will match the string "foo{nbsp}bar" +(1 space), but will not match the string "foo{nbsp}{nbsp}bar" (2 spaces) since the pattern has only 1 space and the +source string has 2 spaces. + +The right padding modifier helps with this case. Adding the right padding modifier to the pattern `%{fookey->} %{barkey}`, +It will now will match "foo{nbsp}bar" (1 space) and "foo{nbsp}{nbsp}bar" (2 spaces) +and even "foo{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}bar" (10 spaces). + +Use the right padding modifier to allow for repetition of the characters after a `%{keyname->}`. + +The right padding modifier may be placed on any key with any other modifiers. It should always be the furthest right +modifier. For example: `%{+keyname/1->}` and `%{->}` + +Right padding modifier example +|====== +| *Pattern* | `%{ts->} %{level}` +| *Input* | 1998-08-10T17:15:42,466{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}WARN +| *Result* a| +* ts = 1998-08-10T17:15:42,466 +* level = WARN +|====== + +The right padding modifier may be used with an empty key to help skip unwanted data. For example, the same input string, but wrapped with brackets requires the use of an empty right padded key to achieve the same result. + +Right padding modifier with empty key example +|====== +| *Pattern* | `[%{ts}]%{->}[%{level}]` +| *Input* | [1998-08-10T17:15:42,466]{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}[WARN] +| *Result* a| +* ts = 1998-08-10T17:15:42,466 +* level = WARN +|====== + +===== Append modifier (`+`) +[[dissect-modifier-append-key]] +Dissect supports appending two or more results together for the output. +Values are appended left to right. An append separator can be specified. +In this example the append_separator is defined as a space. + +Append modifier example +|====== +| *Pattern* | `%{+name} %{+name} %{+name} %{+name}` +| *Input* | john jacob jingleheimer schmidt +| *Result* a| +* name = john jacob jingleheimer schmidt +|====== + +===== Append with order modifier (`+` and `/n`) +[[dissect-modifier-append-key-with-order]] +Dissect supports appending two or more results together for the output. +Values are appended based on the order defined (`/n`). An append separator can be specified. +In this example the append_separator is defined as a comma. + +Append with order modifier example +|====== +| *Pattern* | `%{+name/2} %{+name/4} %{+name/3} %{+name/1}` +| *Input* | john jacob jingleheimer schmidt +| *Result* a| +* name = schmidt,john,jingleheimer,jacob +|====== + +===== Named skip key (`?`) +[[dissect-modifier-named-skip-key]] +Dissect supports ignoring matches in the final result. This can be done with an empty key `%{}`, but for readability +it may be desired to give that empty key a name. + +Named skip key modifier example +|====== +| *Pattern* | `%{clientip} %{?ident} %{?auth} [%{@timestamp}]` +| *Input* | 1.2.3.4 - - [30/Apr/1998:22:00:52 +0000] +| *Result* a| +* ip = 1.2.3.4 +* @timestamp = 30/Apr/1998:22:00:52 +0000 +|====== + +===== Reference keys (`*` and `&`) +[[dissect-modifier-reference-keys]] +Dissect support using parsed values as the key/value pairings for the structured content. Imagine a system that +partially logs in key/value pairs. Reference keys allow you to maintain that key/value relationship. + +Reference key modifier example +|====== +| *Pattern* | `[%{ts}] [%{level}] %{*p1}:%{&p1} %{*p2}:%{&p2}` +| *Input* | [2018-08-10T17:15:42,466] [ERR] ip:1.2.3.4 error:REFUSED +| *Result* a| +* ts = 1998-08-10T17:15:42,466 +* level = ERR +* ip = 1.2.3.4 +* error = REFUSED +|====== + [[dot-expand-processor]] === Dot Expander Processor diff --git a/modules/ingest-common/build.gradle b/modules/ingest-common/build.gradle index 4f35bbee28df..1681258e7c7e 100644 --- a/modules/ingest-common/build.gradle +++ b/modules/ingest-common/build.gradle @@ -26,6 +26,7 @@ esplugin { dependencies { compileOnly project(':modules:lang-painless') compile project(':libs:grok') + compile project(':libs:dissect') } compileJava.options.compilerArgs << "-Xlint:-unchecked,-rawtypes" diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DissectProcessor.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DissectProcessor.java new file mode 100644 index 000000000000..58f04ccdd431 --- /dev/null +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DissectProcessor.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.ingest.common; + +import org.elasticsearch.dissect.DissectParser; +import org.elasticsearch.ingest.AbstractProcessor; +import org.elasticsearch.ingest.ConfigurationUtils; +import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.ingest.Processor; + +import java.util.Map; + +public final class DissectProcessor extends AbstractProcessor { + + public static final String TYPE = "dissect"; + //package private members for testing + final String field; + final boolean ignoreMissing; + final String pattern; + final String appendSeparator; + final DissectParser dissectParser; + + DissectProcessor(String tag, String field, String pattern, String appendSeparator, boolean ignoreMissing) { + super(tag); + this.field = field; + this.ignoreMissing = ignoreMissing; + this.pattern = pattern; + this.appendSeparator = appendSeparator; + this.dissectParser = new DissectParser(pattern, appendSeparator); + } + + @Override + public void execute(IngestDocument ingestDocument) { + String input = ingestDocument.getFieldValue(field, String.class, ignoreMissing); + if (input == null && ignoreMissing) { + return; + } else if (input == null) { + throw new IllegalArgumentException("field [" + field + "] is null, cannot process it."); + } + dissectParser.parse(input).forEach(ingestDocument::setFieldValue); + } + + @Override + public String getType() { + return TYPE; + } + + public static final class Factory implements Processor.Factory { + + @Override + public DissectProcessor create(Map registry, String processorTag, Map config) { + String field = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "field"); + String pattern = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "pattern"); + String appendSeparator = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "append_separator", ""); + boolean ignoreMissing = ConfigurationUtils.readBooleanProperty(TYPE, processorTag, config, "ignore_missing", false); + return new DissectProcessor(processorTag, field, pattern, appendSeparator, ignoreMissing); + } + } +} diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java index bc475a2a0053..b85bf085dabb 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java @@ -82,6 +82,7 @@ public Map getProcessors(Processor.Parameters paramet processors.put(KeyValueProcessor.TYPE, new KeyValueProcessor.Factory()); processors.put(URLDecodeProcessor.TYPE, new URLDecodeProcessor.Factory()); processors.put(BytesProcessor.TYPE, new BytesProcessor.Factory()); + processors.put(DissectProcessor.TYPE, new DissectProcessor.Factory()); return Collections.unmodifiableMap(processors); } diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java new file mode 100644 index 000000000000..ba1b2bd1eb57 --- /dev/null +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorFactoryTests.java @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.ingest.common; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.dissect.DissectException; +import org.elasticsearch.ingest.RandomDocumentPicks; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.is; + +public class DissectProcessorFactoryTests extends ESTestCase { + + public void testCreate() { + DissectProcessor.Factory factory = new DissectProcessor.Factory(); + String fieldName = RandomDocumentPicks.randomFieldName(random()); + String processorTag = randomAlphaOfLength(10); + String pattern = "%{a},%{b},%{c}"; + String appendSeparator = ":"; + + Map config = new HashMap<>(); + config.put("field", fieldName); + config.put("pattern", pattern); + config.put("append_separator", appendSeparator); + config.put("ignore_missing", true); + + DissectProcessor processor = factory.create(null, processorTag, config); + assertThat(processor.getTag(), equalTo(processorTag)); + assertThat(processor.field, equalTo(fieldName)); + assertThat(processor.pattern, equalTo(pattern)); + assertThat(processor.appendSeparator, equalTo(appendSeparator)); + assertThat(processor.dissectParser, is(notNullValue())); + assertThat(processor.ignoreMissing, is(true)); + } + + public void testCreateMissingField() { + DissectProcessor.Factory factory = new DissectProcessor.Factory(); + Map config = new HashMap<>(); + config.put("pattern", "%{a},%{b},%{c}"); + Exception e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, "_tag", config)); + assertThat(e.getMessage(), Matchers.equalTo("[field] required property is missing")); + } + + public void testCreateMissingPattern() { + DissectProcessor.Factory factory = new DissectProcessor.Factory(); + Map config = new HashMap<>(); + config.put("field", randomAlphaOfLength(10)); + Exception e = expectThrows(ElasticsearchParseException.class, () -> factory.create(null, "_tag", config)); + assertThat(e.getMessage(), Matchers.equalTo("[pattern] required property is missing")); + } + + public void testCreateMissingOptionals() { + DissectProcessor.Factory factory = new DissectProcessor.Factory(); + Map config = new HashMap<>(); + config.put("pattern", "%{a},%{b},%{c}"); + config.put("field", randomAlphaOfLength(10)); + DissectProcessor processor = factory.create(null, "_tag", config); + assertThat(processor.appendSeparator, equalTo("")); + assertThat(processor.ignoreMissing, is(false)); + } + + public void testCreateBadPattern() { + DissectProcessor.Factory factory = new DissectProcessor.Factory(); + Map config = new HashMap<>(); + config.put("pattern", "no keys defined"); + config.put("field", randomAlphaOfLength(10)); + expectThrows(DissectException.class, () -> factory.create(null, "_tag", config)); + } +} diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java new file mode 100644 index 000000000000..bb5d26d01a86 --- /dev/null +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/DissectProcessorTests.java @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.ingest.common; + +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.dissect.DissectException; +import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.ingest.Processor; +import org.elasticsearch.ingest.RandomDocumentPicks; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.CoreMatchers; + +import java.util.Collections; +import java.util.HashMap; + +import static org.elasticsearch.ingest.IngestDocumentMatcher.assertIngestDocument; +import static org.hamcrest.Matchers.equalTo; + +/** + * Basic tests for the {@link DissectProcessor}. See the {@link org.elasticsearch.dissect.DissectParser} test suite for a comprehensive + * set of dissect tests. + */ +public class DissectProcessorTests extends ESTestCase { + + public void testMatch() { + IngestDocument ingestDocument = new IngestDocument("_index", "_type", "_id", null, null, null, + Collections.singletonMap("message", "foo,bar,baz")); + DissectProcessor dissectProcessor = new DissectProcessor("", "message", "%{a},%{b},%{c}", "", true); + dissectProcessor.execute(ingestDocument); + assertThat(ingestDocument.getFieldValue("a", String.class), equalTo("foo")); + assertThat(ingestDocument.getFieldValue("b", String.class), equalTo("bar")); + assertThat(ingestDocument.getFieldValue("c", String.class), equalTo("baz")); + } + + public void testMatchOverwrite() { + IngestDocument ingestDocument = new IngestDocument("_index", "_type", "_id", null, null, null, + MapBuilder.newMapBuilder() + .put("message", "foo,bar,baz") + .put("a", "willgetstompped") + .map()); + assertThat(ingestDocument.getFieldValue("a", String.class), equalTo("willgetstompped")); + DissectProcessor dissectProcessor = new DissectProcessor("", "message", "%{a},%{b},%{c}", "", true); + dissectProcessor.execute(ingestDocument); + assertThat(ingestDocument.getFieldValue("a", String.class), equalTo("foo")); + assertThat(ingestDocument.getFieldValue("b", String.class), equalTo("bar")); + assertThat(ingestDocument.getFieldValue("c", String.class), equalTo("baz")); + } + + public void testAdvancedMatch() { + IngestDocument ingestDocument = new IngestDocument("_index", "_type", "_id", null, null, null, + Collections.singletonMap("message", "foo bar,,,,,,,baz nope:notagain 😊 🐇 🙃")); + DissectProcessor dissectProcessor = + new DissectProcessor("", "message", "%{a->} %{*b->},%{&b} %{}:%{?skipme} %{+smile/2} 🐇 %{+smile/1}", "::::", true); + dissectProcessor.execute(ingestDocument); + assertThat(ingestDocument.getFieldValue("a", String.class), equalTo("foo")); + assertThat(ingestDocument.getFieldValue("bar", String.class), equalTo("baz")); + expectThrows(IllegalArgumentException.class, () -> ingestDocument.getFieldValue("nope", String.class)); + expectThrows(IllegalArgumentException.class, () -> ingestDocument.getFieldValue("notagain", String.class)); + assertThat(ingestDocument.getFieldValue("smile", String.class), equalTo("🙃::::😊")); + } + + public void testMiss() { + IngestDocument ingestDocument = new IngestDocument("_index", "_type", "_id", null, null, null, + Collections.singletonMap("message", "foo:bar,baz")); + DissectProcessor dissectProcessor = new DissectProcessor("", "message", "%{a},%{b},%{c}", "", true); + DissectException e = expectThrows(DissectException.class, () -> dissectProcessor.execute(ingestDocument)); + assertThat(e.getMessage(), CoreMatchers.containsString("Unable to find match for dissect pattern")); + } + + public void testNonStringValueWithIgnoreMissing() { + String fieldName = RandomDocumentPicks.randomFieldName(random()); + Processor processor = new DissectProcessor("", fieldName, "%{a},%{b},%{c}", "", true); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), new HashMap<>()); + ingestDocument.setFieldValue(fieldName, randomInt()); + Exception e = expectThrows(IllegalArgumentException.class, () -> processor.execute(ingestDocument)); + assertThat(e.getMessage(), equalTo("field [" + fieldName + "] of type [java.lang.Integer] cannot be cast to [java.lang.String]")); + } + + public void testNullValueWithIgnoreMissing() throws Exception { + String fieldName = RandomDocumentPicks.randomFieldName(random()); + Processor processor = new DissectProcessor("", fieldName, "%{a},%{b},%{c}", "", true); + IngestDocument originalIngestDocument = RandomDocumentPicks + .randomIngestDocument(random(), Collections.singletonMap(fieldName, null)); + IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); + processor.execute(ingestDocument); + assertIngestDocument(originalIngestDocument, ingestDocument); + } + + public void testNullValueWithOutIgnoreMissing() { + String fieldName = RandomDocumentPicks.randomFieldName(random()); + Processor processor = new DissectProcessor("", fieldName, "%{a},%{b},%{c}", "", false); + IngestDocument originalIngestDocument = RandomDocumentPicks + .randomIngestDocument(random(), Collections.singletonMap(fieldName, null)); + IngestDocument ingestDocument = new IngestDocument(originalIngestDocument); + expectThrows(IllegalArgumentException.class, () -> processor.execute(ingestDocument)); + } +} diff --git a/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/200_dissect_processor.yml b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/200_dissect_processor.yml new file mode 100644 index 000000000000..1a7c2e593d43 --- /dev/null +++ b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/200_dissect_processor.yml @@ -0,0 +1,90 @@ +--- +teardown: +- do: + ingest.delete_pipeline: + id: "my_pipeline" + ignore: 404 + +--- +"Test dissect processor match": +- do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "dissect" : { + "field" : "message", + "pattern" : "%{a} %{b} %{c}" + } + } + ] + } +- match: { acknowledged: true } + +- do: + index: + index: test + type: test + id: 1 + pipeline: "my_pipeline" + body: {message: "foo bar baz"} + +- do: + get: + index: test + type: test + id: 1 +- match: { _source.message: "foo bar baz" } +- match: { _source.a: "foo" } +- match: { _source.b: "bar" } +- match: { _source.c: "baz" } +--- +"Test dissect processor mismatch": +- do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "dissect" : { + "field" : "message", + "pattern" : "%{a},%{b},%{c}" + } + } + ] + } +- match: { acknowledged: true } + +- do: + catch: '/Unable to find match for dissect pattern: \%\{a\},\%\{b\},\%\{c\} against source: foo bar baz/' + index: + index: test + type: test + id: 2 + pipeline: "my_pipeline" + body: {message: "foo bar baz"} + +--- +"Test fail to create dissect processor": +- do: + catch: '/Unable to parse pattern/' + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "dissect" : { + "field" : "message", + "pattern" : "bad pattern" + } + } + ] + } + From e2b931e80b8b4134f94e6eef8bfb9cfe31a571a5 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 28 Aug 2018 10:44:15 -0400 Subject: [PATCH 190/283] Use Lucene history in primary-replica resync (#33178) This commit makes primary-replica resyncer use Lucene as the source of history operation instead of translog if soft-deletes is enabled. With this change, we no longer expose translog snapshot directly in IndexShard. Relates #29530 --- .../elasticsearch/index/engine/Engine.java | 6 ------ .../index/engine/InternalEngine.java | 5 ----- .../elasticsearch/index/shard/IndexShard.java | 9 -------- .../index/shard/PrimaryReplicaSyncer.java | 3 +-- .../shard/PrimaryReplicaSyncerTests.java | 21 ++++++++++++------- .../action/bulk/BulkShardOperationsTests.java | 2 +- 6 files changed, 15 insertions(+), 31 deletions(-) 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 020dac78d49e..65d289bedd4f 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -592,12 +592,6 @@ public enum SearcherScope { */ public abstract Closeable acquireRetentionLockForPeerRecovery(); - /** - * Creates a new translog snapshot from this engine for reading translog operations whose seq# in the provided range. - * The caller has to close the returned snapshot after finishing the reading. - */ - public abstract Translog.Snapshot newSnapshotFromMinSeqNo(long minSeqNo) throws IOException; - public abstract TranslogStats getTranslogStats(); /** 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 1baaaf2e1b14..11acf22d338d 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -480,11 +480,6 @@ public void syncTranslog() throws IOException { revisitIndexDeletionPolicyOnTranslogSynced(); } - @Override - public Translog.Snapshot newSnapshotFromMinSeqNo(long minSeqNo) throws IOException { - return getTranslog().newSnapshotFromMinSeqNo(minSeqNo); - } - /** * Creates a new history snapshot for reading operations since the provided seqno. * The returned snapshot can be retrieved from either Lucene index or translog files. 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 aabdd742303e..62518dfc9b82 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -1635,15 +1635,6 @@ public Closeable acquireRetentionLockForPeerRecovery() { return getEngine().acquireRetentionLockForPeerRecovery(); } - /** - * Creates a new translog snapshot for reading translog operations whose seq# at least the provided seq#. - * The caller has to close the returned snapshot after finishing the reading. - */ - public Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException { - // TODO: Remove this method after primary-replica resync use soft-deletes - return getEngine().newSnapshotFromMinSeqNo(minSeqNo); - } - /** * Returns the estimated number of history operations whose seq# at least the provided seq# in this shard. */ diff --git a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java index 909039cea4b7..016a8afff696 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java +++ b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java @@ -89,8 +89,7 @@ public void resync(final IndexShard indexShard, final ActionListener // Wrap translog snapshot to make it synchronized as it is accessed by different threads through SnapshotSender. // Even though those calls are not concurrent, snapshot.next() uses non-synchronized state and is not multi-thread-compatible // Also fail the resync early if the shard is shutting down - // TODO: A follow-up to make resync using soft-deletes - snapshot = indexShard.newTranslogSnapshotFromMinSeqNo(startingSeqNo); + snapshot = indexShard.getHistoryOperations("resync", startingSeqNo); final Translog.Snapshot originalSnapshot = snapshot; final Translog.Snapshot wrappedSnapshot = new Translog.Snapshot() { @Override diff --git a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java index ae2cc84e4870..29b16ca28f4d 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java @@ -106,17 +106,22 @@ public void testSyncerSendsOffCorrectDocuments() throws Exception { .isPresent(), is(false)); } - - assertEquals(globalCheckPoint == numDocs - 1 ? 0 : numDocs, resyncTask.getTotalOperations()); if (syncNeeded && globalCheckPoint < numDocs - 1) { - long skippedOps = globalCheckPoint + 1; // everything up to global checkpoint included - assertEquals(skippedOps, resyncTask.getSkippedOperations()); - assertEquals(numDocs - skippedOps, resyncTask.getResyncedOperations()); + if (shard.indexSettings.isSoftDeleteEnabled()) { + assertThat(resyncTask.getSkippedOperations(), equalTo(0)); + assertThat(resyncTask.getResyncedOperations(), equalTo(resyncTask.getTotalOperations())); + assertThat(resyncTask.getTotalOperations(), equalTo(Math.toIntExact(numDocs - 1 - globalCheckPoint))); + } else { + int skippedOps = Math.toIntExact(globalCheckPoint + 1); // everything up to global checkpoint included + assertThat(resyncTask.getSkippedOperations(), equalTo(skippedOps)); + assertThat(resyncTask.getResyncedOperations(), equalTo(numDocs - skippedOps)); + assertThat(resyncTask.getTotalOperations(), equalTo(globalCheckPoint == numDocs - 1 ? 0 : numDocs)); + } } else { - assertEquals(0, resyncTask.getSkippedOperations()); - assertEquals(0, resyncTask.getResyncedOperations()); + assertThat(resyncTask.getSkippedOperations(), equalTo(0)); + assertThat(resyncTask.getResyncedOperations(), equalTo(0)); + assertThat(resyncTask.getTotalOperations(), equalTo(0)); } - closeShards(shard); } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java index fa06abe65ba6..4c6c0c060e45 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/bulk/BulkShardOperationsTests.java @@ -61,7 +61,7 @@ public void testPrimaryTermFromFollower() throws IOException { final TransportWriteAction.WritePrimaryResult result = TransportBulkShardOperationsAction.shardOperationOnPrimary(followerPrimary.shardId(), operations, followerPrimary, logger); - try (Translog.Snapshot snapshot = followerPrimary.newTranslogSnapshotFromMinSeqNo(0)) { + try (Translog.Snapshot snapshot = followerPrimary.getHistoryOperations("test", 0)) { assertThat(snapshot.totalOperations(), equalTo(operations.size())); Translog.Operation operation; while ((operation = snapshot.next()) != null) { From 1e11b05b58e09f47dfbd155bdfca74399c054ff0 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Tue, 28 Aug 2018 08:55:29 -0600 Subject: [PATCH 191/283] Remove unused User class from protocol (#33137) This commit removes the unused User class from the protocol project. This class was originally moved into protocol in preparation for moving more request and response classes, but given the change in direction for the HLRC this is no longer needed. Additionally, this change also changes the package name for the User object in x-pack/plugin/core to its original name. --- .../xpack/core/security/SecurityContext.java | 2 +- .../xpack/core/security/UserSettings.java | 2 +- .../action/user/AuthenticateResponse.java | 2 +- .../user/ChangePasswordRequestBuilder.java | 2 +- .../action/user/GetUsersResponse.java | 2 +- .../action/user/PutUserRequestBuilder.java | 2 +- .../core/security/authc/Authentication.java | 2 +- .../security/authc/AuthenticationResult.java | 2 +- .../xpack/core/security/authc/Realm.java | 2 +- .../SecurityIndexSearcherWrapper.java | 2 +- .../core/security/user/APMSystemUser.java | 1 - .../core/security/user/AnonymousUser.java | 1 - .../core/security/user/BeatsSystemUser.java | 1 - .../xpack/core/security/user/ElasticUser.java | 1 - .../user/InternalUserSerializationHelper.java | 1 - .../xpack/core/security/user/KibanaUser.java | 1 - .../security/user/LogstashSystemUser.java | 1 - .../xpack/core/security/user/SystemUser.java | 1 - .../core/security/user}/User.java | 2 +- .../core/security/user/XPackSecurityUser.java | 2 - .../xpack/core/security/user/XPackUser.java | 1 - ...SecurityIndexSearcherWrapperUnitTests.java | 2 +- .../core/security/user}/UserTests.java | 3 +- .../saml/TransportSamlLogoutAction.java | 2 +- .../user/TransportAuthenticateAction.java | 2 +- .../action/user/TransportGetUsersAction.java | 2 +- .../user/TransportHasPrivilegesAction.java | 2 +- .../xpack/security/audit/AuditTrail.java | 2 +- .../security/audit/AuditTrailService.java | 2 +- .../security/audit/index/IndexAuditTrail.java | 2 +- .../audit/logfile/LoggingAuditTrail.java | 2 +- .../security/authc/AuthenticationService.java | 2 +- .../security/authc/esnative/NativeRealm.java | 2 +- .../authc/esnative/NativeUsersStore.java | 4 +- .../authc/esnative/ReservedRealm.java | 2 +- .../authc/esnative/UserAndPassword.java | 2 +- .../xpack/security/authc/file/FileRealm.java | 2 +- .../authc/file/FileUserPasswdStore.java | 2 +- .../authc/kerberos/KerberosRealm.java | 2 +- .../xpack/security/authc/ldap/LdapRealm.java | 2 +- .../xpack/security/authc/pki/PkiRealm.java | 2 +- .../xpack/security/authc/saml/SamlRealm.java | 2 +- .../support/CachingUsernamePasswordRealm.java | 2 +- .../security/authz/AuthorizationService.java | 2 +- .../security/authz/AuthorizedIndices.java | 2 +- .../ingest/SetSecurityUserProcessor.java | 2 +- .../rest/action/RestAuthenticateAction.java | 2 +- .../action/user/RestChangePasswordAction.java | 2 +- .../rest/action/user/RestGetUsersAction.java | 2 +- .../transport/ServerTransportFilter.java | 2 +- .../integration/ClearRealmsCacheTests.java | 2 +- .../xpack/security/SecurityContextTests.java | 2 +- .../filter/SecurityActionFilterTests.java | 2 +- ...IndicesAliasesRequestInterceptorTests.java | 2 +- .../ResizeRequestInterceptorTests.java | 2 +- ...sportSamlInvalidateSessionActionTests.java | 2 +- .../saml/TransportSamlLogoutActionTests.java | 2 +- .../TransportCreateTokenActionTests.java | 2 +- .../TransportAuthenticateActionTests.java | 2 +- .../TransportChangePasswordActionTests.java | 2 +- .../user/TransportDeleteUserActionTests.java | 2 +- .../user/TransportGetUsersActionTests.java | 2 +- .../TransportHasPrivilegesActionTests.java | 2 +- .../user/TransportPutUserActionTests.java | 2 +- .../user/TransportSetEnabledActionTests.java | 2 +- .../audit/AuditTrailServiceTests.java | 2 +- .../index/IndexAuditTrailMutedTests.java | 2 +- .../audit/index/IndexAuditTrailTests.java | 2 +- .../logfile/LoggingAuditTrailFilterTests.java | 2 +- .../audit/logfile/LoggingAuditTrailTests.java | 2 +- .../authc/AuthenticationServiceTests.java | 2 +- .../xpack/security/authc/RealmsTests.java | 2 +- .../security/authc/TokenServiceTests.java | 2 +- .../xpack/security/authc/UserTokenTests.java | 2 +- .../authc/esnative/NativeRealmIntegTests.java | 2 +- .../authc/esnative/NativeUsersStoreTests.java | 2 +- .../authc/esnative/ReservedRealmTests.java | 2 +- .../security/authc/file/FileRealmTests.java | 2 +- .../authc/file/FileUserPasswdStoreTests.java | 2 +- .../KerberosRealmAuthenticateFailedTests.java | 2 +- .../kerberos/KerberosRealmCacheTests.java | 2 +- .../authc/kerberos/KerberosRealmTestCase.java | 2 +- .../authc/kerberos/KerberosRealmTests.java | 2 +- .../authc/ldap/ActiveDirectoryRealmTests.java | 2 +- .../ldap/CancellableLdapRunnableTests.java | 2 +- .../security/authc/ldap/LdapRealmTests.java | 2 +- .../security/authc/pki/PkiRealmTests.java | 2 +- .../CachingUsernamePasswordRealmTests.java | 2 +- .../mapper/NativeRoleMappingStoreTests.java | 2 +- .../authz/AuthorizationServiceTests.java | 2 +- .../authz/AuthorizationUtilsTests.java | 2 +- .../authz/AuthorizedIndicesTests.java | 2 +- .../authz/IndicesAndAliasesResolverTests.java | 2 +- .../SecuritySearchOperationListenerTests.java | 2 +- .../ingest/SetSecurityUserProcessorTests.java | 2 +- ...curityServerTransportInterceptorTests.java | 2 +- .../transport/ServerTransportFilterTests.java | 2 +- .../security/user/AnonymousUserTests.java | 2 +- .../security/user/UserSerializationTests.java | 2 +- .../execution/ExecutionServiceTests.java | 2 +- .../protocol/xpack/security/User.java | 249 ------------------ .../protocol/xpack/security/package-info.java | 24 -- .../protocol/xpack/security/UserTests.java | 39 --- .../example/realm/CustomRealm.java | 2 +- .../example/realm/CustomRealmTests.java | 2 +- .../xpack/security/MigrateToolIT.java | 2 +- 106 files changed, 95 insertions(+), 417 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/{protocol/xpack/security => xpack/core/security/user}/User.java (99%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/{protocol/xpack/security => xpack/core/security/user}/UserTests.java (91%) delete mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java delete mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java delete mode 100644 x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java index 8d56221d78be..99788ac1de43 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java @@ -13,7 +13,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext; import org.elasticsearch.node.Node; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; import java.util.Objects; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/UserSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/UserSettings.java index 536464cb3376..7f22f90351ef 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/UserSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/UserSettings.java @@ -10,7 +10,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java index c3e126b92ce1..0cf7ace1103d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java @@ -8,7 +8,7 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequestBuilder.java index 05b3af41de22..d7538c2a556f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequestBuilder.java @@ -18,7 +18,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.support.Validation; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; import java.io.IOException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUsersResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUsersResponse.java index 0da525b6ffc4..666b79cfe5db 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUsersResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/GetUsersResponse.java @@ -8,7 +8,7 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; import java.util.Collection; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java index 7dc958bbef9f..eea804d81fe9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java @@ -20,7 +20,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.support.Validation; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; import java.io.IOException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index e72df007cf63..161d9d449990 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -12,7 +12,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; import java.util.Base64; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java index 08e01025e7e8..0f073ef4ae39 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java @@ -6,7 +6,7 @@ package org.elasticsearch.xpack.core.security.authc; import org.elasticsearch.common.Nullable; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.util.Objects; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java index ae04d474f41c..2c63ca95eb98 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java @@ -9,7 +9,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.xpack.core.XPackField; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.util.Collections; import java.util.HashMap; 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 e812f0cfc733..9426b6436478 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 @@ -63,7 +63,7 @@ import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField; import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader; import org.elasticsearch.xpack.core.security.support.Exceptions; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; import java.util.ArrayList; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java index 48a72be5c1a8..c26b66875e6e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/APMSystemUser.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.core.security.user; import org.elasticsearch.Version; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.support.MetadataUtils; /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AnonymousUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AnonymousUser.java index ae6f41c3a1bd..36354ff58b31 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AnonymousUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/AnonymousUser.java @@ -9,7 +9,6 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.support.MetadataUtils; import java.util.Collections; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/BeatsSystemUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/BeatsSystemUser.java index 9db64da97a53..dfa437fa8d2c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/BeatsSystemUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/BeatsSystemUser.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.core.security.user; import org.elasticsearch.Version; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.support.MetadataUtils; /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/ElasticUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/ElasticUser.java index c58f86ea4224..ec618a4f4821 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/ElasticUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/ElasticUser.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.security.user; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.support.MetadataUtils; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java index c0b45aea57c2..fa41828a7bba 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/InternalUserSerializationHelper.java @@ -7,7 +7,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.protocol.xpack.security.User; import java.io.IOException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/KibanaUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/KibanaUser.java index 3e816aa54bca..8dfa149987d0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/KibanaUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/KibanaUser.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.security.user; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.support.MetadataUtils; /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java index 71e43ff5a30f..88381482ef3f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/LogstashSystemUser.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.security.user; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.support.MetadataUtils; /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java index 1c7ac129d17b..4569c2a68a09 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/SystemUser.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.security.user; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.authz.privilege.SystemPrivilege; import java.util.function.Predicate; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java similarity index 99% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java index 16ed33ae9408..028b14f882aa 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/security/User.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.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.protocol.xpack.security; +package org.elasticsearch.xpack.core.security.user; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java index e98df7fb50a5..906d35483778 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackSecurityUser.java @@ -5,8 +5,6 @@ */ package org.elasticsearch.xpack.core.security.user; -import org.elasticsearch.protocol.xpack.security.User; - /** * internal user that manages xpack security. Has all cluster/indices permissions. */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java index fe50b1b9c88b..38c9fe84aa93 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/XPackUser.java @@ -5,7 +5,6 @@ */ package org.elasticsearch.xpack.core.security.user; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.index.IndexAuditTrailField; 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 c26968ce54aa..dccbd14c0470 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 @@ -76,7 +76,7 @@ import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.junit.After; import org.junit.Before; import org.mockito.ArgumentCaptor; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/UserTests.java similarity index 91% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/UserTests.java index 28a27e639985..d652575bb9fc 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/user/UserTests.java @@ -3,9 +3,10 @@ * 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.protocol.xpack.security; +package org.elasticsearch.xpack.core.security.user; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.user.User; import java.util.Collections; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java index 3e489c69d1db..63931d119e0f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutAction.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Realm; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.Realms; import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.security.authc.saml.SamlNameId; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java index af56ab8d4eb7..57510ce116f7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java @@ -18,7 +18,7 @@ import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import java.util.function.Supplier; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java index f89745d23e31..7e17cda75f0a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersAction.java @@ -18,7 +18,7 @@ import org.elasticsearch.xpack.core.security.action.user.GetUsersResponse; import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java index eefaaa72b1e0..b49984b28da0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesAction.java @@ -30,7 +30,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.support.Automatons; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java index 8dcaf1a61ff4..3f19d2819258 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java @@ -9,7 +9,7 @@ import org.elasticsearch.transport.TransportMessage; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import java.net.InetAddress; 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 a9245c7653e0..3cd12b1a7ceb 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 @@ -12,7 +12,7 @@ import org.elasticsearch.transport.TransportMessage; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import java.net.InetAddress; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java index 6d12455fdea6..d8b4b4e4bc19 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java @@ -55,7 +55,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.index.IndexAuditTrailField; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.core.template.TemplateUtils; import org.elasticsearch.xpack.security.audit.AuditLevel; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index 3a6cfa501935..5da6a9eb77cd 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -30,7 +30,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditLevel; import org.elasticsearch.xpack.security.audit.AuditTrail; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 416acfca3abd..85084da84648 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -31,7 +31,7 @@ import org.elasticsearch.xpack.core.security.support.Exceptions; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java index ffd9c3f73bca..a84b76beab8b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealm.java @@ -11,7 +11,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index 507ed4684a14..d923a0298041 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -48,8 +48,8 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.client.SecurityClient; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; -import org.elasticsearch.protocol.xpack.security.User.Fields; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.security.user.User.Fields; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index c3651224c49a..337266719110 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -30,7 +30,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.LogstashSystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/UserAndPassword.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/UserAndPassword.java index d9971ab2388f..3f636312f0f0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/UserAndPassword.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/UserAndPassword.java @@ -7,7 +7,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.xpack.core.security.authc.support.Hasher; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; /** * Like User, but includes the hashed password diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java index 8d529897534e..e2586ea836de 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileRealm.java @@ -12,7 +12,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.file.FileRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; import java.util.Map; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java index 220108b56375..15a6c2c41dae 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStore.java @@ -24,7 +24,7 @@ import org.elasticsearch.xpack.core.security.support.NoOpLogger; import org.elasticsearch.xpack.core.security.support.Validation; import org.elasticsearch.xpack.core.security.support.Validation.Users; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.support.SecurityFiles; import java.io.IOException; 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 53146203ee2f..d57bb3052d8e 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 @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.support.CachingRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java index f689bc287890..87749850141b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java @@ -25,7 +25,7 @@ import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.ldap.support.LdapLoadBalancing; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index 58e10a547557..7b9eabfd7066 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -26,7 +26,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; import org.elasticsearch.xpack.security.authc.BytesKey; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index a8f50d975e8d..cc160c8f78b3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -43,7 +43,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLConfiguration; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.core.ssl.SSLService; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java index ab559cebe54e..6e321f9f7dd5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java @@ -18,7 +18,7 @@ import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.util.Collections; import java.util.Map; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 2c9bb8ce3c0c..642bc167f7d4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -57,7 +57,7 @@ import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java index 07845a131b75..3068a3993d30 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizedIndices.java @@ -8,7 +8,7 @@ import org.elasticsearch.cluster.metadata.AliasOrIndex; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.xpack.core.security.authz.permission.Role; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.util.ArrayList; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java index 6db19d8edeb1..15ac88b4d946 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessor.java @@ -10,7 +10,7 @@ import org.elasticsearch.ingest.IngestDocument; import org.elasticsearch.ingest.Processor; import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.util.Arrays; import java.util.EnumSet; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java index 10b8e65c1682..b280b3a89a20 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateAction.java @@ -20,7 +20,7 @@ import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java index 7e33844e99bb..1b64b3ce2bae 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestChangePasswordAction.java @@ -21,7 +21,7 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.client.SecurityClient; import org.elasticsearch.xpack.core.security.rest.RestRequestFilter; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUsersAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUsersAction.java index 3e8cf26ad7d6..1ab80954e9b5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUsersAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestGetUsersAction.java @@ -18,7 +18,7 @@ import org.elasticsearch.rest.action.RestBuilderListener; import org.elasticsearch.xpack.core.security.action.user.GetUsersResponse; import org.elasticsearch.xpack.core.security.client.SecurityClient; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java index b686994a2ee9..fcbae00ba090 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/ServerTransportFilter.java @@ -26,7 +26,7 @@ import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.action.SecurityActionMapper; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java index 0b6321e59601..fc02a5c4d625 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClearRealmsCacheTests.java @@ -22,7 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.client.SecurityClient; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.Realms; import org.junit.BeforeClass; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java index 2b9e540f3bfe..e3b1cd31246a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityContextTests.java @@ -15,7 +15,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.junit.Before; import java.io.IOException; 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 e4e1e7ca1c01..577c7ddb2496 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 @@ -33,7 +33,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.junit.Before; 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 0809276932da..7c951c0014e8 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 @@ -22,7 +22,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; import java.util.Collections; 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 f939b175e48b..f1363214b070 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 @@ -24,7 +24,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; import java.util.Collections; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java index 81b0b1a72911..17a45f238930 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java @@ -56,7 +56,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.Realms; import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.security.authc.UserToken; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java index c58a63d27ccb..291c102f396c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java @@ -46,7 +46,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.Realms; import org.elasticsearch.xpack.security.authc.TokenService; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java index b9c89d8875ae..a6b92d79f158 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java @@ -28,7 +28,6 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.node.Node; -import org.elasticsearch.protocol.xpack.security.User; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; @@ -40,6 +39,7 @@ import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java index 7862097d0006..a8e246480582 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import java.util.Collections; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java index 410c164ffe74..aabaa40381f6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordActionTests.java @@ -22,7 +22,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.mockito.invocation.InvocationOnMock; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java index 0c1ddbd9ba75..4e6e0b3551bc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportDeleteUserActionTests.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.mockito.invocation.InvocationOnMock; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java index ebdb14555919..1c5f93187c05 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportGetUsersActionTests.java @@ -23,7 +23,7 @@ import org.elasticsearch.xpack.core.security.action.user.GetUsersResponse; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java index d7795d3ab918..a2e283e1b36f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportHasPrivilegesActionTests.java @@ -34,7 +34,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; import org.hamcrest.Matchers; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java index 36ba3f46b5e9..b6037932f8a8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportPutUserActionTests.java @@ -25,7 +25,7 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java index 4ca7ab97f73d..d811b6359b18 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportSetEnabledActionTests.java @@ -24,7 +24,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.mockito.invocation.InvocationOnMock; 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 ba9a67eb48f7..b346fc6857e7 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 @@ -13,7 +13,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java index 923c918f011d..9bc5c989d1f9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java @@ -26,7 +26,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.State; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; import org.junit.After; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java index dcb8d8b7569c..cb1b69708bdf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java @@ -55,7 +55,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.index.IndexAuditTrailField; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.LocalStateSecurity; import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.Message; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java index a3a9d05704f0..4c9df8fd9d38 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java @@ -28,7 +28,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.AuditEventMetaInfo; import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrailTests.MockMessage; import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrailTests.RestContent; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index c8e14e668c91..1059e22abd66 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -35,7 +35,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; 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 a07bc734361d..1640ab727fe3 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 @@ -64,7 +64,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; 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 1da7d68c91ca..9d795826298a 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 @@ -23,7 +23,7 @@ import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 2d5b5707cd26..b92b4cad39a2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -48,7 +48,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.TokenMetaData; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.watcher.watch.ClockMock; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.AfterClass; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/UserTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/UserTokenTests.java index c79d77718ca4..1a8f8dc3b5d2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/UserTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/UserTokenTests.java @@ -10,7 +10,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; import java.time.Clock; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java index 6ebf6dca2cf1..c2a7ea495a18 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java @@ -44,7 +44,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.junit.Before; import org.junit.BeforeClass; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java index 243d2d981b21..f280e85f4ab1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStoreTests.java @@ -31,7 +31,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.LogstashSystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index dffbe6b3eaa4..36d1690b8b20 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -27,7 +27,7 @@ import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.LogstashSystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.UsernamesField; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore.ReservedUserInfo; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java index aec2a4eb8208..f5dad8b7c684 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileRealmTests.java @@ -17,7 +17,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.junit.Before; import org.mockito.stubbing.Answer; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java index ee89c2efe4f1..739952af63ee 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.support.Hasher; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.junit.After; import org.junit.Before; 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 11aefb758fb2..5bc239241cf1 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 @@ -14,7 +14,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.ietf.jgss.GSSException; import java.nio.file.Path; 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 7ce8ee39c038..69ebe15c5d74 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 @@ -11,7 +11,7 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData; import org.ietf.jgss.GSSException; 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 69b246cd7ca6..9c2c6484c82a 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 @@ -21,7 +21,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.support.Exceptions; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; 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 9e6fafc481db..fee8df535f25 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 @@ -14,7 +14,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.TestEnvironment; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.RealmConfig; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java index a8f555bc3a39..2c6756aada7a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java @@ -35,7 +35,7 @@ import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.DnRoleMapperSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.VerificationMode; import org.elasticsearch.xpack.security.authc.ldap.ActiveDirectorySessionFactory.DownLevelADAuthenticator; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/CancellableLdapRunnableTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/CancellableLdapRunnableTests.java index 2807c501a5d6..18b84df6d618 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/CancellableLdapRunnableTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/CancellableLdapRunnableTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.ldap.LdapRealm.CancellableLdapRunnable; import java.util.concurrent.CountDownLatch; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java index 5c98e2347cf1..4aff821217d1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java @@ -27,7 +27,7 @@ import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.DnRoleMapperSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.VerificationMode; import org.elasticsearch.xpack.security.authc.ldap.support.LdapTestCase; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index 2410d8c46499..44d5859d12b6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -21,7 +21,7 @@ import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.support.NoOpLogger; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java index e6830be18c58..052758d83718 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java @@ -22,7 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.junit.After; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java index 2bee8fa09e38..052ba3855102 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/mapper/NativeRoleMappingStoreTests.java @@ -25,7 +25,7 @@ import org.elasticsearch.xpack.core.security.authc.support.mapper.ExpressionRoleMapping; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression.FieldValue; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 65c558d3d81e..7722a9d21663 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -122,7 +122,7 @@ import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java index a581d1abbb5d..9c9f2b1b1a42 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java @@ -15,7 +15,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java index d31f9f37b915..c48ac4568989 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizedIndicesTests.java @@ -20,7 +20,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.support.SecurityIndexManager; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java index c0867875b018..ebced2307978 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/IndicesAndAliasesResolverTests.java @@ -60,7 +60,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.AnonymousUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; 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 087749da2406..fac88e8af09b 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 @@ -22,7 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; import static org.elasticsearch.mock.orig.Mockito.verifyNoMoreInteractions; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java index 05c2882f3ac1..26c59a1ef547 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/ingest/SetSecurityUserProcessorTests.java @@ -11,7 +11,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor.Property; import java.util.Collections; 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 09072f99fc20..dd7dda48ae81 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 @@ -33,7 +33,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java index bf8d8042546f..17df337d2916 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ServerTransportFilterTests.java @@ -28,7 +28,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.authc.AuthenticationService; import org.elasticsearch.xpack.security.authz.AuthorizationService; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java index 32816e40e087..4c72afeb5cee 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/AnonymousUserTests.java @@ -9,7 +9,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.user.AnonymousUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.equalTo; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java index 0d5941eaf267..68b54198980d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/user/UserSerializationTests.java @@ -11,7 +11,7 @@ import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import java.util.Arrays; diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java index bb593bcb67ae..d3f46d3d452f 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/execution/ExecutionServiceTests.java @@ -33,7 +33,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.watcher.actions.Action; import org.elasticsearch.xpack.core.watcher.actions.ActionStatus; import org.elasticsearch.xpack.core.watcher.actions.ActionWrapper; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java deleted file mode 100644 index e08289e98215..000000000000 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/User.java +++ /dev/null @@ -1,249 +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.protocol.xpack.security; - -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - -/** - * An authenticated user - */ -public class User implements ToXContentObject { - - private final String username; - private final String[] roles; - private final User authenticatedUser; - private final Map metadata; - private final boolean enabled; - - @Nullable private final String fullName; - @Nullable private final String email; - - public User(String username, String... roles) { - this(username, roles, null, null, null, true); - } - - public User(String username, String[] roles, User authenticatedUser) { - this(username, roles, null, null, null, true, authenticatedUser); - } - - public User(User user, User authenticatedUser) { - this(user.principal(), user.roles(), user.fullName(), user.email(), user.metadata(), user.enabled(), authenticatedUser); - } - - public User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled) { - this(username, roles, fullName, email, metadata, enabled, null); - } - - private User(String username, String[] roles, String fullName, String email, Map metadata, boolean enabled, - User authenticatedUser) { - this.username = username; - this.roles = roles == null ? Strings.EMPTY_ARRAY : roles; - this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); - this.fullName = fullName; - this.email = email; - this.enabled = enabled; - assert (authenticatedUser == null || authenticatedUser.isRunAs() == false) : "the authenticated user should not be a run_as user"; - this.authenticatedUser = authenticatedUser; - } - - /** - * @return The principal of this user - effectively serving as the - * unique identity of of the user. - */ - public String principal() { - return this.username; - } - - /** - * @return The roles this user is associated with. The roles are - * identified by their unique names and each represents as - * set of permissions - */ - public String[] roles() { - return this.roles; - } - - /** - * @return The metadata that is associated with this user. Can never be {@code null}. - */ - public Map metadata() { - return metadata; - } - - /** - * @return The full name of this user. May be {@code null}. - */ - public String fullName() { - return fullName; - } - - /** - * @return The email of this user. May be {@code null}. - */ - public String email() { - return email; - } - - /** - * @return whether the user is enabled or not - */ - public boolean enabled() { - return enabled; - } - - /** - * @return The user that was originally authenticated. - * This may be the user itself, or a different user which used runAs. - */ - public User authenticatedUser() { - return authenticatedUser == null ? this : authenticatedUser; - } - - /** Return true if this user was not the originally authenticated user, false otherwise. */ - public boolean isRunAs() { - return authenticatedUser != null; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("User[username=").append(username); - sb.append(",roles=[").append(Strings.arrayToCommaDelimitedString(roles)).append("]"); - sb.append(",fullName=").append(fullName); - sb.append(",email=").append(email); - sb.append(",metadata="); - sb.append(metadata); - if (authenticatedUser != null) { - sb.append(",authenticatedUser=[").append(authenticatedUser.toString()).append("]"); - } - sb.append("]"); - return sb.toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o instanceof User == false) return false; - - User user = (User) o; - - if (!username.equals(user.username)) return false; - // Probably incorrect - comparing Object[] arrays with Arrays.equals - if (!Arrays.equals(roles, user.roles)) return false; - if (authenticatedUser != null ? !authenticatedUser.equals(user.authenticatedUser) : user.authenticatedUser != null) return false; - if (!metadata.equals(user.metadata)) return false; - if (fullName != null ? !fullName.equals(user.fullName) : user.fullName != null) return false; - return !(email != null ? !email.equals(user.email) : user.email != null); - - } - - @Override - public int hashCode() { - int result = username.hashCode(); - result = 31 * result + Arrays.hashCode(roles); - result = 31 * result + (authenticatedUser != null ? authenticatedUser.hashCode() : 0); - result = 31 * result + metadata.hashCode(); - result = 31 * result + (fullName != null ? fullName.hashCode() : 0); - result = 31 * result + (email != null ? email.hashCode() : 0); - return result; - } - - @Override - public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(Fields.USERNAME.getPreferredName(), principal()); - builder.array(Fields.ROLES.getPreferredName(), roles()); - builder.field(Fields.FULL_NAME.getPreferredName(), fullName()); - builder.field(Fields.EMAIL.getPreferredName(), email()); - builder.field(Fields.METADATA.getPreferredName(), metadata()); - builder.field(Fields.ENABLED.getPreferredName(), enabled()); - return builder.endObject(); - } - - public static User partialReadFrom(String username, StreamInput input) throws IOException { - String[] roles = input.readStringArray(); - Map metadata = input.readMap(); - String fullName = input.readOptionalString(); - String email = input.readOptionalString(); - boolean enabled = input.readBoolean(); - User outerUser = new User(username, roles, fullName, email, metadata, enabled, null); - boolean hasInnerUser = input.readBoolean(); - if (hasInnerUser) { - User innerUser = readFrom(input); - return new User(outerUser, innerUser); - } else { - return outerUser; - } - } - - public static User readFrom(StreamInput input) throws IOException { - final boolean isInternalUser = input.readBoolean(); - assert isInternalUser == false: "should always return false. Internal users should use the InternalUserSerializationHelper"; - final String username = input.readString(); - return partialReadFrom(username, input); - } - - public static void writeTo(User user, StreamOutput output) throws IOException { - if (user.authenticatedUser == null) { - // no backcompat necessary, since there is no inner user - writeUser(user, output); - } else { - writeUser(user, output); - output.writeBoolean(true); - writeUser(user.authenticatedUser, output); - } - output.writeBoolean(false); // last user written, regardless of bwc, does not have an inner user - } - - /** Write just the given {@link User}, but not the inner {@link #authenticatedUser}. */ - private static void writeUser(User user, StreamOutput output) throws IOException { - output.writeBoolean(false); // not a system user - output.writeString(user.username); - output.writeStringArray(user.roles); - output.writeMap(user.metadata); - output.writeOptionalString(user.fullName); - output.writeOptionalString(user.email); - output.writeBoolean(user.enabled); - } - - public interface Fields { - ParseField USERNAME = new ParseField("username"); - ParseField PASSWORD = new ParseField("password"); - ParseField PASSWORD_HASH = new ParseField("password_hash"); - ParseField ROLES = new ParseField("roles"); - ParseField FULL_NAME = new ParseField("full_name"); - ParseField EMAIL = new ParseField("email"); - ParseField METADATA = new ParseField("metadata"); - ParseField ENABLED = new ParseField("enabled"); - ParseField TYPE = new ParseField("type"); - } -} - diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java deleted file mode 100644 index 216990d9f0ec..000000000000 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/security/package-info.java +++ /dev/null @@ -1,24 +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. - */ - -/** - * Request and Response objects for the default distribution's Security - * APIs. - */ -package org.elasticsearch.protocol.xpack.security; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java deleted file mode 100644 index 2e3c67131df7..000000000000 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/security/UserTests.java +++ /dev/null @@ -1,39 +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.protocol.xpack.security; - -import org.elasticsearch.test.ESTestCase; - -import java.util.Collections; - -import static org.hamcrest.Matchers.is; - -public class UserTests extends ESTestCase { - - public void testUserToString() { - User user = new User("u1", "r1"); - assertThat(user.toString(), is("User[username=u1,roles=[r1],fullName=null,email=null,metadata={}]")); - user = new User("u1", new String[] { "r1", "r2" }, "user1", "user1@domain.com", Collections.singletonMap("key", "val"), true); - assertThat(user.toString(), is("User[username=u1,roles=[r1,r2],fullName=user1,email=user1@domain.com,metadata={key=val}]")); - user = new User("u1", new String[] {"r1"}, new User("u2", "r2", "r3")); - assertThat(user.toString(), is("User[username=u1,roles=[r1],fullName=null,email=null,metadata={}," + - "authenticatedUser=[User[username=u2,roles=[r2,r3],fullName=null,email=null,metadata={}]]]")); - } -} diff --git a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java index c6502c05d252..dfd4a81ea215 100644 --- a/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java +++ b/x-pack/qa/security-example-spi-extension/src/main/java/org/elasticsearch/example/realm/CustomRealm.java @@ -14,7 +14,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.common.CharArrays; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; public class CustomRealm extends Realm { diff --git a/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java b/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java index e206de6e3927..d1435ebaa3c2 100644 --- a/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java +++ b/x-pack/qa/security-example-spi-extension/src/test/java/org/elasticsearch/example/realm/CustomRealmTests.java @@ -14,7 +14,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; diff --git a/x-pack/qa/security-migrate-tests/src/test/java/org/elasticsearch/xpack/security/MigrateToolIT.java b/x-pack/qa/security-migrate-tests/src/test/java/org/elasticsearch/xpack/security/MigrateToolIT.java index e810e638f689..4ac927c6646c 100644 --- a/x-pack/qa/security-migrate-tests/src/test/java/org/elasticsearch/xpack/security/MigrateToolIT.java +++ b/x-pack/qa/security-migrate-tests/src/test/java/org/elasticsearch/xpack/security/MigrateToolIT.java @@ -25,7 +25,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition; import org.elasticsearch.xpack.core.security.client.SecurityClient; -import org.elasticsearch.protocol.xpack.security.User; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.esnative.ESNativeRealmMigrateTool; import org.junit.Before; From 5f0f990afd670ac20f8e8810705d23fc2f4aff74 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Tue, 28 Aug 2018 10:33:18 -0500 Subject: [PATCH 192/283] HLRC: Use Optional in validation logic (#33104) The Validatable class comes from an old class in server code, that assumed null was returned in the event of validation having no errors. This commit changes that to use Optional, which is cleaner than passing around null objects. --- .../client/RestHighLevelClient.java | 13 ++++++------ .../org/elasticsearch/client/Validatable.java | 20 ++++++++----------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index b6784060e24e..c45ec048ae8b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -176,6 +176,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; import java.util.function.Function; @@ -1010,9 +1011,9 @@ protected final Resp performRequest(Req request, RequestOptions options, CheckedFunction responseConverter, Set ignores) throws IOException { - ValidationException validationException = request.validate(); - if (validationException != null && validationException.validationErrors().isEmpty() == false) { - throw validationException; + Optional validationException = request.validate(); + if (validationException != null && validationException.isPresent()) { + throw validationException.get(); } return internalPerformRequest(request, requestConverter, options, responseConverter, ignores); } @@ -1105,9 +1106,9 @@ protected final void performRequestAsync(Req req RequestOptions options, CheckedFunction responseConverter, ActionListener listener, Set ignores) { - ValidationException validationException = request.validate(); - if (validationException != null && validationException.validationErrors().isEmpty() == false) { - listener.onFailure(validationException); + Optional validationException = request.validate(); + if (validationException != null && validationException.isPresent()) { + listener.onFailure(validationException.get()); return; } internalPerformRequestAsync(request, requestConverter, options, responseConverter, listener, ignores); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.java index 2efff4d3663b..fe4a1fc42cb3 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/Validatable.java @@ -18,24 +18,20 @@ */ package org.elasticsearch.client; +import java.util.Optional; + /** * Defines a validation layer for Requests. */ public interface Validatable { - ValidationException EMPTY_VALIDATION = new ValidationException() { - @Override - public void addValidationError(String error) { - throw new UnsupportedOperationException("Validation messages should not be added to the empty validation"); - } - }; - /** - * Perform validation. This method does not have to be overridden in the event that no validation needs to be done. + * Perform validation. This method does not have to be overridden in the event that no validation needs to be done, + * or the validation was done during object construction time. A {@link ValidationException} that is not null is + * assumed to contain validation errors and will be thrown. * - * @return potentially null, in the event of older actions, an empty {@link ValidationException} in newer actions, or finally a - * {@link ValidationException} that contains a list of all failed validation. + * @return An {@link Optional} {@link ValidationException} that contains a list of validation errors. */ - default ValidationException validate() { - return EMPTY_VALIDATION; + default Optional validate() { + return Optional.empty(); } } From 353112a033fce837db96c19a46ea706d8a33f2fe Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Tue, 28 Aug 2018 11:50:35 -0400 Subject: [PATCH 193/283] [Rollup] Better error message when trying to set non-rollup index (#32965) We don't allow the user to configure a rollup index against an existing index, but the exceptions that we return are not clear about that. They indicate issues with metadata, instead of stating the real reason (not allowed to use a non-rollup index to store rollup data). This makes the exception better, and adds a bit more testing --- .../action/TransportPutRollupJobAction.java | 8 ++-- .../action/PutJobStateMachineTests.java | 43 ++++++++++++++++++- .../rest-api-spec/test/rollup/put_job.yml | 32 ++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java index 9f20fba8e92d..f0600d80f82a 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java @@ -158,7 +158,8 @@ static void updateMapping(RollupJob job, ActionListener li MappingMetaData mappings = getMappingResponse.getMappings().get(indexName).get(RollupField.TYPE_NAME); Object m = mappings.getSourceAsMap().get("_meta"); if (m == null) { - String msg = "Expected to find _meta key in mapping of rollup index [" + indexName + "] but not found."; + String msg = "Rollup data cannot be added to existing indices that contain non-rollup data (expected " + + "to find _meta key in mapping of rollup index [" + indexName + "] but not found)."; logger.error(msg); listener.onFailure(new RuntimeException(msg)); return; @@ -166,8 +167,9 @@ static void updateMapping(RollupJob job, ActionListener li Map metadata = (Map) m; if (metadata.get(RollupField.ROLLUP_META) == null) { - String msg = "Expected to find rollup meta key [" + RollupField.ROLLUP_META + "] in mapping of rollup index [" + indexName - + "] but not found."; + String msg = "Rollup data cannot be added to existing indices that contain non-rollup data (expected " + + "to find rollup meta key [" + RollupField.ROLLUP_META + "] in mapping of rollup index [" + + indexName + "] but not found)."; logger.error(msg); listener.onFailure(new RuntimeException(msg)); return; diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java index 5599c50321cf..3d346456ea98 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java @@ -180,8 +180,9 @@ public void testNoMetadataInMapping() { ActionListener testListener = ActionListener.wrap(response -> { fail("Listener success should not have been triggered."); }, e -> { - assertThat(e.getMessage(), equalTo("Expected to find _meta key in mapping of rollup index [" - + job.getConfig().getRollupIndex() + "] but not found.")); + assertThat(e.getMessage(), equalTo("Rollup data cannot be added to existing indices that contain " + + "non-rollup data (expected to find _meta key in mapping of rollup index [" + + job.getConfig().getRollupIndex() + "] but not found).")); }); Logger logger = mock(Logger.class); @@ -206,6 +207,44 @@ public void testNoMetadataInMapping() { verify(client).execute(eq(GetMappingsAction.INSTANCE), any(GetMappingsRequest.class), any()); } + @SuppressWarnings("unchecked") + public void testMetadataButNotRollup() { + RollupJob job = new RollupJob(ConfigTestHelpers.randomRollupJobConfig(random()), Collections.emptyMap()); + + ActionListener testListener = ActionListener.wrap(response -> { + fail("Listener success should not have been triggered."); + }, e -> { + assertThat(e.getMessage(), equalTo("Rollup data cannot be added to existing indices that contain " + + "non-rollup data (expected to find rollup meta key [_rollup] in mapping of rollup index [" + + job.getConfig().getRollupIndex() + "] but not found).")); + }); + + Logger logger = mock(Logger.class); + Client client = mock(Client.class); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ActionListener.class); + doAnswer(invocation -> { + GetMappingsResponse response = mock(GetMappingsResponse.class); + Map m = new HashMap<>(2); + m.put("random", + Collections.singletonMap(job.getConfig().getId(), job.getConfig())); + MappingMetaData meta = new MappingMetaData(RollupField.TYPE_NAME, + Collections.singletonMap("_meta", m)); + ImmutableOpenMap.Builder builder = ImmutableOpenMap.builder(1); + builder.put(RollupField.TYPE_NAME, meta); + + ImmutableOpenMap.Builder> builder2 = ImmutableOpenMap.builder(1); + builder2.put(job.getConfig().getRollupIndex(), builder.build()); + + when(response.getMappings()).thenReturn(builder2.build()); + requestCaptor.getValue().onResponse(response); + return null; + }).when(client).execute(eq(GetMappingsAction.INSTANCE), any(GetMappingsRequest.class), requestCaptor.capture()); + + TransportPutRollupJobAction.updateMapping(job, testListener, mock(PersistentTasksService.class), client, logger); + verify(client).execute(eq(GetMappingsAction.INSTANCE), any(GetMappingsRequest.class), any()); + } + @SuppressWarnings("unchecked") public void testNoMappingVersion() { RollupJob job = new RollupJob(ConfigTestHelpers.randomRollupJobConfig(random()), Collections.emptyMap()); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/put_job.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/put_job.yml index 516be25be2a2..23df0c583770 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/put_job.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/put_job.yml @@ -128,6 +128,38 @@ setup: ] } +--- +"Test put_job in non-rollup index": + - do: + indices.create: + index: non-rollup + - do: + catch: /foo/ + headers: + Authorization: "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA==" # run as x_pack_rest_user, i.e. the test setup superuser + xpack.rollup.put_job: + id: foo + body: > + { + "index_pattern": "foo", + "rollup_index": "non-rollup", + "cron": "*/30 * * * * ?", + "page_size" :10, + "groups" : { + "date_histogram": { + "field": "the_field", + "interval": "1h" + } + }, + "metrics": [ + { + "field": "value_field", + "metrics": ["min", "max", "sum"] + } + ] + } + + --- "Try to include headers": From e39689a1983429596072ac37b3a4de43bc6ca3d1 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 28 Aug 2018 12:32:09 -0400 Subject: [PATCH 194/283] Send only ops after checkpoint in file-based recovery with soft-deletes (#33190) Today a file-based recovery will replay all existing translog operations from the primary on a replica so that that replica can have a full history in translog as the primary. However, with soft-deletes enabled, we should not do it because: 1. All operations before the local checkpoint of the safe commit exist in the commit already. 2. The number of operations before the local checkpoint may be considerable and requires a significant amount of time to replay on a replica. Relates #30522 Relates #29530 --- .../recovery/RecoverySourceHandler.java | 9 +- .../gateway/RecoveryFromGatewayIT.java | 4 +- .../RecoveryDuringReplicationTests.java | 16 ++-- .../indices/recovery/RecoveryTests.java | 88 +++++++++++++++---- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 3ed53f6c3e11..10f796e5e155 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -162,12 +162,13 @@ public RecoveryResponse recoverToTarget() throws IOException { } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "snapshot failed", e); } - // we set this to 0 to create a translog roughly according to the retention policy - // on the target. Note that it will still filter out legacy operations with no sequence numbers - startingSeqNo = 0; //TODO: A follow-up to send only ops above the local checkpoint if soft-deletes enabled. - // but we must have everything above the local checkpoint in the commit + // We must have everything above the local checkpoint in the commit requiredSeqNoRangeStart = Long.parseLong(phase1Snapshot.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + // If soft-deletes enabled, we need to transfer only operations after the local_checkpoint of the commit to have + // the same history on the target. However, with translog, we need to set this to 0 to create a translog roughly + // according to the retention policy on the target. Note that it will still filter out legacy operations without seqNo. + startingSeqNo = shard.indexSettings().isSoftDeleteEnabled() ? requiredSeqNoRangeStart : 0; try { final int estimateNumOps = shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo); phase1(phase1Snapshot.getIndexCommit(), () -> estimateNumOps); diff --git a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java index 3b92f04df089..b0b6c35f92a1 100644 --- a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java @@ -482,9 +482,7 @@ public Settings onNodeStopped(String nodeName) throws Exception { assertThat("all existing files should be reused, file count mismatch", recoveryState.getIndex().reusedFileCount(), equalTo(filesReused)); assertThat(recoveryState.getIndex().reusedFileCount(), equalTo(recoveryState.getIndex().totalFileCount() - filesRecovered)); assertThat("> 0 files should be reused", recoveryState.getIndex().reusedFileCount(), greaterThan(0)); - // both cases will be zero once we start sending only ops after local checkpoint of the safe commit - int expectedTranslogOps = softDeleteEnabled ? numDocs + moreDocs : 0; - assertThat("no translog ops should be recovered", recoveryState.getTranslog().recoveredOperations(), equalTo(expectedTranslogOps)); + assertThat("no translog ops should be recovered", recoveryState.getTranslog().recoveredOperations(), equalTo(0)); } } } 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 ba2345af4f70..28122665e9bb 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java @@ -219,8 +219,7 @@ public void testRecoveryToReplicaThatReceivedExtraDocument() throws Exception { @TestLogging("org.elasticsearch.index.shard:TRACE,org.elasticsearch.indices.recovery:TRACE") public void testRecoveryAfterPrimaryPromotion() throws Exception { - Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build(); - try (ReplicationGroup shards = createGroup(2, settings)) { + try (ReplicationGroup shards = createGroup(2)) { shards.startAll(); int totalDocs = shards.indexDocs(randomInt(10)); int committedDocs = 0; @@ -232,7 +231,6 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { final IndexShard oldPrimary = shards.getPrimary(); final IndexShard newPrimary = shards.getReplicas().get(0); final IndexShard replica = shards.getReplicas().get(1); - boolean softDeleteEnabled = replica.indexSettings().isSoftDeleteEnabled(); if (randomBoolean()) { // simulate docs that were inflight when primary failed, these will be rolled back final int rollbackDocs = randomIntBetween(1, 5); @@ -280,12 +278,13 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { assertThat(newPrimary.getLastSyncedGlobalCheckpoint(), equalTo(newPrimary.seqNoStats().getMaxSeqNo())); }); newPrimary.flush(new FlushRequest().force(true)); - uncommittedOpsOnPrimary = shards.indexDocs(randomIntBetween(0, 10)); - totalDocs += uncommittedOpsOnPrimary; - // we need an extra flush or refresh to advance the min_retained_seqno on the new primary so that ops-based won't happen - if (softDeleteEnabled) { + if (replica.indexSettings().isSoftDeleteEnabled()) { + // We need an extra flush to advance the min_retained_seqno on the new primary so ops-based won't happen. + // The min_retained_seqno only advances when a merge asks for the retention query. newPrimary.flush(new FlushRequest().force(true)); } + uncommittedOpsOnPrimary = shards.indexDocs(randomIntBetween(0, 10)); + totalDocs += uncommittedOpsOnPrimary; } if (randomBoolean()) { @@ -305,8 +304,7 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { assertThat(newReplica.recoveryState().getTranslog().recoveredOperations(), equalTo(totalDocs - committedDocs)); } else { assertThat(newReplica.recoveryState().getIndex().fileDetails(), not(empty())); - int expectOps = softDeleteEnabled ? totalDocs : uncommittedOpsOnPrimary; - assertThat(newReplica.recoveryState().getTranslog().recoveredOperations(), equalTo(expectOps)); + assertThat(newReplica.recoveryState().getTranslog().recoveredOperations(), equalTo(uncommittedOpsOnPrimary)); } // roll back the extra ops in the replica diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java index fae1b1662f25..45535e19672c 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java @@ -64,13 +64,13 @@ public void testTranslogHistoryTransferred() throws Exception { int docs = shards.indexDocs(10); getTranslog(shards.getPrimary()).rollGeneration(); shards.flush(); - if (randomBoolean()) { - docs += shards.indexDocs(10); - } + int moreDocs = shards.indexDocs(randomInt(10)); shards.addReplica(); shards.startAll(); final IndexShard replica = shards.getReplicas().get(0); - assertThat(getTranslog(replica).totalOperations(), equalTo(docs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? moreDocs : docs + moreDocs)); + shards.assertAllEqual(docs + moreDocs); } } @@ -107,7 +107,7 @@ public void testRetentionPolicyChangeDuringRecovery() throws Exception { } } - public void testRecoveryWithOutOfOrderDelete() throws Exception { + public void testRecoveryWithOutOfOrderDeleteWithTranslog() throws Exception { /* * The flow of this test: * - delete #1 @@ -117,12 +117,9 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { * - flush (commit point has max_seqno 3, and local checkpoint 1 -> points at gen 2, previous commit point is maintained) * - index #2 * - index #5 - * - If flush and the translog/lucene retention disabled, delete #1 will be removed while index #0 is still retained and replayed. + * - If flush and the translog retention disabled, delete #1 will be removed while index #0 is still retained and replayed. */ - Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 10) - // If soft-deletes is enabled, delete#1 will be reclaimed because its segment (segment_1) is fully deleted - // index#0 will be retained if merge is disabled; otherwise it will be reclaimed because gcp=3 and retained_ops=0 - .put(MergePolicyConfig.INDEX_MERGE_ENABLED, false).build(); + Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); try (ReplicationGroup shards = createGroup(1, settings)) { shards.startAll(); // create out of order delete and index op on replica @@ -131,7 +128,7 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { // delete #1 orgReplica.applyDeleteOperationOnReplica(1, 2, "type", "id"); - orgReplica.flush(new FlushRequest().force(true)); // isolate delete#1 in its own translog generation and lucene segment + getTranslog(orgReplica).rollGeneration(); // isolate the delete in it's own generation // index #0 orgReplica.applyIndexOperationOnReplica(0, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, SourceToParse.source(indexName, "type", "id", new BytesArray("{}"), XContentType.JSON)); @@ -151,17 +148,16 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { final int translogOps; if (randomBoolean()) { if (randomBoolean()) { - logger.info("--> flushing shard (translog/soft-deletes will be trimmed)"); + logger.info("--> flushing shard (translog will be trimmed)"); IndexMetaData.Builder builder = IndexMetaData.builder(orgReplica.indexSettings().getIndexMetaData()); builder.settings(Settings.builder().put(orgReplica.indexSettings().getSettings()) .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), "-1") - .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1") - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0)); + .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1")); orgReplica.indexSettings().updateIndexMetaData(builder.build()); orgReplica.onSettingsChanged(); translogOps = 5; // 4 ops + seqno gaps (delete #1 is removed but index #0 will be replayed). } else { - logger.info("--> flushing shard (translog/soft-deletes will be retained)"); + logger.info("--> flushing shard (translog will be retained)"); translogOps = 6; // 5 ops + seqno gaps } flushShard(orgReplica); @@ -180,6 +176,62 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { } } + public void testRecoveryWithOutOfOrderDeleteWithSoftDeletes() throws Exception { + Settings settings = Settings.builder() + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 10) + // If soft-deletes is enabled, delete#1 will be reclaimed because its segment (segment_1) is fully deleted + // index#0 will be retained if merge is disabled; otherwise it will be reclaimed because gcp=3 and retained_ops=0 + .put(MergePolicyConfig.INDEX_MERGE_ENABLED, false).build(); + try (ReplicationGroup shards = createGroup(1, settings)) { + shards.startAll(); + // create out of order delete and index op on replica + final IndexShard orgReplica = shards.getReplicas().get(0); + final String indexName = orgReplica.shardId().getIndexName(); + + // delete #1 + orgReplica.applyDeleteOperationOnReplica(1, 2, "type", "id"); + orgReplica.flush(new FlushRequest().force(true)); // isolate delete#1 in its own translog generation and lucene segment + // index #0 + orgReplica.applyIndexOperationOnReplica(0, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id", new BytesArray("{}"), XContentType.JSON)); + // index #3 + orgReplica.applyIndexOperationOnReplica(3, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-3", new BytesArray("{}"), XContentType.JSON)); + // Flushing a new commit with local checkpoint=1 allows to delete the translog gen #1. + orgReplica.flush(new FlushRequest().force(true).waitIfOngoing(true)); + // index #2 + orgReplica.applyIndexOperationOnReplica(2, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-2", new BytesArray("{}"), XContentType.JSON)); + orgReplica.updateGlobalCheckpointOnReplica(3L, "test"); + // index #5 -> force NoOp #4. + orgReplica.applyIndexOperationOnReplica(5, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-5", new BytesArray("{}"), XContentType.JSON)); + + if (randomBoolean()) { + if (randomBoolean()) { + logger.info("--> flushing shard (translog/soft-deletes will be trimmed)"); + IndexMetaData.Builder builder = IndexMetaData.builder(orgReplica.indexSettings().getIndexMetaData()); + builder.settings(Settings.builder().put(orgReplica.indexSettings().getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0)); + orgReplica.indexSettings().updateIndexMetaData(builder.build()); + orgReplica.onSettingsChanged(); + } + flushShard(orgReplica); + } + + final IndexShard orgPrimary = shards.getPrimary(); + shards.promoteReplicaToPrimary(orgReplica).get(); // wait for primary/replica sync to make sure seq# gap is closed. + + IndexShard newReplica = shards.addReplicaWithExistingPath(orgPrimary.shardPath(), orgPrimary.routingEntry().currentNodeId()); + shards.recoverReplica(newReplica); + shards.assertAllEqual(3); + try (Translog.Snapshot snapshot = newReplica.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.size(6)); + } + } + } + public void testDifferentHistoryUUIDDisablesOPsRecovery() throws Exception { try (ReplicationGroup shards = createGroup(1)) { shards.startAll(); @@ -228,7 +280,8 @@ public void testDifferentHistoryUUIDDisablesOPsRecovery() throws Exception { shards.recoverReplica(newReplica); // file based recovery should be made assertThat(newReplica.recoveryState().getIndex().fileDetails(), not(empty())); - assertThat(getTranslog(newReplica).totalOperations(), equalTo(numDocs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(newReplica).totalOperations(), equalTo(softDeletesEnabled ? nonFlushedDocs : numDocs)); // history uuid was restored assertThat(newReplica.getHistoryUUID(), equalTo(historyUUID)); @@ -332,7 +385,8 @@ public void testShouldFlushAfterPeerRecovery() throws Exception { shards.recoverReplica(replica); // Make sure the flushing will eventually be completed (eg. `shouldPeriodicallyFlush` is false) assertBusy(() -> assertThat(getEngine(replica).shouldPeriodicallyFlush(), equalTo(false))); - assertThat(getTranslog(replica).totalOperations(), equalTo(numDocs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? 0 : numDocs)); shards.assertAllEqual(numDocs); } } From 7f5e29ddb21ee5935852633f0dff483c8fbd34b2 Mon Sep 17 00:00:00 2001 From: Sohaib Iftikhar Date: Tue, 28 Aug 2018 19:02:23 +0200 Subject: [PATCH 195/283] HLREST: add reindex API (#32679) Adds the reindex API to the high level REST client. --- .../client/RequestConverters.java | 16 + .../client/RestHighLevelClient.java | 29 ++ .../java/org/elasticsearch/client/CrudIT.java | 67 +++ .../client/RequestConvertersTests.java | 63 +++ .../client/RestHighLevelClientTests.java | 1 - .../documentation/CRUDDocumentationIT.java | 149 ++++++ .../high-level/document/reindex.asciidoc | 215 +++++++++ .../high-level/supported-apis.asciidoc | 2 + .../index/reindex/RestReindexAction.java | 3 +- .../index/reindex/ReindexMetadataTests.java | 3 +- .../index/reindex/ReindexScriptTests.java | 3 +- .../index/reindex/RestReindexActionTests.java | 4 +- .../index/reindex/RoundTripTests.java | 3 +- .../action/bulk/BulkItemResponse.java | 35 +- .../reindex/AbstractBulkByScrollRequest.java | 8 + .../index/reindex/BulkByScrollResponse.java | 113 ++++- .../reindex/BulkByScrollResponseBuilder.java | 76 +++ .../index/reindex/BulkByScrollTask.java | 435 +++++++++++++++++- .../index/reindex/ReindexRequest.java | 174 ++++++- .../index/reindex/RemoteInfo.java | 25 +- .../index/reindex/ScrollableHitSource.java | 13 +- .../search/builder/SearchSourceBuilder.java | 11 +- .../reindex/BulkByScrollResponseTests.java | 51 +- ...ulkByScrollTaskStatusOrExceptionTests.java | 101 ++++ .../reindex/BulkByScrollTaskStatusTests.java | 92 +++- .../index/reindex/ReindexRequestTests.java | 8 +- .../xpack/upgrade/InternalIndexReindexer.java | 10 +- 27 files changed, 1643 insertions(+), 67 deletions(-) create mode 100644 docs/java-rest/high-level/document/reindex.asciidoc create mode 100644 server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponseBuilder.java create mode 100644 server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 9dd316a0fb02..15bbde6b5bfa 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -106,6 +106,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.rankeval.RankEvalRequest; +import org.elasticsearch.index.reindex.ReindexRequest; import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.XPackUsageRequest; import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest; @@ -820,6 +821,21 @@ static Request clusterHealth(ClusterHealthRequest healthRequest) { return request; } + static Request reindex(ReindexRequest reindexRequest) throws IOException { + String endpoint = new EndpointBuilder().addPathPart("_reindex").build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + Params params = new Params(request) + .withRefresh(reindexRequest.isRefresh()) + .withTimeout(reindexRequest.getTimeout()) + .withWaitForActiveShards(reindexRequest.getWaitForActiveShards()); + + if (reindexRequest.getScrollTime() != null) { + params.putParam("scroll", reindexRequest.getScrollTime()); + } + request.setEntity(createEntity(reindexRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request rollover(RolloverRequest rolloverRequest) throws IOException { String endpoint = new EndpointBuilder().addPathPart(rolloverRequest.getAlias()).addPathPartAsIs("_rollover") .addPathPart(rolloverRequest.getNewIndexName()).build(); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index c45ec048ae8b..4ac5bfd080f1 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -64,6 +64,8 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.rankeval.RankEvalRequest; import org.elasticsearch.index.rankeval.RankEvalResponse; +import org.elasticsearch.index.reindex.BulkByScrollResponse; +import org.elasticsearch.index.reindex.ReindexRequest; import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestStatus; @@ -395,6 +397,33 @@ public final void bulkAsync(BulkRequest bulkRequest, RequestOptions options, Act performRequestAsyncAndParseEntity(bulkRequest, RequestConverters::bulk, options, BulkResponse::fromXContent, listener, emptySet()); } + /** + * Executes a reindex request. + * See Reindex API on elastic.co + * @param reindexRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public final BulkByScrollResponse reindex(ReindexRequest reindexRequest, RequestOptions options) throws IOException { + return performRequestAndParseEntity( + reindexRequest, RequestConverters::reindex, options, BulkByScrollResponse::fromXContent, emptySet() + ); + } + + /** + * Asynchronously executes a reindex request. + * See Reindex API on elastic.co + * @param reindexRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public final void reindexAsync(ReindexRequest reindexRequest, RequestOptions options, ActionListener listener) { + performRequestAsyncAndParseEntity( + reindexRequest, RequestConverters::reindex, options, BulkByScrollResponse::fromXContent, listener, emptySet() + ); + } + /** * Pings the remote Elasticsearch cluster and returns true if the ping succeeded, false otherwise * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java index 89f357477fa0..7978d76c56d7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java @@ -41,12 +41,16 @@ import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.index.query.IdsQueryBuilder; +import org.elasticsearch.index.reindex.BulkByScrollResponse; +import org.elasticsearch.index.reindex.ReindexRequest; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; @@ -624,6 +628,69 @@ public void testBulk() throws IOException { validateBulkResponses(nbItems, errors, bulkResponse, bulkRequest); } + public void testReindex() throws IOException { + final String sourceIndex = "source1"; + final String destinationIndex = "dest"; + { + // Prepare + Settings settings = Settings.builder() + .put("number_of_shards", 1) + .put("number_of_replicas", 0) + .build(); + createIndex(sourceIndex, settings); + createIndex(destinationIndex, settings); + assertEquals( + RestStatus.OK, + highLevelClient().bulk( + new BulkRequest() + .add(new IndexRequest(sourceIndex, "type", "1") + .source(Collections.singletonMap("foo", "bar"), XContentType.JSON)) + .add(new IndexRequest(sourceIndex, "type", "2") + .source(Collections.singletonMap("foo2", "bar2"), XContentType.JSON)) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE), + RequestOptions.DEFAULT + ).status() + ); + } + { + // test1: create one doc in dest + ReindexRequest reindexRequest = new ReindexRequest(); + reindexRequest.setSourceIndices(sourceIndex); + reindexRequest.setDestIndex(destinationIndex); + reindexRequest.setSourceQuery(new IdsQueryBuilder().addIds("1").types("type")); + reindexRequest.setRefresh(true); + BulkByScrollResponse bulkResponse = execute(reindexRequest, highLevelClient()::reindex, highLevelClient()::reindexAsync); + assertEquals(1, bulkResponse.getCreated()); + assertEquals(1, bulkResponse.getTotal()); + assertEquals(0, bulkResponse.getDeleted()); + assertEquals(0, bulkResponse.getNoops()); + assertEquals(0, bulkResponse.getVersionConflicts()); + assertEquals(1, bulkResponse.getBatches()); + assertTrue(bulkResponse.getTook().getMillis() > 0); + assertEquals(1, bulkResponse.getBatches()); + assertEquals(0, bulkResponse.getBulkFailures().size()); + assertEquals(0, bulkResponse.getSearchFailures().size()); + } + { + // test2: create 1 and update 1 + ReindexRequest reindexRequest = new ReindexRequest(); + reindexRequest.setSourceIndices(sourceIndex); + reindexRequest.setDestIndex(destinationIndex); + BulkByScrollResponse bulkResponse = execute(reindexRequest, highLevelClient()::reindex, highLevelClient()::reindexAsync); + assertEquals(1, bulkResponse.getCreated()); + assertEquals(2, bulkResponse.getTotal()); + assertEquals(1, bulkResponse.getUpdated()); + assertEquals(0, bulkResponse.getDeleted()); + assertEquals(0, bulkResponse.getNoops()); + assertEquals(0, bulkResponse.getVersionConflicts()); + assertEquals(1, bulkResponse.getBatches()); + assertTrue(bulkResponse.getTook().getMillis() > 0); + assertEquals(1, bulkResponse.getBatches()); + assertEquals(0, bulkResponse.getBulkFailures().size()); + assertEquals(0, bulkResponse.getSearchFailures().size()); + } + } + public void testBulkProcessorIntegration() throws IOException { int nbItems = randomIntBetween(10, 100); boolean[] errors = new boolean[nbItems]; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index ebabb8f95b59..44b4ae05b57c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -116,6 +116,7 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.RandomCreateIndexGenerator; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.query.QueryBuilder; @@ -126,6 +127,8 @@ import org.elasticsearch.index.rankeval.RankEvalSpec; import org.elasticsearch.index.rankeval.RatedRequest; import org.elasticsearch.index.rankeval.RestRankEvalAction; +import org.elasticsearch.index.reindex.ReindexRequest; +import org.elasticsearch.index.reindex.RemoteInfo; import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; @@ -172,6 +175,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.elasticsearch.client.RequestConverters.REQUEST_BODY_CONTENT_TYPE; import static org.elasticsearch.client.RequestConverters.enforceSameContentType; @@ -179,6 +183,7 @@ import static org.elasticsearch.index.RandomCreateIndexGenerator.randomCreateIndexRequest; import static org.elasticsearch.index.RandomCreateIndexGenerator.randomIndexSettings; import static org.elasticsearch.index.alias.RandomAliasActionsGenerator.randomAliasAction; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.search.RandomSearchRequestGenerator.randomSearchRequest; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; import static org.hamcrest.CoreMatchers.equalTo; @@ -407,6 +412,64 @@ public void testUpdateAliases() throws IOException { assertToXContentBody(indicesAliasesRequest, request.getEntity()); } + public void testReindex() throws IOException { + ReindexRequest reindexRequest = new ReindexRequest(); + reindexRequest.setSourceIndices("source_idx"); + reindexRequest.setDestIndex("dest_idx"); + Map expectedParams = new HashMap<>(); + if (randomBoolean()) { + XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); + RemoteInfo remoteInfo = new RemoteInfo("http", "remote-host", 9200, null, + BytesReference.bytes(matchAllQuery().toXContent(builder, ToXContent.EMPTY_PARAMS)), + "user", + "pass", + emptyMap(), + RemoteInfo.DEFAULT_SOCKET_TIMEOUT, + RemoteInfo.DEFAULT_CONNECT_TIMEOUT + ); + reindexRequest.setRemoteInfo(remoteInfo); + } + if (randomBoolean()) { + reindexRequest.setSourceDocTypes("doc", "tweet"); + } + if (randomBoolean()) { + reindexRequest.setSourceBatchSize(randomInt(100)); + } + if (randomBoolean()) { + reindexRequest.setDestDocType("tweet_and_doc"); + } + if (randomBoolean()) { + reindexRequest.setDestOpType("create"); + } + if (randomBoolean()) { + reindexRequest.setDestPipeline("my_pipeline"); + } + if (randomBoolean()) { + reindexRequest.setDestRouting("=cat"); + } + if (randomBoolean()) { + reindexRequest.setSize(randomIntBetween(100, 1000)); + } + if (randomBoolean()) { + reindexRequest.setAbortOnVersionConflict(false); + } + if (randomBoolean()) { + String ts = randomTimeValue(); + reindexRequest.setScroll(TimeValue.parseTimeValue(ts, "scroll")); + } + if (reindexRequest.getRemoteInfo() == null && randomBoolean()) { + reindexRequest.setSourceQuery(new TermQueryBuilder("foo", "fooval")); + } + setRandomTimeout(reindexRequest::setTimeout, ReplicationRequest.DEFAULT_TIMEOUT, expectedParams); + setRandomWaitForActiveShards(reindexRequest::setWaitForActiveShards, ActiveShardCount.DEFAULT, expectedParams); + expectedParams.put("scroll", reindexRequest.getScrollTime().getStringRep()); + Request request = RequestConverters.reindex(reindexRequest); + assertEquals("/_reindex", request.getEndpoint()); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals(expectedParams, request.getParameters()); + assertToXContentBody(reindexRequest, request.getEntity()); + } + public void testPutMapping() throws IOException { PutMappingRequest putMappingRequest = new PutMappingRequest(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 15f2b80b2523..d2585b6f3f4c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -660,7 +660,6 @@ public void testApiNamingConventions() throws Exception { "indices.put_alias", "mtermvectors", "put_script", - "reindex", "reindex_rethrottle", "render_search_template", "scripts_painless_execute", diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java index ad41c139ddc3..9c69a2a48361 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java @@ -50,6 +50,8 @@ import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; @@ -59,13 +61,22 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.index.reindex.BulkByScrollResponse; +import org.elasticsearch.index.reindex.ReindexRequest; +import org.elasticsearch.index.reindex.RemoteInfo; +import org.elasticsearch.index.reindex.ScrollableHitSource; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; +import org.elasticsearch.search.sort.SortOrder; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -750,6 +761,144 @@ public void onFailure(Exception e) { } } + public void testReindex() throws Exception { + RestHighLevelClient client = highLevelClient(); + { + String mapping = + "\"doc\": {\n" + + " \"properties\": {\n" + + " \"user\": {\n" + + " \"type\": \"text\"\n" + + " },\n" + + " \"field1\": {\n" + + " \"type\": \"integer\"\n" + + " },\n" + + " \"field2\": {\n" + + " \"type\": \"integer\"\n" + + " }\n" + + " }\n" + + " }"; + createIndex("source1", Settings.EMPTY, mapping); + createIndex("source2", Settings.EMPTY, mapping); + createPipeline("my_pipeline"); + } + { + // tag::reindex-request + ReindexRequest request = new ReindexRequest(); // <1> + request.setSourceIndices("source1", "source2"); // <2> + request.setDestIndex("dest"); // <3> + // end::reindex-request + // tag::reindex-request-versionType + request.setDestVersionType(VersionType.EXTERNAL); // <1> + // end::reindex-request-versionType + // tag::reindex-request-opType + request.setDestOpType("create"); // <1> + // end::reindex-request-opType + // tag::reindex-request-conflicts + request.setConflicts("proceed"); // <1> + // end::reindex-request-conflicts + // tag::reindex-request-typeOrQuery + request.setSourceDocTypes("doc"); // <1> + request.setSourceQuery(new TermQueryBuilder("user", "kimchy")); // <2> + // end::reindex-request-typeOrQuery + // tag::reindex-request-size + request.setSize(10); // <1> + // end::reindex-request-size + // tag::reindex-request-sourceSize + request.setSourceBatchSize(100); // <1> + // end::reindex-request-sourceSize + // tag::reindex-request-pipeline + request.setDestPipeline("my_pipeline"); // <1> + // end::reindex-request-pipeline + // tag::reindex-request-sort + request.addSortField("field1", SortOrder.DESC); // <1> + request.addSortField("field2", SortOrder.ASC); // <2> + // end::reindex-request-sort + // tag::reindex-request-script + request.setScript( + new Script( + ScriptType.INLINE, "painless", + "if (ctx._source.user == 'kimchy') {ctx._source.likes++;}", + Collections.emptyMap())); // <1> + // end::reindex-request-script + // tag::reindex-request-remote + request.setRemoteInfo( + new RemoteInfo( + "https", "localhost", 9002, null, new BytesArray(new MatchAllQueryBuilder().toString()), + "user", "pass", Collections.emptyMap(), new TimeValue(100, TimeUnit.MILLISECONDS), + new TimeValue(100, TimeUnit.SECONDS) + ) + ); // <1> + // end::reindex-request-remote + request.setRemoteInfo(null); // Remove it for tests + // tag::reindex-request-timeout + request.setTimeout(TimeValue.timeValueMinutes(2)); // <1> + // end::reindex-request-timeout + // tag::reindex-request-refresh + request.setRefresh(true); // <1> + // end::reindex-request-refresh + // tag::reindex-request-slices + request.setSlices(2); // <1> + // end::reindex-request-slices + // tag::reindex-request-scroll + request.setScroll(TimeValue.timeValueMinutes(10)); // <1> + // end::reindex-request-scroll + + + // tag::reindex-execute + BulkByScrollResponse bulkResponse = client.reindex(request, RequestOptions.DEFAULT); + // end::reindex-execute + assertSame(0, bulkResponse.getSearchFailures().size()); + assertSame(0, bulkResponse.getBulkFailures().size()); + // tag::reindex-response + TimeValue timeTaken = bulkResponse.getTook(); // <1> + boolean timedOut = bulkResponse.isTimedOut(); // <2> + long totalDocs = bulkResponse.getTotal(); // <3> + long updatedDocs = bulkResponse.getUpdated(); // <4> + long createdDocs = bulkResponse.getCreated(); // <5> + long deletedDocs = bulkResponse.getDeleted(); // <6> + long batches = bulkResponse.getBatches(); // <7> + long noops = bulkResponse.getNoops(); // <8> + long versionConflicts = bulkResponse.getVersionConflicts(); // <9> + long bulkRetries = bulkResponse.getBulkRetries(); // <10> + long searchRetries = bulkResponse.getSearchRetries(); // <11> + TimeValue throttledMillis = bulkResponse.getStatus().getThrottled(); // <12> + TimeValue throttledUntilMillis = bulkResponse.getStatus().getThrottledUntil(); // <13> + List searchFailures = bulkResponse.getSearchFailures(); // <14> + List bulkFailures = bulkResponse.getBulkFailures(); // <15> + // end::reindex-response + } + { + ReindexRequest request = new ReindexRequest(); + request.setSourceIndices("source1"); + request.setDestIndex("dest"); + + // tag::reindex-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(BulkByScrollResponse bulkResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::reindex-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::reindex-execute-async + client.reindexAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::reindex-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGet() throws Exception { RestHighLevelClient client = highLevelClient(); { diff --git a/docs/java-rest/high-level/document/reindex.asciidoc b/docs/java-rest/high-level/document/reindex.asciidoc new file mode 100644 index 000000000000..b6d98b42dc50 --- /dev/null +++ b/docs/java-rest/high-level/document/reindex.asciidoc @@ -0,0 +1,215 @@ +[[java-rest-high-document-reindex]] +=== Reindex API + +[[java-rest-high-document-reindex-request]] +==== Reindex Request + +A `ReindexRequest` can be used to copy documents from one or more indexes into a destination index. + +It requires an existing source index and a target index which may or may not exist pre-request. Reindex does not attempt +to set up the destination index. It does not copy the settings of the source index. You should set up the destination +index prior to running a _reindex action, including setting up mappings, shard counts, replicas, etc. + +The simplest form of a `ReindexRequest` looks like follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request] +-------------------------------------------------- +<1> Creates the `ReindexRequest` +<2> Adds a list of sources to copy from +<3> Adds the destination index + +The `dest` element can be configured like the index API to control optimistic concurrency control. Just leaving out +`versionType` (as above) or setting it to internal will cause Elasticsearch to blindly dump documents into the target. +Setting `versionType` to external will cause Elasticsearch to preserve the version from the source, create any documents +that are missing, and update any documents that have an older version in the destination index than they do in the +source index. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-versionType] +-------------------------------------------------- +<1> Set the versionType to `EXTERNAL` + +Setting `opType` to `create` will cause `_reindex` to only create missing documents in the target index. All existing +documents will cause a version conflict. The default `opType` is `index`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-opType] +-------------------------------------------------- +<1> Set the opType to `create` + +By default version conflicts abort the `_reindex` process but you can just count them by settings it to `proceed` +in the request body + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-conflicts] +-------------------------------------------------- +<1> Set `proceed` on version conflict + +You can limit the documents by adding a type to the source or by adding a query. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-typeOrQuery] +-------------------------------------------------- +<1> Only copy `doc` type +<2> Only copy documents which have field `user` set to `kimchy` + +It’s also possible to limit the number of processed documents by setting size. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-size] +-------------------------------------------------- +<1> Only copy 10 documents + +By default `_reindex` uses batches of 1000. You can change the batch size with `sourceBatchSize`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-sourceSize] +-------------------------------------------------- +<1> Use batches of 100 documents + +Reindex can also use the ingest feature by specifying a `pipeline`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-pipeline] +-------------------------------------------------- +<1> set pipeline to `my_pipeline` + +If you want a particular set of documents from the source index you’ll need to use sort. If possible, prefer a more +selective query to size and sort. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-sort] +-------------------------------------------------- +<1> add descending sort to`field1` +<2> add ascending sort to `field2` + +`ReindexRequest` also supports a `script` that modifies the document. It allows you to also change the document's +metadata. The following example illustrates that. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-script] +-------------------------------------------------- +<1> `setScript` to increment the `likes` field on all documents with user `kimchy`. + +`ReindexRequest` supports reindexing from a remote Elasticsearch cluster. When using a remote cluster the query should be +specified inside the `RemoteInfo` object and not using `setSourceQuery`. If both the remote info and the source query are +set it results in a validation error during the request. The reason for this is that the remote Elasticsearch may not +understand queries built by the modern query builders. The remote cluster support works all the way back to Elasticsearch +0.90 and the query language has changed since then. When reaching older versions, it is safer to write the query by hand +in JSON. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-remote] +-------------------------------------------------- +<1> set remote elastic cluster + +`ReindexRequest` also helps in automatically parallelizing using `sliced-scroll` to +slice on `_uid`. Use `setSlices` to specify the number of slices to use. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-slices] +-------------------------------------------------- +<1> set number of slices to use + +`ReindexRequest` uses the `scroll` parameter to control how long it keeps the "search context" alive. +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-scroll] +-------------------------------------------------- +<1> set scroll time + + +==== Optional arguments +In addition to the options above the following arguments can optionally be also provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-timeout] +-------------------------------------------------- +<1> Timeout to wait for the reindex request to be performed as a `TimeValue` + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-request-refresh] +-------------------------------------------------- +<1> Refresh index after calling reindex + + +[[java-rest-high-document-reindex-sync]] +==== Synchronous Execution + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-execute] +-------------------------------------------------- + +[[java-rest-high-document-reindex-async]] +==== Asynchronous Execution + +The asynchronous execution of a reindex request requires both the `ReindexRequest` +instance and an `ActionListener` instance to be passed to the asynchronous +method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-execute-async] +-------------------------------------------------- +<1> The `ReindexRequest` 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 `BulkByScrollResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument and contains a list of individual results for each +operation that was executed. Note that one or more operations might have +failed while the others have been successfully executed. +<2> Called when the whole `ReindexRequest` fails. In this case the raised +exception is provided as an argument and no operation has been executed. + +[[java-rest-high-document-reindex-response]] +==== Reindex Response + +The returned `BulkByScrollResponse` contains information about the executed operations and + allows to iterate over each result as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[reindex-response] +-------------------------------------------------- +<1> Get total time taken +<2> Check if the request timed out +<3> Get total number of docs processed +<4> Number of docs that were updated +<5> Number of docs that were created +<6> Number of docs that were deleted +<7> Number of batches that were executed +<8> Number of skipped docs +<9> Number of version conflicts +<10> Number of times request had to retry bulk index operations +<11> Number of times request had to retry search operations +<12> The total time this request has throttled itself not including the current throttle time if it is currently sleeping +<13> Remaining delay of any current throttle sleep or 0 if not sleeping +<14> Failures during search phase +<15> Failures during bulk index operation diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index e04e391f3e0b..64c95912b5ea 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -15,6 +15,7 @@ Single document APIs:: Multi-document APIs:: * <> * <> +* <> include::document/index.asciidoc[] include::document/get.asciidoc[] @@ -23,6 +24,7 @@ include::document/delete.asciidoc[] include::document/update.asciidoc[] include::document/bulk.asciidoc[] include::document/multi-get.asciidoc[] +include::document/reindex.asciidoc[] == Search APIs diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestReindexAction.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestReindexAction.java index a5520c90b0ff..50d01535d7ff 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestReindexAction.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestReindexAction.java @@ -20,7 +20,6 @@ package org.elasticsearch.index.reindex; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; @@ -118,7 +117,7 @@ protected ReindexRequest buildRequest(RestRequest request) throws IOException { throw new IllegalArgumentException("_reindex doesn't support [pipeline] as a query parameter. " + "Specify it in the [dest] object instead."); } - ReindexRequest internal = new ReindexRequest(new SearchRequest(), new IndexRequest()); + ReindexRequest internal = new ReindexRequest(); try (XContentParser parser = request.contentParser()) { PARSER.parse(parser, internal, null); } diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexMetadataTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexMetadataTests.java index 4611f9dcbcdd..ec34da777b53 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexMetadataTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexMetadataTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.index.reindex.ScrollableHitSource.Hit; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.settings.Settings; /** @@ -73,7 +72,7 @@ protected TestAction action() { @Override protected ReindexRequest request() { - return new ReindexRequest(new SearchRequest(), new IndexRequest()); + return new ReindexRequest(); } private class TestAction extends TransportReindexAction.AsyncIndexBySearchAction { diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexScriptTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexScriptTests.java index 6d3ce558c756..a90b60357c4f 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexScriptTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexScriptTests.java @@ -20,7 +20,6 @@ package org.elasticsearch.index.reindex; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.script.ScriptService; @@ -100,7 +99,7 @@ public void testSetRouting() throws Exception { @Override protected ReindexRequest request() { - return new ReindexRequest(new SearchRequest(), new IndexRequest()); + return new ReindexRequest(); } @Override diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RestReindexActionTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RestReindexActionTests.java index b06948b90581..70e29ed12c5b 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RestReindexActionTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RestReindexActionTests.java @@ -19,8 +19,6 @@ package org.elasticsearch.index.reindex; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; @@ -144,7 +142,7 @@ public void testReindexFromRemoteRequestParsing() throws IOException { request = BytesReference.bytes(b); } try (XContentParser p = createParser(JsonXContent.jsonXContent, request)) { - ReindexRequest r = new ReindexRequest(new SearchRequest(), new IndexRequest()); + ReindexRequest r = new ReindexRequest(); RestReindexAction.PARSER.parse(p, r, null); assertEquals("localhost", r.getRemoteInfo().getHost()); assertArrayEquals(new String[] {"source"}, r.getSearchRequest().indices()); diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java index 0efedf449b56..cc848900b781 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java @@ -20,7 +20,6 @@ package org.elasticsearch.index.reindex; import org.elasticsearch.Version; -import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -47,7 +46,7 @@ */ public class RoundTripTests extends ESTestCase { public void testReindexRequest() throws IOException { - ReindexRequest reindex = new ReindexRequest(new SearchRequest(), new IndexRequest()); + ReindexRequest reindex = new ReindexRequest(); randomRequest(reindex); reindex.getDestination().version(randomFrom(Versions.MATCH_ANY, Versions.MATCH_DELETED, 12L, 1L, 123124L, 12L)); reindex.getDestination().index("test"); diff --git a/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java b/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java index 9b9be3a41476..838293b8b1f6 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/BulkItemResponse.java @@ -28,11 +28,13 @@ import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.common.CheckedConsumer; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Streamable; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.StatusToXContentObject; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -42,6 +44,8 @@ import java.io.IOException; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.elasticsearch.common.xcontent.XContentParserUtils.throwUnknownField; @@ -161,11 +165,11 @@ public static BulkItemResponse fromXContent(XContentParser parser, int id) throw * Represents a failure. */ public static class Failure implements Writeable, ToXContentFragment { - static final String INDEX_FIELD = "index"; - static final String TYPE_FIELD = "type"; - static final String ID_FIELD = "id"; - static final String CAUSE_FIELD = "cause"; - static final String STATUS_FIELD = "status"; + public static final String INDEX_FIELD = "index"; + public static final String TYPE_FIELD = "type"; + public static final String ID_FIELD = "id"; + public static final String CAUSE_FIELD = "cause"; + public static final String STATUS_FIELD = "status"; private final String index; private final String type; @@ -175,6 +179,23 @@ public static class Failure implements Writeable, ToXContentFragment { private final long seqNo; private final boolean aborted; + public static ConstructingObjectParser PARSER = + new ConstructingObjectParser<>( + "bulk_failures", + true, + a -> + new Failure( + (String)a[0], (String)a[1], (String)a[2], (Exception)a[3], RestStatus.fromCode((int)a[4]) + ) + ); + static { + PARSER.declareString(constructorArg(), new ParseField(INDEX_FIELD)); + PARSER.declareString(constructorArg(), new ParseField(TYPE_FIELD)); + PARSER.declareString(optionalConstructorArg(), new ParseField(ID_FIELD)); + PARSER.declareObject(constructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), new ParseField(CAUSE_FIELD)); + PARSER.declareInt(constructorArg(), new ParseField(STATUS_FIELD)); + } + /** * For write failures before operation was assigned a sequence number. * @@ -322,6 +343,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } + public static Failure fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + @Override public String toString() { return Strings.toString(this); diff --git a/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java b/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java index 8536337bfdbc..3b635c823878 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java @@ -252,6 +252,14 @@ public Self setTimeout(TimeValue timeout) { return self(); } + /** + * Timeout to wait for the shards on to be available for each bulk request? + */ + public Self setTimeout(String timeout) { + this.timeout = TimeValue.parseTimeValue(timeout, this.timeout, getClass().getSimpleName() + ".timeout"); + return self(); + } + /** * The number of shard copies that must be active before proceeding with the write. */ diff --git a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponse.java b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponse.java index ac206c2c44f0..7fe60db2ddda 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponse.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponse.java @@ -19,14 +19,23 @@ package org.elasticsearch.index.reindex; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.bulk.BulkItemResponse.Failure; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.index.reindex.BulkByScrollTask.Status; import org.elasticsearch.common.Nullable; +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.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; +import org.elasticsearch.index.reindex.ScrollableHitSource.SearchFailure; +import org.elasticsearch.rest.RestStatus; import java.io.IOException; import java.util.ArrayList; @@ -36,6 +45,7 @@ import static java.lang.Math.min; import static java.util.Objects.requireNonNull; import static org.elasticsearch.common.unit.TimeValue.timeValueNanos; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; /** * Response used for actions that index many documents using a scroll request. @@ -47,6 +57,27 @@ public class BulkByScrollResponse extends ActionResponse implements ToXContentFr private List searchFailures; private boolean timedOut; + private static final String TOOK_FIELD = "took"; + private static final String TIMED_OUT_FIELD = "timed_out"; + private static final String FAILURES_FIELD = "failures"; + + @SuppressWarnings("unchecked") + private static final ObjectParser PARSER = + new ObjectParser<>( + "bulk_by_scroll_response", + true, + BulkByScrollResponseBuilder::new + ); + static { + PARSER.declareLong(BulkByScrollResponseBuilder::setTook, new ParseField(TOOK_FIELD)); + PARSER.declareBoolean(BulkByScrollResponseBuilder::setTimedOut, new ParseField(TIMED_OUT_FIELD)); + PARSER.declareObjectArray( + BulkByScrollResponseBuilder::setFailures, (p, c) -> parseFailure(p), new ParseField(FAILURES_FIELD) + ); + // since the result of BulkByScrollResponse.Status are mixed we also parse that in this + Status.declareFields(PARSER); + } + public BulkByScrollResponse() { } @@ -87,6 +118,10 @@ public long getCreated() { return status.getCreated(); } + public long getTotal() { + return status.getTotal(); + } + public long getDeleted() { return status.getDeleted(); } @@ -171,8 +206,8 @@ public void readFrom(StreamInput in) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.field("took", took.millis()); - builder.field("timed_out", timedOut); + builder.field(TOOK_FIELD, took.millis()); + builder.field(TIMED_OUT_FIELD, timedOut); status.innerXContent(builder, params); builder.startArray("failures"); for (Failure failure: bulkFailures) { @@ -187,6 +222,80 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } + public static BulkByScrollResponse fromXContent(XContentParser parser) { + return PARSER.apply(parser, null).buildResponse(); + } + + private static Object parseFailure(XContentParser parser) throws IOException { + ensureExpectedToken(Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation); + Token token; + String index = null; + String type = null; + String id = null; + Integer status = null; + Integer shardId = null; + String nodeId = null; + ElasticsearchException bulkExc = null; + ElasticsearchException searchExc = null; + while ((token = parser.nextToken()) != Token.END_OBJECT) { + ensureExpectedToken(Token.FIELD_NAME, token, parser::getTokenLocation); + String name = parser.currentName(); + token = parser.nextToken(); + if (token == Token.START_ARRAY) { + parser.skipChildren(); + } else if (token == Token.START_OBJECT) { + switch (name) { + case SearchFailure.REASON_FIELD: + bulkExc = ElasticsearchException.fromXContent(parser); + break; + case Failure.CAUSE_FIELD: + searchExc = ElasticsearchException.fromXContent(parser); + break; + default: + parser.skipChildren(); + } + } else if (token == Token.VALUE_STRING) { + switch (name) { + // This field is the same as SearchFailure.index + case Failure.INDEX_FIELD: + index = parser.text(); + break; + case Failure.TYPE_FIELD: + type = parser.text(); + break; + case Failure.ID_FIELD: + id = parser.text(); + break; + case SearchFailure.NODE_FIELD: + nodeId = parser.text(); + break; + default: + // Do nothing + break; + } + } else if (token == Token.VALUE_NUMBER) { + switch (name) { + case Failure.STATUS_FIELD: + status = parser.intValue(); + break; + case SearchFailure.SHARD_FIELD: + shardId = parser.intValue(); + break; + default: + // Do nothing + break; + } + } + } + if (bulkExc != null) { + return new Failure(index, type, id, bulkExc, RestStatus.fromCode(status)); + } else if (searchExc != null) { + return new SearchFailure(searchExc, index, shardId, nodeId); + } else { + throw new ElasticsearchParseException("failed to parse failures array. At least one of {reason,cause} must be present"); + } + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponseBuilder.java b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponseBuilder.java new file mode 100644 index 000000000000..ad5bfd6e03cd --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollResponseBuilder.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.action.bulk.BulkItemResponse.Failure; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.index.reindex.ScrollableHitSource.SearchFailure; +import org.elasticsearch.index.reindex.BulkByScrollTask.StatusBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Helps build a {@link BulkByScrollResponse}. Used by an instance of {@link ObjectParser} when parsing from XContent. + */ +class BulkByScrollResponseBuilder extends StatusBuilder { + private TimeValue took; + private BulkByScrollTask.Status status; + private List bulkFailures = new ArrayList<>(); + private List searchFailures = new ArrayList<>(); + private boolean timedOut; + + BulkByScrollResponseBuilder() {} + + public void setTook(long took) { + setTook(new TimeValue(took, TimeUnit.MILLISECONDS)); + } + + public void setTook(TimeValue took) { + this.took = took; + } + + public void setStatus(BulkByScrollTask.Status status) { + this.status = status; + } + + public void setFailures(List failures) { + if (failures != null) { + for (Object object: failures) { + if (object instanceof Failure) { + bulkFailures.add((Failure) object); + } else if (object instanceof SearchFailure) { + searchFailures.add((SearchFailure) object); + } + } + } + } + + public void setTimedOut(boolean timedOut) { + this.timedOut = timedOut; + } + + public BulkByScrollResponse buildResponse() { + status = super.buildStatus(); + return new BulkByScrollResponse(took, status, bulkFailures, searchFailures, timedOut); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java index 66e83907d499..5beb86fae6ba 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java @@ -21,27 +21,40 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.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.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.tasks.TaskInfo; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; import static java.lang.Math.min; import static java.util.Collections.emptyList; import static org.elasticsearch.common.unit.TimeValue.timeValueNanos; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; /** * Task storing information about a currently running BulkByScroll request. @@ -188,6 +201,120 @@ public boolean shouldCancelChildrenOnCancellation() { return true; } + /** + * This class acts as a builder for {@link Status}. Once the {@link Status} object is built by calling + * {@link #buildStatus()} it is immutable. Used by an instance of {@link ObjectParser} when parsing from + * XContent. + */ + public static class StatusBuilder { + private Integer sliceId = null; + private Long total = null; + private Long updated = null; + private Long created = null; + private Long deleted = null; + private Integer batches = null; + private Long versionConflicts = null; + private Long noops = null; + private Long bulkRetries = null; + private Long searchRetries = null; + private TimeValue throttled = null; + private Float requestsPerSecond = null; + private String reasonCancelled = null; + private TimeValue throttledUntil = null; + private List sliceStatuses = emptyList(); + + public void setSliceId(Integer sliceId) { + this.sliceId = sliceId; + } + + public void setTotal(Long total) { + this.total = total; + } + + public void setUpdated(Long updated) { + this.updated = updated; + } + + public void setCreated(Long created) { + this.created = created; + } + + public void setDeleted(Long deleted) { + this.deleted = deleted; + } + + public void setBatches(Integer batches) { + this.batches = batches; + } + + public void setVersionConflicts(Long versionConflicts) { + this.versionConflicts = versionConflicts; + } + + public void setNoops(Long noops) { + this.noops = noops; + } + + public void setRetries(Tuple retries) { + if (retries != null) { + setBulkRetries(retries.v1()); + setSearchRetries(retries.v2()); + } + } + + public void setBulkRetries(Long bulkRetries) { + this.bulkRetries = bulkRetries; + } + + public void setSearchRetries(Long searchRetries) { + this.searchRetries = searchRetries; + } + + public void setThrottled(Long throttled) { + if (throttled != null) { + this.throttled = new TimeValue(throttled, TimeUnit.MILLISECONDS); + } + } + + public void setRequestsPerSecond(Float requestsPerSecond) { + if (requestsPerSecond != null) { + requestsPerSecond = requestsPerSecond == -1 ? Float.POSITIVE_INFINITY : requestsPerSecond; + this.requestsPerSecond = requestsPerSecond; + } + } + + public void setReasonCancelled(String reasonCancelled) { + this.reasonCancelled = reasonCancelled; + } + + public void setThrottledUntil(Long throttledUntil) { + if (throttledUntil != null) { + this.throttledUntil = new TimeValue(throttledUntil, TimeUnit.MILLISECONDS); + } + } + + public void setSliceStatuses(List sliceStatuses) { + if (sliceStatuses != null) { + this.sliceStatuses = sliceStatuses; + } + } + + public Status buildStatus() { + if (sliceStatuses.isEmpty()) { + try { + return new Status( + sliceId, total, updated, created, deleted, batches, versionConflicts, noops, bulkRetries, + searchRetries, throttled, requestsPerSecond, reasonCancelled, throttledUntil + ); + } catch (NullPointerException npe) { + throw new IllegalArgumentException("a required field is null when building Status"); + } + } else { + return new Status(sliceStatuses, reasonCancelled); + } + } + } + public static class Status implements Task.Status, SuccessfullyProcessed { public static final String NAME = "bulk-by-scroll"; @@ -203,6 +330,76 @@ public static class Status implements Task.Status, SuccessfullyProcessed { */ public static final String INCLUDE_UPDATED = "include_updated"; + public static final String SLICE_ID_FIELD = "slice_id"; + public static final String TOTAL_FIELD = "total"; + public static final String UPDATED_FIELD = "updated"; + public static final String CREATED_FIELD = "created"; + public static final String DELETED_FIELD = "deleted"; + public static final String BATCHES_FIELD = "batches"; + public static final String VERSION_CONFLICTS_FIELD = "version_conflicts"; + public static final String NOOPS_FIELD = "noops"; + public static final String RETRIES_FIELD = "retries"; + public static final String RETRIES_BULK_FIELD = "bulk"; + public static final String RETRIES_SEARCH_FIELD = "search"; + public static final String THROTTLED_RAW_FIELD = "throttled_millis"; + public static final String THROTTLED_HR_FIELD = "throttled"; + public static final String REQUESTS_PER_SEC_FIELD = "requests_per_second"; + public static final String CANCELED_FIELD = "canceled"; + public static final String THROTTLED_UNTIL_RAW_FIELD = "throttled_until_millis"; + public static final String THROTTLED_UNTIL_HR_FIELD = "throttled_until"; + public static final String SLICES_FIELD = "slices"; + + public static Set FIELDS_SET = new HashSet<>(); + static { + FIELDS_SET.add(SLICE_ID_FIELD); + FIELDS_SET.add(TOTAL_FIELD); + FIELDS_SET.add(UPDATED_FIELD); + FIELDS_SET.add(CREATED_FIELD); + FIELDS_SET.add(DELETED_FIELD); + FIELDS_SET.add(BATCHES_FIELD); + FIELDS_SET.add(VERSION_CONFLICTS_FIELD); + FIELDS_SET.add(NOOPS_FIELD); + FIELDS_SET.add(RETRIES_FIELD); + // No need for inner level fields for retries in the set of outer level fields + FIELDS_SET.add(THROTTLED_RAW_FIELD); + FIELDS_SET.add(THROTTLED_HR_FIELD); + FIELDS_SET.add(REQUESTS_PER_SEC_FIELD); + FIELDS_SET.add(CANCELED_FIELD); + FIELDS_SET.add(THROTTLED_UNTIL_RAW_FIELD); + FIELDS_SET.add(THROTTLED_UNTIL_HR_FIELD); + FIELDS_SET.add(SLICES_FIELD); + } + + @SuppressWarnings("unchecked") + static ConstructingObjectParser, Void> RETRIES_PARSER = new ConstructingObjectParser<>( + "bulk_by_scroll_task_status_retries", + true, + a -> new Tuple(a[0], a[1]) + ); + static { + RETRIES_PARSER.declareLong(constructorArg(), new ParseField(RETRIES_BULK_FIELD)); + RETRIES_PARSER.declareLong(constructorArg(), new ParseField(RETRIES_SEARCH_FIELD)); + } + + public static void declareFields(ObjectParser parser) { + parser.declareInt(StatusBuilder::setSliceId, new ParseField(SLICE_ID_FIELD)); + parser.declareLong(StatusBuilder::setTotal, new ParseField(TOTAL_FIELD)); + parser.declareLong(StatusBuilder::setUpdated, new ParseField(UPDATED_FIELD)); + parser.declareLong(StatusBuilder::setCreated, new ParseField(CREATED_FIELD)); + parser.declareLong(StatusBuilder::setDeleted, new ParseField(DELETED_FIELD)); + parser.declareInt(StatusBuilder::setBatches, new ParseField(BATCHES_FIELD)); + parser.declareLong(StatusBuilder::setVersionConflicts, new ParseField(VERSION_CONFLICTS_FIELD)); + parser.declareLong(StatusBuilder::setNoops, new ParseField(NOOPS_FIELD)); + parser.declareObject(StatusBuilder::setRetries, RETRIES_PARSER, new ParseField(RETRIES_FIELD)); + parser.declareLong(StatusBuilder::setThrottled, new ParseField(THROTTLED_RAW_FIELD)); + parser.declareFloat(StatusBuilder::setRequestsPerSecond, new ParseField(REQUESTS_PER_SEC_FIELD)); + parser.declareString(StatusBuilder::setReasonCancelled, new ParseField(CANCELED_FIELD)); + parser.declareLong(StatusBuilder::setThrottledUntil, new ParseField(THROTTLED_UNTIL_RAW_FIELD)); + parser.declareObjectArray( + StatusBuilder::setSliceStatuses, (p, c) -> StatusOrException.fromXContent(p), new ParseField(SLICES_FIELD) + ); + } + private final Integer sliceId; private final long total; private final long updated; @@ -353,35 +550,40 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder.endObject(); } + /** + * We need to write a manual parser for this because of {@link StatusOrException}. Since + * {@link StatusOrException#fromXContent(XContentParser)} tries to peek at a field first before deciding + * what needs to be it cannot use an {@link ObjectParser}. + */ public XContentBuilder innerXContent(XContentBuilder builder, Params params) throws IOException { if (sliceId != null) { - builder.field("slice_id", sliceId); + builder.field(SLICE_ID_FIELD, sliceId); } - builder.field("total", total); + builder.field(TOTAL_FIELD, total); if (params.paramAsBoolean(INCLUDE_UPDATED, true)) { - builder.field("updated", updated); + builder.field(UPDATED_FIELD, updated); } if (params.paramAsBoolean(INCLUDE_CREATED, true)) { - builder.field("created", created); + builder.field(CREATED_FIELD, created); } - builder.field("deleted", deleted); - builder.field("batches", batches); - builder.field("version_conflicts", versionConflicts); - builder.field("noops", noops); - builder.startObject("retries"); { - builder.field("bulk", bulkRetries); - builder.field("search", searchRetries); + builder.field(DELETED_FIELD, deleted); + builder.field(BATCHES_FIELD, batches); + builder.field(VERSION_CONFLICTS_FIELD, versionConflicts); + builder.field(NOOPS_FIELD, noops); + builder.startObject(RETRIES_FIELD); { + builder.field(RETRIES_BULK_FIELD, bulkRetries); + builder.field(RETRIES_SEARCH_FIELD, searchRetries); } builder.endObject(); - builder.humanReadableField("throttled_millis", "throttled", throttled); - builder.field("requests_per_second", requestsPerSecond == Float.POSITIVE_INFINITY ? -1 : requestsPerSecond); + builder.humanReadableField(THROTTLED_RAW_FIELD, THROTTLED_HR_FIELD, throttled); + builder.field(REQUESTS_PER_SEC_FIELD, requestsPerSecond == Float.POSITIVE_INFINITY ? -1 : requestsPerSecond); if (reasonCancelled != null) { - builder.field("canceled", reasonCancelled); + builder.field(CANCELED_FIELD, reasonCancelled); } - builder.humanReadableField("throttled_until_millis", "throttled_until", throttledUntil); + builder.humanReadableField(THROTTLED_UNTIL_RAW_FIELD, THROTTLED_UNTIL_HR_FIELD, throttledUntil); if (false == sliceStatuses.isEmpty()) { - builder.startArray("slices"); + builder.startArray(SLICES_FIELD); for (StatusOrException slice : sliceStatuses) { if (slice == null) { builder.nullValue(); @@ -394,6 +596,114 @@ public XContentBuilder innerXContent(XContentBuilder builder, Params params) return builder; } + public static Status fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token; + if (parser.currentToken() == Token.START_OBJECT) { + token = parser.nextToken(); + } else { + token = parser.nextToken(); + } + ensureExpectedToken(Token.START_OBJECT, token, parser::getTokenLocation); + token = parser.nextToken(); + ensureExpectedToken(Token.FIELD_NAME, token, parser::getTokenLocation); + return innerFromXContent(parser); + } + + public static Status innerFromXContent(XContentParser parser) throws IOException { + Token token = parser.currentToken(); + String fieldName = parser.currentName(); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); + Integer sliceId = null; + Long total = null; + Long updated = null; + Long created = null; + Long deleted = null; + Integer batches = null; + Long versionConflicts = null; + Long noOps = null; + Long bulkRetries = null; + Long searchRetries = null; + TimeValue throttled = null; + Float requestsPerSecond = null; + String reasonCancelled = null; + TimeValue throttledUntil = null; + List sliceStatuses = new ArrayList<>(); + while ((token = parser.nextToken()) != Token.END_OBJECT) { + if (token == Token.FIELD_NAME) { + fieldName = parser.currentName(); + } else if (token == Token.START_OBJECT) { + if (fieldName.equals(Status.RETRIES_FIELD)) { + Tuple retries = + Status.RETRIES_PARSER.parse(parser, null); + bulkRetries = retries.v1(); + searchRetries = retries.v2(); + } else { + parser.skipChildren(); + } + } else if (token == Token.START_ARRAY) { + if (fieldName.equals(Status.SLICES_FIELD)) { + while ((token = parser.nextToken()) != Token.END_ARRAY) { + sliceStatuses.add(StatusOrException.fromXContent(parser)); + } + } else { + parser.skipChildren(); + } + } else { // else if it is a value + switch (fieldName) { + case Status.SLICE_ID_FIELD: + sliceId = parser.intValue(); + break; + case Status.TOTAL_FIELD: + total = parser.longValue(); + break; + case Status.UPDATED_FIELD: + updated = parser.longValue(); + break; + case Status.CREATED_FIELD: + created = parser.longValue(); + break; + case Status.DELETED_FIELD: + deleted = parser.longValue(); + break; + case Status.BATCHES_FIELD: + batches = parser.intValue(); + break; + case Status.VERSION_CONFLICTS_FIELD: + versionConflicts = parser.longValue(); + break; + case Status.NOOPS_FIELD: + noOps = parser.longValue(); + break; + case Status.THROTTLED_RAW_FIELD: + throttled = new TimeValue(parser.longValue(), TimeUnit.MILLISECONDS); + break; + case Status.REQUESTS_PER_SEC_FIELD: + requestsPerSecond = parser.floatValue(); + requestsPerSecond = requestsPerSecond == -1 ? Float.POSITIVE_INFINITY : requestsPerSecond; + break; + case Status.CANCELED_FIELD: + reasonCancelled = parser.text(); + break; + case Status.THROTTLED_UNTIL_RAW_FIELD: + throttledUntil = new TimeValue(parser.longValue(), TimeUnit.MILLISECONDS); + break; + default: + break; + } + } + } + if (sliceStatuses.isEmpty()) { + return + new Status( + sliceId, total, updated, created, deleted, batches, versionConflicts, noOps, bulkRetries, + searchRetries, throttled, requestsPerSecond, reasonCancelled, throttledUntil + ); + } else { + return new Status(sliceStatuses, reasonCancelled); + } + + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -520,6 +830,44 @@ public List getSliceStatuses() { return sliceStatuses; } + @Override + public int hashCode() { + return Objects.hash( + sliceId, total, updated, created, deleted, batches, versionConflicts, noops, searchRetries, + bulkRetries, throttled, requestsPerSecond, reasonCancelled, throttledUntil, sliceStatuses + ); + } + + public boolean equalsWithoutSliceStatus(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Status other = (Status) o; + return + Objects.equals(sliceId, other.sliceId) && + total == other.total && + updated == other.updated && + created == other.created && + deleted == other.deleted && + batches == other.batches && + versionConflicts == other.versionConflicts && + noops == other.noops && + searchRetries == other.searchRetries && + bulkRetries == other.bulkRetries && + Objects.equals(throttled, other.throttled) && + requestsPerSecond == other.requestsPerSecond && + Objects.equals(reasonCancelled, other.reasonCancelled) && + Objects.equals(throttledUntil, other.throttledUntil); + } + + @Override + public boolean equals(Object o) { + if (equalsWithoutSliceStatus(o)) { + return Objects.equals(sliceStatuses, ((Status) o).sliceStatuses); + } else { + return false; + } + } + private int checkPositive(int value, String name) { if (value < 0) { throw new IllegalArgumentException(name + " must be greater than 0 but was [" + value + "]"); @@ -543,6 +891,19 @@ public static class StatusOrException implements Writeable, ToXContentObject { private final Status status; private final Exception exception; + public static Set EXPECTED_EXCEPTION_FIELDS = new HashSet<>(); + static { + EXPECTED_EXCEPTION_FIELDS.add("type"); + EXPECTED_EXCEPTION_FIELDS.add("reason"); + EXPECTED_EXCEPTION_FIELDS.add("caused_by"); + EXPECTED_EXCEPTION_FIELDS.add("suppressed"); + EXPECTED_EXCEPTION_FIELDS.add("stack_trace"); + EXPECTED_EXCEPTION_FIELDS.add("header"); + EXPECTED_EXCEPTION_FIELDS.add("error"); + EXPECTED_EXCEPTION_FIELDS.add("root_cause"); + } + + public StatusOrException(Status status) { this.status = status; exception = null; @@ -597,6 +958,48 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } + /** + * Since {@link StatusOrException} can contain either an {@link Exception} or a {@link Status} we need to peek + * at a field first before deciding what needs to be parsed since the same object could contains either. + * The {@link #EXPECTED_EXCEPTION_FIELDS} contains the fields that are expected when the serialised object + * was an instance of exception and the {@link Status#FIELDS_SET} is the set of fields expected when the + * serialized object was an instance of Status. + */ + public static StatusOrException fromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + if (token == null) { + token = parser.nextToken(); + } + if (token == Token.VALUE_NULL) { + return null; + } else { + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + token = parser.nextToken(); + // This loop is present only to ignore unknown tokens. It breaks as soon as we find a field + // that is allowed. + while (token != Token.END_OBJECT) { + ensureExpectedToken(Token.FIELD_NAME, token, parser::getTokenLocation); + String fieldName = parser.currentName(); + // weird way to ignore unknown tokens + if (Status.FIELDS_SET.contains(fieldName)) { + return new StatusOrException( + Status.innerFromXContent(parser) + ); + } else if (EXPECTED_EXCEPTION_FIELDS.contains(fieldName)){ + return new StatusOrException(ElasticsearchException.innerFromXContent(parser, false)); + } else { + // Ignore unknown tokens + token = parser.nextToken(); + if (token == Token.START_OBJECT || token == Token.START_ARRAY) { + parser.skipChildren(); + } + token = parser.nextToken(); + } + } + throw new XContentParseException("Unable to parse StatusFromException. Expected fields not found."); + } + } + @Override public boolean equals(Object obj) { if (obj == null) { diff --git a/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java b/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java index e45d039edaea..52a1c89d4b3f 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/ReindexRequest.java @@ -26,6 +26,11 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.uid.Versions; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.tasks.TaskId; import java.io.IOException; @@ -39,7 +44,8 @@ * of reasons, not least of which that scripts are allowed to change the destination request in drastic ways, including changing the index * to which documents are written. */ -public class ReindexRequest extends AbstractBulkIndexByScrollRequest implements CompositeIndicesRequest { +public class ReindexRequest extends AbstractBulkIndexByScrollRequest + implements CompositeIndicesRequest, ToXContentObject { /** * Prototype for index requests. */ @@ -48,9 +54,10 @@ public class ReindexRequest extends AbstractBulkIndexByScrollRequest 0) { + builder.field("size", getSize()); + } + if (getScript() != null) { + builder.field("script", getScript()); + } + if (isAbortOnVersionConflict() == false) { + builder.field("conflicts", "proceed"); + } + } + builder.endObject(); + return builder; + } } diff --git a/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java b/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java index 3ebd261b5847..e255b4db34e3 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/RemoteInfo.java @@ -26,6 +26,8 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; import java.util.HashMap; @@ -35,7 +37,7 @@ import static java.util.Objects.requireNonNull; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; -public class RemoteInfo implements Writeable { +public class RemoteInfo implements Writeable, ToXContentObject { /** * Default {@link #socketTimeout} for requests that don't have one set. */ @@ -190,4 +192,25 @@ public String toString() { } return b.toString(); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (username != null) { + builder.field("username", username); + } + if (password != null) { + builder.field("password", password); + } + builder.field("host", scheme + "://" + host + ":" + port + + (pathPrefix == null ? "" : "/" + pathPrefix)); + if (headers.size() >0 ) { + builder.field("headers", headers); + } + builder.field("socket_timeout", socketTimeout.getStringRep()); + builder.field("connect_timeout", connectTimeout.getStringRep()); + builder.field("query", query); + builder.endObject(); + return builder; + } } diff --git a/server/src/main/java/org/elasticsearch/index/reindex/ScrollableHitSource.java b/server/src/main/java/org/elasticsearch/index/reindex/ScrollableHitSource.java index 917b57a9c974..a3901bb7a568 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/ScrollableHitSource.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/ScrollableHitSource.java @@ -284,6 +284,11 @@ public static class SearchFailure implements Writeable, ToXContentObject { @Nullable private final String nodeId; + public static final String INDEX_FIELD = "index"; + public static final String SHARD_FIELD = "shard"; + public static final String NODE_FIELD = "node"; + public static final String REASON_FIELD = "reason"; + public SearchFailure(Throwable reason, @Nullable String index, @Nullable Integer shardId, @Nullable String nodeId) { this.index = index; this.shardId = shardId; @@ -337,15 +342,15 @@ public String getNodeId() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); if (index != null) { - builder.field("index", index); + builder.field(INDEX_FIELD, index); } if (shardId != null) { - builder.field("shard", shardId); + builder.field(SHARD_FIELD, shardId); } if (nodeId != null) { - builder.field("node", nodeId); + builder.field(NODE_FIELD, nodeId); } - builder.field("reason"); + builder.field(REASON_FIELD); { builder.startObject(); ElasticsearchException.generateThrowableXContent(builder, params, reason); diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index c7564dc5ea83..92ae481a830d 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -1150,9 +1150,7 @@ public void parseXContent(XContentParser parser, boolean checkTrailingTokens) th } } - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); + public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException { if (from != -1) { builder.field(FROM_FIELD.getPreferredName(), from); } @@ -1290,6 +1288,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (collapse != null) { builder.field(COLLAPSE.getPreferredName(), collapse); } + return builder; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + innerToXContent(builder, params); builder.endObject(); return builder; } diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java index a288328391a9..0dd4d6bc8497 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java @@ -24,7 +24,8 @@ import org.elasticsearch.action.bulk.BulkItemResponse.Failure; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; import java.util.List; @@ -33,8 +34,9 @@ import static java.util.Collections.singletonList; import static org.apache.lucene.util.TestUtil.randomSimpleString; import static org.elasticsearch.common.unit.TimeValue.timeValueMillis; +import static org.hamcrest.Matchers.containsString; -public class BulkByScrollResponseTests extends ESTestCase { +public class BulkByScrollResponseTests extends AbstractXContentTestCase { public void testRountTrip() throws IOException { BulkByScrollResponse response = new BulkByScrollResponse(timeValueMillis(randomNonNegativeLong()), @@ -94,4 +96,49 @@ private void assertResponseEquals(BulkByScrollResponse expected, BulkByScrollRes assertEquals(expectedFailure.getReason().getMessage(), actualFailure.getReason().getMessage()); } } + + @Override + protected void assertEqualInstances(BulkByScrollResponse expected, BulkByScrollResponse actual) { + assertEquals(expected.getTook(), actual.getTook()); + BulkByScrollTaskStatusTests.assertEqualStatus(expected.getStatus(), actual.getStatus()); + assertEquals(expected.getBulkFailures().size(), actual.getBulkFailures().size()); + for (int i = 0; i < expected.getBulkFailures().size(); i++) { + Failure expectedFailure = expected.getBulkFailures().get(i); + Failure actualFailure = actual.getBulkFailures().get(i); + assertEquals(expectedFailure.getIndex(), actualFailure.getIndex()); + assertEquals(expectedFailure.getType(), actualFailure.getType()); + assertEquals(expectedFailure.getId(), actualFailure.getId()); + assertThat(expectedFailure.getMessage(), containsString(actualFailure.getMessage())); + assertEquals(expectedFailure.getStatus(), actualFailure.getStatus()); + } + assertEquals(expected.getSearchFailures().size(), actual.getSearchFailures().size()); + for (int i = 0; i < expected.getSearchFailures().size(); i++) { + ScrollableHitSource.SearchFailure expectedFailure = expected.getSearchFailures().get(i); + ScrollableHitSource.SearchFailure actualFailure = actual.getSearchFailures().get(i); + assertEquals(expectedFailure.getIndex(), actualFailure.getIndex()); + assertEquals(expectedFailure.getShardId(), actualFailure.getShardId()); + assertEquals(expectedFailure.getNodeId(), actualFailure.getNodeId()); + assertThat(expectedFailure.getReason().getMessage(), containsString(actualFailure.getReason().getMessage())); + } + } + + @Override + protected BulkByScrollResponse createTestInstance() { + // failures are tested separately, so we can test XContent equivalence at least when we have no failures + return + new BulkByScrollResponse( + timeValueMillis(randomNonNegativeLong()), BulkByScrollTaskStatusTests.randomStatusWithoutException(), + emptyList(), emptyList(), randomBoolean() + ); + } + + @Override + protected BulkByScrollResponse doParseInstance(XContentParser parser) throws IOException { + return BulkByScrollResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } } diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java new file mode 100644 index 000000000000..33c56bacd912 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.index.reindex.BulkByScrollTask.StatusOrException; + +import java.io.IOException; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.containsString; + +public class BulkByScrollTaskStatusOrExceptionTests extends AbstractXContentTestCase { + @Override + protected StatusOrException createTestInstance() { + // failures are tested separately, so we can test XContent equivalence at least when we have no failures + return createTestInstanceWithoutExceptions(); + } + + static StatusOrException createTestInstanceWithoutExceptions() { + return new StatusOrException(BulkByScrollTaskStatusTests.randomStatusWithoutException()); + } + + static StatusOrException createTestInstanceWithExceptions() { + if (randomBoolean()) { + return new StatusOrException(new ElasticsearchException("test_exception")); + } else { + return new StatusOrException(BulkByScrollTaskStatusTests.randomStatus()); + } + } + + @Override + protected StatusOrException doParseInstance(XContentParser parser) throws IOException { + return StatusOrException.fromXContent(parser); + } + + public static void assertEqualStatusOrException(StatusOrException expected, StatusOrException actual) { + if (expected != null && actual != null) { + assertNotSame(expected, actual); + if (expected.getException() == null) { + BulkByScrollTaskStatusTests.assertEqualStatus(expected.getStatus(), actual.getStatus()); + } else { + assertThat( + actual.getException().getMessage(), + containsString(expected.getException().getMessage()) + ); + } + } else { + // If one of them is null both of them should be null + assertSame(expected, actual); + } + } + + @Override + protected void assertEqualInstances(StatusOrException expected, StatusOrException actual) { + assertEqualStatusOrException(expected, actual); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + /** + * Test parsing {@link StatusOrException} with inner failures as they don't support asserting on xcontent equivalence, given that + * exceptions are not parsed back as the same original class. We run the usual {@link AbstractXContentTestCase#testFromXContent()} + * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. + */ + public void testFromXContentWithFailures() throws IOException { + Supplier instanceSupplier = BulkByScrollTaskStatusOrExceptionTests::createTestInstanceWithExceptions; + //with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, + //but that does not bother our assertions, as we only want to test that we don't break. + boolean supportsUnknownFields = true; + //exceptions are not of the same type whenever parsed back + boolean assertToXContentEquivalence = false; + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + getRandomFieldsExcludeFilter(), this::createParser, this::doParseInstance, + this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java index dff07e0f215e..368e1b3bdac0 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java @@ -23,20 +23,27 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.Version; import org.elasticsearch.common.Randomness; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; import org.hamcrest.Matchers; +import org.elasticsearch.index.reindex.BulkByScrollTask.Status; import java.io.IOException; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.stream.IntStream; import static java.lang.Math.abs; import static java.util.stream.Collectors.toList; import static org.apache.lucene.util.TestUtil.randomSimpleString; -import static org.elasticsearch.common.unit.TimeValue.parseTimeValue; +import static org.hamcrest.Matchers.equalTo; -public class BulkByScrollTaskStatusTests extends ESTestCase { +public class BulkByScrollTaskStatusTests extends AbstractXContentTestCase { public void testBulkByTaskStatus() throws IOException { BulkByScrollTask.Status status = randomStatus(); BytesStreamOutput out = new BytesStreamOutput(); @@ -98,6 +105,22 @@ public static BulkByScrollTask.Status randomStatus() { return new BulkByScrollTask.Status(statuses, randomBoolean() ? "test" : null); } + public static BulkByScrollTask.Status randomStatusWithoutException() { + if (randomBoolean()) { + return randomWorkingStatus(null); + } + boolean canHaveNullStatues = randomBoolean(); + List statuses = IntStream.range(0, between(0, 10)) + .mapToObj(i -> { + if (canHaveNullStatues && LuceneTestCase.rarely()) { + return null; + } + return new BulkByScrollTask.StatusOrException(randomWorkingStatus(i)); + }) + .collect(toList()); + return new BulkByScrollTask.Status(statuses, randomBoolean() ? "test" : null); + } + private static BulkByScrollTask.Status randomWorkingStatus(Integer sliceId) { // These all should be believably small because we sum them if we have multiple workers int total = between(0, 10000000); @@ -109,8 +132,65 @@ private static BulkByScrollTask.Status randomWorkingStatus(Integer sliceId) { long versionConflicts = between(0, total); long bulkRetries = between(0, 10000000); long searchRetries = between(0, 100000); - return new BulkByScrollTask.Status(sliceId, total, updated, created, deleted, batches, versionConflicts, noops, bulkRetries, - searchRetries, parseTimeValue(randomPositiveTimeValue(), "test"), abs(Randomness.get().nextFloat()), - randomBoolean() ? null : randomSimpleString(Randomness.get()), parseTimeValue(randomPositiveTimeValue(), "test")); + // smallest unit of time during toXContent is Milliseconds + TimeUnit[] timeUnits = {TimeUnit.MILLISECONDS, TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS}; + TimeValue throttled = new TimeValue(randomIntBetween(0, 1000), randomFrom(timeUnits)); + TimeValue throttledUntil = new TimeValue(randomIntBetween(0, 1000), randomFrom(timeUnits)); + return + new BulkByScrollTask.Status( + sliceId, total, updated, created, deleted, batches, versionConflicts, noops, + bulkRetries, searchRetries, throttled, abs(Randomness.get().nextFloat()), + randomBoolean() ? null : randomSimpleString(Randomness.get()), throttledUntil + ); + } + + public static void assertEqualStatus(BulkByScrollTask.Status expected, BulkByScrollTask.Status actual) { + assertNotSame(expected, actual); + assertTrue(expected.equalsWithoutSliceStatus(actual)); + assertThat(expected.getSliceStatuses().size(), equalTo(actual.getSliceStatuses().size())); + for (int i = 0; i< expected.getSliceStatuses().size(); i++) { + BulkByScrollTaskStatusOrExceptionTests.assertEqualStatusOrException( + expected.getSliceStatuses().get(i), + actual.getSliceStatuses().get(i) + ); + } + } + + @Override + protected void assertEqualInstances(BulkByScrollTask.Status first, BulkByScrollTask.Status second) { + assertEqualStatus(first, second); + } + + @Override + protected BulkByScrollTask.Status createTestInstance() { + // failures are tested separately, so we can test xcontent equivalence at least when we have no failures + return randomStatusWithoutException(); + } + + @Override + protected BulkByScrollTask.Status doParseInstance(XContentParser parser) throws IOException { + return BulkByScrollTask.Status.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + /** + * Test parsing {@link Status} with inner failures as they don't support asserting on xcontent equivalence, given that + * exceptions are not parsed back as the same original class. We run the usual {@link AbstractXContentTestCase#testFromXContent()} + * without failures, and this other test with failures where we disable asserting on xcontent equivalence at the end. + */ + public void testFromXContentWithFailures() throws IOException { + Supplier instanceSupplier = BulkByScrollTaskStatusTests::randomStatus; + //with random fields insertion in the inner exceptions, some random stuff may be parsed back as metadata, + //but that does not bother our assertions, as we only want to test that we don't break. + boolean supportsUnknownFields = true; + //exceptions are not of the same type whenever parsed back + boolean assertToXContentEquivalence = false; + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + getRandomFieldsExcludeFilter(), this::createParser, this::doParseInstance, + this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); } } diff --git a/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java b/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java index 6c1988a1440e..1c3d539263e7 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java @@ -20,8 +20,6 @@ package org.elasticsearch.index.reindex; import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.search.slice.SliceBuilder; @@ -89,9 +87,9 @@ protected void extraForSliceAssertions(ReindexRequest original, ReindexRequest f @Override protected ReindexRequest newRequest() { - ReindexRequest reindex = new ReindexRequest(new SearchRequest(), new IndexRequest()); - reindex.getSearchRequest().indices("source"); - reindex.getDestination().index("dest"); + ReindexRequest reindex = new ReindexRequest(); + reindex.setSourceIndices("source"); + reindex.setDestIndex("dest"); return reindex; } } diff --git a/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexer.java b/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexer.java index 82208f1f5ceb..d7be0a389e30 100644 --- a/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexer.java +++ b/x-pack/plugin/upgrade/src/main/java/org/elasticsearch/xpack/upgrade/InternalIndexReindexer.java @@ -6,8 +6,6 @@ package org.elasticsearch.xpack.upgrade; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.Client; import org.elasticsearch.client.ParentTaskAssigningClient; @@ -116,10 +114,10 @@ private void removeReadOnlyBlock(ParentTaskAssigningClient parentAwareClient, St private void reindex(ParentTaskAssigningClient parentAwareClient, String index, String newIndex, ActionListener listener) { - SearchRequest sourceRequest = new SearchRequest(index); - sourceRequest.types(types); - IndexRequest destinationRequest = new IndexRequest(newIndex); - ReindexRequest reindexRequest = new ReindexRequest(sourceRequest, destinationRequest); + ReindexRequest reindexRequest = new ReindexRequest(); + reindexRequest.setSourceIndices(index); + reindexRequest.setSourceDocTypes(types); + reindexRequest.setDestIndex(newIndex); reindexRequest.setRefresh(true); reindexRequest.setScript(transformScript); parentAwareClient.execute(ReindexAction.INSTANCE, reindexRequest, listener); From 6c8f5688083ba6198c1084eeb2ef75f3a24d1f7d Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 28 Aug 2018 14:20:14 -0400 Subject: [PATCH 196/283] Switch remaining LLREST usage to new style Requests (#33171) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. In a long series of PRs I've changed all of the old style requests that I could find with `grep`. In this PR I change all requests that I could find by *removing* the deprecated methods. Since this is a non-trivial change I do not include actually removing the deprecated requests. I'll do that in a follow up. But this should be the last set of usage removals before the actual deprecated method removal. Yay! --- .../SearchTemplateWithoutContentIT.java | 5 +- .../reindex/ReindexWithoutContentIT.java | 3 +- .../rest/Netty4BadRequestIT.java | 3 +- .../exporter/http/HttpExportBulk.java | 12 ++- .../AbstractPrivilegeTestCase.java | 82 +++++++++---------- .../integration/ClusterPrivilegeTests.java | 22 ++--- .../integration/IndexPrivilegeTests.java | 22 ++--- 7 files changed, 74 insertions(+), 75 deletions(-) diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateWithoutContentIT.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateWithoutContentIT.java index cbc6adf6be22..023d3b246761 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateWithoutContentIT.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateWithoutContentIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.script.mustache; +import org.elasticsearch.client.Request; import org.elasticsearch.client.ResponseException; import org.elasticsearch.test.rest.ESRestTestCase; @@ -30,14 +31,14 @@ public class SearchTemplateWithoutContentIT extends ESRestTestCase { public void testSearchTemplateMissingBody() throws IOException { ResponseException responseException = expectThrows(ResponseException.class, () -> client().performRequest( - randomBoolean() ? "POST" : "GET", "/_search/template")); + new Request(randomBoolean() ? "POST" : "GET", "/_search/template"))); assertEquals(400, responseException.getResponse().getStatusLine().getStatusCode()); assertThat(responseException.getMessage(), containsString("request body or source parameter is required")); } public void testMultiSearchTemplateMissingBody() throws IOException { ResponseException responseException = expectThrows(ResponseException.class, () -> client().performRequest( - randomBoolean() ? "POST" : "GET", "/_msearch/template")); + new Request(randomBoolean() ? "POST" : "GET", "/_msearch/template"))); assertEquals(400, responseException.getResponse().getStatusLine().getStatusCode()); assertThat(responseException.getMessage(), containsString("request body or source parameter is required")); } diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexWithoutContentIT.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexWithoutContentIT.java index f580b1400c3b..73745ca690d7 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexWithoutContentIT.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexWithoutContentIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.reindex; +import org.elasticsearch.client.Request; import org.elasticsearch.client.ResponseException; import org.elasticsearch.test.rest.ESRestTestCase; @@ -30,7 +31,7 @@ public class ReindexWithoutContentIT extends ESRestTestCase { public void testReindexMissingBody() throws IOException { ResponseException responseException = expectThrows(ResponseException.class, () -> client().performRequest( - "POST", "/_reindex")); + new Request("POST", "/_reindex"))); assertEquals(400, responseException.getResponse().getStatusLine().getStatusCode()); assertThat(responseException.getMessage(), containsString("request body is required")); } diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/rest/Netty4BadRequestIT.java b/modules/transport-netty4/src/test/java/org/elasticsearch/rest/Netty4BadRequestIT.java index 17a62b3a440e..cfda71f10096 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/rest/Netty4BadRequestIT.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/rest/Netty4BadRequestIT.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.nio.charset.Charset; -import java.util.Collections; import java.util.Map; import static org.elasticsearch.rest.RestStatus.BAD_REQUEST; @@ -71,7 +70,7 @@ public void testBadRequest() throws IOException { final ResponseException e = expectThrows( ResponseException.class, - () -> client().performRequest(randomFrom("GET", "POST", "PUT"), path, Collections.emptyMap())); + () -> client().performRequest(new Request(randomFrom("GET", "POST", "PUT"), path))); assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(BAD_REQUEST.getStatus())); assertThat(e, hasToString(containsString("too_long_frame_exception"))); assertThat(e, hasToString(matches("An HTTP line is larger than \\d+ bytes"))); diff --git a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExportBulk.java b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExportBulk.java index f23a545b6a5f..ded3064a2a66 100644 --- a/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExportBulk.java +++ b/x-pack/plugin/monitoring/src/main/java/org/elasticsearch/xpack/monitoring/exporter/http/HttpExportBulk.java @@ -5,14 +5,14 @@ */ package org.elasticsearch.xpack.monitoring.exporter.http; -import org.apache.http.HttpEntity; -import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NByteArrayEntity; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.apache.lucene.util.BytesRef; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseListener; import org.elasticsearch.client.RestClient; @@ -94,9 +94,13 @@ public void doFlush(ActionListener listener) throws ExportException { if (payload == null) { listener.onFailure(new ExportException("unable to send documents because none were loaded for export bulk [{}]", name)); } else if (payload.length != 0) { - final HttpEntity body = new ByteArrayEntity(payload, ContentType.APPLICATION_JSON); + final Request request = new Request("POST", "/_bulk"); + for (Map.Entry param : params.entrySet()) { + request.addParameter(param.getKey(), param.getValue()); + } + request.setEntity(new NByteArrayEntity(payload, ContentType.APPLICATION_JSON)); - client.performRequestAsync("POST", "/_bulk", params, body, new ResponseListener() { + client.performRequestAsync(request, new ResponseListener() { @Override public void onSuccess(Response response) { try { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/AbstractPrivilegeTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/AbstractPrivilegeTestCase.java index 246b584a83b0..9317e9f8dcb5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/AbstractPrivilegeTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/AbstractPrivilegeTestCase.java @@ -7,10 +7,9 @@ import org.apache.http.HttpEntity; import org.apache.http.StatusLine; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.message.BasicHeader; import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; import org.elasticsearch.common.settings.SecureString; @@ -18,9 +17,7 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import java.io.IOException; -import java.util.HashMap; import java.util.Locale; -import java.util.Map; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -28,64 +25,59 @@ import static org.hamcrest.Matchers.not; /** - * a helper class that contains a couple of HTTP helper methods + * A helper class that contains a couple of HTTP helper methods. */ public abstract class AbstractPrivilegeTestCase extends SecuritySingleNodeTestCase { - protected void assertAccessIsAllowed(String user, String method, String uri, String body, - Map params) throws IOException { - Response response = getRestClient().performRequest(method, uri, params, entityOrNull(body), - new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, - UsernamePasswordToken.basicAuthHeaderValue(user, new SecureString("passwd".toCharArray())))); + protected void assertAccessIsAllowed(String user, Request request) throws IOException { + setUser(request, user); + Response response = getRestClient().performRequest(request); StatusLine statusLine = response.getStatusLine(); - String message = String.format(Locale.ROOT, "%s %s: Expected no error got %s %s with body %s", method, uri, - statusLine.getStatusCode(), statusLine.getReasonPhrase(), EntityUtils.toString(response.getEntity())); + String message = String.format(Locale.ROOT, "%s %s: Expected no error got %s %s with body %s", + request.getMethod(), request.getEndpoint(), statusLine.getStatusCode(), + statusLine.getReasonPhrase(), EntityUtils.toString(response.getEntity())); assertThat(message, statusLine.getStatusCode(), is(not(greaterThanOrEqualTo(400)))); } protected void assertAccessIsAllowed(String user, String method, String uri, String body) throws IOException { - assertAccessIsAllowed(user, method, uri, body, new HashMap<>()); + Request request = new Request(method, uri); + request.setJsonEntity(body); + assertAccessIsAllowed(user, request); } protected void assertAccessIsAllowed(String user, String method, String uri) throws IOException { - assertAccessIsAllowed(user, method, uri, null, new HashMap<>()); + assertAccessIsAllowed(user, new Request(method, uri)); } - protected void assertAccessIsDenied(String user, String method, String uri, String body) throws IOException { - assertAccessIsDenied(user, method, uri, body, new HashMap<>()); - } - - protected void assertAccessIsDenied(String user, String method, String uri) throws IOException { - assertAccessIsDenied(user, method, uri, null, new HashMap<>()); - } - - protected void assertAccessIsDenied(String user, String method, String uri, String body, - Map params) throws IOException { - ResponseException responseException = expectThrows(ResponseException.class, - () -> getRestClient().performRequest(method, uri, params, entityOrNull(body), - new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, - UsernamePasswordToken.basicAuthHeaderValue(user, new SecureString("passwd".toCharArray()))))); + protected void assertAccessIsDenied(String user, Request request) throws IOException { + setUser(request, user); + ResponseException responseException = expectThrows(ResponseException.class, () -> getRestClient().performRequest(request)); StatusLine statusLine = responseException.getResponse().getStatusLine(); - String message = String.format(Locale.ROOT, "%s %s body %s: Expected 403, got %s %s with body %s", method, uri, body, + String requestBody = request.getEntity() == null ? "" : "with body " + EntityUtils.toString(request.getEntity()); + String message = String.format(Locale.ROOT, "%s %s body %s: Expected 403, got %s %s with body %s", + request.getMethod(), request.getEndpoint(), requestBody, statusLine.getStatusCode(), statusLine.getReasonPhrase(), EntityUtils.toString(responseException.getResponse().getEntity())); assertThat(message, statusLine.getStatusCode(), is(403)); } + protected void assertAccessIsDenied(String user, String method, String uri, String body) throws IOException { + Request request = new Request(method, uri); + request.setJsonEntity(body); + assertAccessIsDenied(user, request); + } - protected void assertBodyHasAccessIsDenied(String user, String method, String uri, String body) throws IOException { - assertBodyHasAccessIsDenied(user, method, uri, body, new HashMap<>()); + protected void assertAccessIsDenied(String user, String method, String uri) throws IOException { + assertAccessIsDenied(user, new Request(method, uri)); } /** * Like {@code assertAcessIsDenied}, but for _bulk requests since the entire * request will not be failed, just the individual ones */ - protected void assertBodyHasAccessIsDenied(String user, String method, String uri, String body, - Map params) throws IOException { - Response resp = getRestClient().performRequest(method, uri, params, entityOrNull(body), - new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, - UsernamePasswordToken.basicAuthHeaderValue(user, new SecureString("passwd".toCharArray())))); + protected void assertBodyHasAccessIsDenied(String user, Request request) throws IOException { + setUser(request, user); + Response resp = getRestClient().performRequest(request); StatusLine statusLine = resp.getStatusLine(); assertThat(statusLine.getStatusCode(), is(200)); HttpEntity bodyEntity = resp.getEntity(); @@ -93,11 +85,15 @@ protected void assertBodyHasAccessIsDenied(String user, String method, String ur assertThat(bodyStr, containsString("unauthorized for user [" + user + "]")); } - private static HttpEntity entityOrNull(String body) { - HttpEntity entity = null; - if (body != null) { - entity = new StringEntity(body, ContentType.APPLICATION_JSON); - } - return entity; + protected void assertBodyHasAccessIsDenied(String user, String method, String uri, String body) throws IOException { + Request request = new Request(method, uri); + request.setJsonEntity(body); + assertBodyHasAccessIsDenied(user, request); + } + + private void setUser(Request request, String user) { + RequestOptions.Builder options = RequestOptions.DEFAULT.toBuilder(); + options.addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(user, new SecureString("passwd".toCharArray()))); + request.setOptions(options); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java index 03d0310a136b..bf81fd77dc59 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/ClusterPrivilegeTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.integration; import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse; +import org.elasticsearch.client.Request; import org.elasticsearch.cluster.SnapshotsInProgress; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; @@ -15,9 +16,7 @@ import org.junit.BeforeClass; import java.nio.file.Path; -import java.util.Map; -import static java.util.Collections.singletonMap; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.is; @@ -132,10 +131,12 @@ public void testThatSnapshotAndRestore() throws Exception { assertAccessIsDenied("user_c", "PUT", "/_snapshot/my-repo", repoJson); assertAccessIsAllowed("user_a", "PUT", "/_snapshot/my-repo", repoJson); - Map params = singletonMap("refresh", "true"); - assertAccessIsDenied("user_a", "PUT", "/someindex/bar/1", "{ \"name\" : \"elasticsearch\" }", params); - assertAccessIsDenied("user_b", "PUT", "/someindex/bar/1", "{ \"name\" : \"elasticsearch\" }", params); - assertAccessIsAllowed("user_c", "PUT", "/someindex/bar/1", "{ \"name\" : \"elasticsearch\" }", params); + Request createBar = new Request("PUT", "/someindex/bar/1"); + createBar.setJsonEntity("{ \"name\" : \"elasticsearch\" }"); + createBar.addParameter("refresh", "true"); + assertAccessIsDenied("user_a", createBar); + assertAccessIsDenied("user_b", createBar); + assertAccessIsAllowed("user_c", createBar); assertAccessIsDenied("user_b", "PUT", "/_snapshot/my-repo/my-snapshot", "{ \"indices\": \"someindex\" }"); assertAccessIsDenied("user_c", "PUT", "/_snapshot/my-repo/my-snapshot", "{ \"indices\": \"someindex\" }"); @@ -152,10 +153,11 @@ public void testThatSnapshotAndRestore() throws Exception { assertAccessIsDenied("user_b", "DELETE", "/someindex"); assertAccessIsAllowed("user_c", "DELETE", "/someindex"); - params = singletonMap("wait_for_completion", "true"); - assertAccessIsDenied("user_b", "POST", "/_snapshot/my-repo/my-snapshot/_restore", null, params); - assertAccessIsDenied("user_c", "POST", "/_snapshot/my-repo/my-snapshot/_restore", null, params); - assertAccessIsAllowed("user_a", "POST", "/_snapshot/my-repo/my-snapshot/_restore", null, params); + Request restoreSnapshotRequest = new Request("POST", "/_snapshot/my-repo/my-snapshot/_restore"); + restoreSnapshotRequest.addParameter("wait_for_completion", "true"); + assertAccessIsDenied("user_b", restoreSnapshotRequest); + assertAccessIsDenied("user_c", restoreSnapshotRequest); + assertAccessIsAllowed("user_a", restoreSnapshotRequest); assertAccessIsDenied("user_a", "GET", "/someindex/bar/1"); assertAccessIsDenied("user_b", "GET", "/someindex/bar/1"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java index 57262822982f..ed82808af761 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java @@ -13,11 +13,8 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.junit.Before; -import java.util.Collections; import java.util.Locale; -import java.util.Map; -import static java.util.Collections.singletonMap; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTimeout; import static org.hamcrest.Matchers.is; @@ -143,11 +140,12 @@ protected String configUsersRoles() { @Before public void insertBaseDocumentsAsAdmin() throws Exception { // indices: a,b,c,abc - Map params = singletonMap("refresh", "true"); - assertAccessIsAllowed("admin", "PUT", "/a/foo/1", jsonDoc, params); - assertAccessIsAllowed("admin", "PUT", "/b/foo/1", jsonDoc, params); - assertAccessIsAllowed("admin", "PUT", "/c/foo/1", jsonDoc, params); - assertAccessIsAllowed("admin", "PUT", "/abc/foo/1", jsonDoc, params); + for (String index : new String[] {"a", "b", "c", "abc"}) { + Request request = new Request("PUT", "/" + index + "/foo/1"); + request.setJsonEntity(jsonDoc); + request.addParameter("refresh", "true"); + assertAccessIsAllowed("admin", request); + } } private static String randomIndex() { @@ -402,8 +400,6 @@ public void testThatUnknownUserIsRejectedProperly() throws Exception { } private void assertUserExecutes(String user, String action, String index, boolean userIsAllowed) throws Exception { - Map refreshParams = Collections.emptyMap();//singletonMap("refresh", "true"); - switch (action) { case "all" : if (userIsAllowed) { @@ -438,7 +434,7 @@ private void assertUserExecutes(String user, String action, String index, boolea assertAccessIsAllowed(user, "POST", "/" + index + "/_open"); assertAccessIsAllowed(user, "POST", "/" + index + "/_cache/clear"); // indexing a document to have the mapping available, and wait for green state to make sure index is created - assertAccessIsAllowed("admin", "PUT", "/" + index + "/foo/1", jsonDoc, refreshParams); + assertAccessIsAllowed("admin", "PUT", "/" + index + "/foo/1", jsonDoc); assertNoTimeout(client().admin().cluster().prepareHealth(index).setWaitForGreenStatus().get()); assertAccessIsAllowed(user, "GET", "/" + index + "/_mapping/foo/field/name"); assertAccessIsAllowed(user, "GET", "/" + index + "/_settings"); @@ -535,8 +531,8 @@ private void assertUserExecutes(String user, String action, String index, boolea case "delete" : String jsonDoc = "{ \"name\" : \"docToDelete\"}"; - assertAccessIsAllowed("admin", "PUT", "/" + index + "/foo/docToDelete", jsonDoc, refreshParams); - assertAccessIsAllowed("admin", "PUT", "/" + index + "/foo/docToDelete2", jsonDoc, refreshParams); + assertAccessIsAllowed("admin", "PUT", "/" + index + "/foo/docToDelete", jsonDoc); + assertAccessIsAllowed("admin", "PUT", "/" + index + "/foo/docToDelete2", jsonDoc); if (userIsAllowed) { assertAccessIsAllowed(user, "DELETE", "/" + index + "/foo/docToDelete"); } else { From e9b0807c674b6494a82e3fbdca826d9818985bbd Mon Sep 17 00:00:00 2001 From: Jake Landis Date: Tue, 28 Aug 2018 11:55:04 -0700 Subject: [PATCH 197/283] ingest: minor - update test to include dissect (#33211) This change also includes placing the bytes processor in the correct order (helps to avoid merge conflict when back patching processors) --- .../src/test/resources/rest-api-spec/test/ingest/10_basic.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/10_basic.yml b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/10_basic.yml index 86557946ac0d..eb23b7840ee6 100644 --- a/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/10_basic.yml +++ b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/10_basic.yml @@ -10,9 +10,11 @@ - contains: { nodes.$master.modules: { name: ingest-common } } - contains: { nodes.$master.ingest.processors: { type: append } } + - contains: { nodes.$master.ingest.processors: { type: bytes } } - contains: { nodes.$master.ingest.processors: { type: convert } } - contains: { nodes.$master.ingest.processors: { type: date } } - contains: { nodes.$master.ingest.processors: { type: date_index_name } } + - contains: { nodes.$master.ingest.processors: { type: dissect } } - contains: { nodes.$master.ingest.processors: { type: dot_expander } } - contains: { nodes.$master.ingest.processors: { type: fail } } - contains: { nodes.$master.ingest.processors: { type: foreach } } @@ -30,4 +32,3 @@ - contains: { nodes.$master.ingest.processors: { type: split } } - contains: { nodes.$master.ingest.processors: { type: trim } } - contains: { nodes.$master.ingest.processors: { type: uppercase } } - - contains: { nodes.$master.ingest.processors: { type: bytes } } From a381749aac3857e4c186f709ec35adef897a4a6f Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Tue, 28 Aug 2018 12:40:41 -0700 Subject: [PATCH 198/283] Painless: Fix Semicolon Regression (#33212) Trailers (statements following something like an if statement) that don't use brackets currently require a semicolon even if they're the last statement. This is a regression caused by (#29566) and noted by (#33193). This change fixes the regression and adds a test for the broken case. --- .../src/main/antlr/PainlessParser.g4 | 4 +- .../painless/antlr/PainlessLexer.java | 88 +- .../painless/antlr/PainlessParser.java | 1255 ++++++++--------- .../elasticsearch/painless/antlr/Walker.java | 4 - .../elasticsearch/painless/BasicAPITests.java | 4 + .../elasticsearch/painless/RegexTests.java | 2 +- .../painless/WhenThingsGoWrongTests.java | 2 +- 7 files changed, 667 insertions(+), 692 deletions(-) diff --git a/modules/lang-painless/src/main/antlr/PainlessParser.g4 b/modules/lang-painless/src/main/antlr/PainlessParser.g4 index 5292b4d19505..27db9222f32e 100644 --- a/modules/lang-painless/src/main/antlr/PainlessParser.g4 +++ b/modules/lang-painless/src/main/antlr/PainlessParser.g4 @@ -22,7 +22,7 @@ parser grammar PainlessParser; options { tokenVocab=PainlessLexer; } source - : function* statement* dstatement? EOF + : function* statement* EOF ; function @@ -35,7 +35,7 @@ parameters statement : rstatement - | dstatement SEMICOLON + | dstatement ( SEMICOLON | EOF ) ; // Note we use a predicate on the if/else case here to prevent the diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessLexer.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessLexer.java index 7fa10f6e9fbf..feebacc60687 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessLexer.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessLexer.java @@ -1,17 +1,13 @@ // ANTLR GENERATED CODE: DO NOT EDIT package org.elasticsearch.painless.antlr; - -import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.Lexer; -import org.antlr.v4.runtime.RuleContext; -import org.antlr.v4.runtime.RuntimeMetaData; -import org.antlr.v4.runtime.Vocabulary; -import org.antlr.v4.runtime.VocabularyImpl; -import org.antlr.v4.runtime.atn.ATN; -import org.antlr.v4.runtime.atn.ATNDeserializer; -import org.antlr.v4.runtime.atn.LexerATNSimulator; -import org.antlr.v4.runtime.atn.PredictionContextCache; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.atn.*; import org.antlr.v4.runtime.dfa.DFA; +import org.antlr.v4.runtime.misc.*; @SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"}) abstract class PainlessLexer extends Lexer { @@ -21,16 +17,16 @@ abstract class PainlessLexer extends Lexer { protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); public static final int - WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, - NSDOT=10, COMMA=11, SEMICOLON=12, IF=13, IN=14, ELSE=15, WHILE=16, DO=17, - FOR=18, CONTINUE=19, BREAK=20, RETURN=21, NEW=22, TRY=23, CATCH=24, THROW=25, - THIS=26, INSTANCEOF=27, BOOLNOT=28, BWNOT=29, MUL=30, DIV=31, REM=32, - ADD=33, SUB=34, LSH=35, RSH=36, USH=37, LT=38, LTE=39, GT=40, GTE=41, - EQ=42, EQR=43, NE=44, NER=45, BWAND=46, XOR=47, BWOR=48, BOOLAND=49, BOOLOR=50, - COND=51, COLON=52, ELVIS=53, REF=54, ARROW=55, FIND=56, MATCH=57, INCR=58, - DECR=59, ASSIGN=60, AADD=61, ASUB=62, AMUL=63, ADIV=64, AREM=65, AAND=66, - AXOR=67, AOR=68, ALSH=69, ARSH=70, AUSH=71, OCTAL=72, HEX=73, INTEGER=74, - DECIMAL=75, STRING=76, REGEX=77, TRUE=78, FALSE=79, NULL=80, TYPE=81, + WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, + NSDOT=10, COMMA=11, SEMICOLON=12, IF=13, IN=14, ELSE=15, WHILE=16, DO=17, + FOR=18, CONTINUE=19, BREAK=20, RETURN=21, NEW=22, TRY=23, CATCH=24, THROW=25, + THIS=26, INSTANCEOF=27, BOOLNOT=28, BWNOT=29, MUL=30, DIV=31, REM=32, + ADD=33, SUB=34, LSH=35, RSH=36, USH=37, LT=38, LTE=39, GT=40, GTE=41, + EQ=42, EQR=43, NE=44, NER=45, BWAND=46, XOR=47, BWOR=48, BOOLAND=49, BOOLOR=50, + COND=51, COLON=52, ELVIS=53, REF=54, ARROW=55, FIND=56, MATCH=57, INCR=58, + DECR=59, ASSIGN=60, AADD=61, ASUB=62, AMUL=63, ADIV=64, AREM=65, AAND=66, + AXOR=67, AOR=68, ALSH=69, ARSH=70, AUSH=71, OCTAL=72, HEX=73, INTEGER=74, + DECIMAL=75, STRING=76, REGEX=77, TRUE=78, FALSE=79, NULL=80, TYPE=81, ID=82, DOTINTEGER=83, DOTID=84; public static final int AFTER_DOT = 1; public static String[] modeNames = { @@ -38,39 +34,39 @@ abstract class PainlessLexer extends Lexer { }; public static final String[] ruleNames = { - "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", "DOT", - "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", "DO", "FOR", - "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "THIS", - "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", "SUB", "LSH", - "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", "NER", "BWAND", - "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", "REF", "ARROW", - "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", "ADIV", - "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", "HEX", - "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", "NULL", "TYPE", + "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", "DOT", + "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", "DO", "FOR", + "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", "THIS", + "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", "SUB", "LSH", + "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", "NER", "BWAND", + "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", "REF", "ARROW", + "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", "AMUL", "ADIV", + "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", "OCTAL", "HEX", + "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", "NULL", "TYPE", "ID", "DOTINTEGER", "DOTID" }; private static final String[] _LITERAL_NAMES = { - null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "'?.'", - "','", "';'", "'if'", "'in'", "'else'", "'while'", "'do'", "'for'", "'continue'", - "'break'", "'return'", "'new'", "'try'", "'catch'", "'throw'", "'this'", - "'instanceof'", "'!'", "'~'", "'*'", "'/'", "'%'", "'+'", "'-'", "'<<'", - "'>>'", "'>>>'", "'<'", "'<='", "'>'", "'>='", "'=='", "'==='", "'!='", - "'!=='", "'&'", "'^'", "'|'", "'&&'", "'||'", "'?'", "':'", "'?:'", "'::'", - "'->'", "'=~'", "'==~'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", - "'/='", "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, + null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "'?.'", + "','", "';'", "'if'", "'in'", "'else'", "'while'", "'do'", "'for'", "'continue'", + "'break'", "'return'", "'new'", "'try'", "'catch'", "'throw'", "'this'", + "'instanceof'", "'!'", "'~'", "'*'", "'/'", "'%'", "'+'", "'-'", "'<<'", + "'>>'", "'>>>'", "'<'", "'<='", "'>'", "'>='", "'=='", "'==='", "'!='", + "'!=='", "'&'", "'^'", "'|'", "'&&'", "'||'", "'?'", "':'", "'?:'", "'::'", + "'->'", "'=~'", "'==~'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", + "'/='", "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, null, null, null, null, "'true'", "'false'", "'null'" }; private static final String[] _SYMBOLIC_NAMES = { - null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", - "DOT", "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", "DO", - "FOR", "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", - "THIS", "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", - "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", - "NER", "BWAND", "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", - "REF", "ARROW", "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", - "AMUL", "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", - "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", + null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", + "DOT", "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", "DO", + "FOR", "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", + "THIS", "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", + "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", + "NER", "BWAND", "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", + "REF", "ARROW", "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", + "AMUL", "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", + "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", "NULL", "TYPE", "ID", "DOTINTEGER", "DOTID" }; public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessParser.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessParser.java index bef57d22e9ea..5a823ecfda30 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessParser.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/PainlessParser.java @@ -1,25 +1,13 @@ // ANTLR GENERATED CODE: DO NOT EDIT package org.elasticsearch.painless.antlr; - -import org.antlr.v4.runtime.FailedPredicateException; -import org.antlr.v4.runtime.NoViableAltException; -import org.antlr.v4.runtime.Parser; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.RecognitionException; -import org.antlr.v4.runtime.RuleContext; -import org.antlr.v4.runtime.RuntimeMetaData; -import org.antlr.v4.runtime.TokenStream; -import org.antlr.v4.runtime.Vocabulary; -import org.antlr.v4.runtime.VocabularyImpl; -import org.antlr.v4.runtime.atn.ATN; -import org.antlr.v4.runtime.atn.ATNDeserializer; -import org.antlr.v4.runtime.atn.ParserATNSimulator; -import org.antlr.v4.runtime.atn.PredictionContextCache; +import org.antlr.v4.runtime.atn.*; import org.antlr.v4.runtime.dfa.DFA; -import org.antlr.v4.runtime.tree.ParseTreeVisitor; -import org.antlr.v4.runtime.tree.TerminalNode; - +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.misc.*; +import org.antlr.v4.runtime.tree.*; import java.util.List; +import java.util.Iterator; +import java.util.ArrayList; @SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"}) class PainlessParser extends Parser { @@ -29,57 +17,57 @@ class PainlessParser extends Parser { protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); public static final int - WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, - NSDOT=10, COMMA=11, SEMICOLON=12, IF=13, IN=14, ELSE=15, WHILE=16, DO=17, - FOR=18, CONTINUE=19, BREAK=20, RETURN=21, NEW=22, TRY=23, CATCH=24, THROW=25, - THIS=26, INSTANCEOF=27, BOOLNOT=28, BWNOT=29, MUL=30, DIV=31, REM=32, - ADD=33, SUB=34, LSH=35, RSH=36, USH=37, LT=38, LTE=39, GT=40, GTE=41, - EQ=42, EQR=43, NE=44, NER=45, BWAND=46, XOR=47, BWOR=48, BOOLAND=49, BOOLOR=50, - COND=51, COLON=52, ELVIS=53, REF=54, ARROW=55, FIND=56, MATCH=57, INCR=58, - DECR=59, ASSIGN=60, AADD=61, ASUB=62, AMUL=63, ADIV=64, AREM=65, AAND=66, - AXOR=67, AOR=68, ALSH=69, ARSH=70, AUSH=71, OCTAL=72, HEX=73, INTEGER=74, - DECIMAL=75, STRING=76, REGEX=77, TRUE=78, FALSE=79, NULL=80, TYPE=81, + WS=1, COMMENT=2, LBRACK=3, RBRACK=4, LBRACE=5, RBRACE=6, LP=7, RP=8, DOT=9, + NSDOT=10, COMMA=11, SEMICOLON=12, IF=13, IN=14, ELSE=15, WHILE=16, DO=17, + FOR=18, CONTINUE=19, BREAK=20, RETURN=21, NEW=22, TRY=23, CATCH=24, THROW=25, + THIS=26, INSTANCEOF=27, BOOLNOT=28, BWNOT=29, MUL=30, DIV=31, REM=32, + ADD=33, SUB=34, LSH=35, RSH=36, USH=37, LT=38, LTE=39, GT=40, GTE=41, + EQ=42, EQR=43, NE=44, NER=45, BWAND=46, XOR=47, BWOR=48, BOOLAND=49, BOOLOR=50, + COND=51, COLON=52, ELVIS=53, REF=54, ARROW=55, FIND=56, MATCH=57, INCR=58, + DECR=59, ASSIGN=60, AADD=61, ASUB=62, AMUL=63, ADIV=64, AREM=65, AAND=66, + AXOR=67, AOR=68, ALSH=69, ARSH=70, AUSH=71, OCTAL=72, HEX=73, INTEGER=74, + DECIMAL=75, STRING=76, REGEX=77, TRUE=78, FALSE=79, NULL=80, TYPE=81, ID=82, DOTINTEGER=83, DOTID=84; public static final int - RULE_source = 0, RULE_function = 1, RULE_parameters = 2, RULE_statement = 3, - RULE_rstatement = 4, RULE_dstatement = 5, RULE_trailer = 6, RULE_block = 7, - RULE_empty = 8, RULE_initializer = 9, RULE_afterthought = 10, RULE_declaration = 11, - RULE_decltype = 12, RULE_declvar = 13, RULE_trap = 14, RULE_expression = 15, - RULE_unary = 16, RULE_chain = 17, RULE_primary = 18, RULE_postfix = 19, - RULE_postdot = 20, RULE_callinvoke = 21, RULE_fieldaccess = 22, RULE_braceaccess = 23, - RULE_arrayinitializer = 24, RULE_listinitializer = 25, RULE_mapinitializer = 26, - RULE_maptoken = 27, RULE_arguments = 28, RULE_argument = 29, RULE_lambda = 30, + RULE_source = 0, RULE_function = 1, RULE_parameters = 2, RULE_statement = 3, + RULE_rstatement = 4, RULE_dstatement = 5, RULE_trailer = 6, RULE_block = 7, + RULE_empty = 8, RULE_initializer = 9, RULE_afterthought = 10, RULE_declaration = 11, + RULE_decltype = 12, RULE_declvar = 13, RULE_trap = 14, RULE_expression = 15, + RULE_unary = 16, RULE_chain = 17, RULE_primary = 18, RULE_postfix = 19, + RULE_postdot = 20, RULE_callinvoke = 21, RULE_fieldaccess = 22, RULE_braceaccess = 23, + RULE_arrayinitializer = 24, RULE_listinitializer = 25, RULE_mapinitializer = 26, + RULE_maptoken = 27, RULE_arguments = 28, RULE_argument = 29, RULE_lambda = 30, RULE_lamtype = 31, RULE_funcref = 32; public static final String[] ruleNames = { - "source", "function", "parameters", "statement", "rstatement", "dstatement", - "trailer", "block", "empty", "initializer", "afterthought", "declaration", - "decltype", "declvar", "trap", "expression", "unary", "chain", "primary", - "postfix", "postdot", "callinvoke", "fieldaccess", "braceaccess", "arrayinitializer", - "listinitializer", "mapinitializer", "maptoken", "arguments", "argument", + "source", "function", "parameters", "statement", "rstatement", "dstatement", + "trailer", "block", "empty", "initializer", "afterthought", "declaration", + "decltype", "declvar", "trap", "expression", "unary", "chain", "primary", + "postfix", "postdot", "callinvoke", "fieldaccess", "braceaccess", "arrayinitializer", + "listinitializer", "mapinitializer", "maptoken", "arguments", "argument", "lambda", "lamtype", "funcref" }; private static final String[] _LITERAL_NAMES = { - null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "'?.'", - "','", "';'", "'if'", "'in'", "'else'", "'while'", "'do'", "'for'", "'continue'", - "'break'", "'return'", "'new'", "'try'", "'catch'", "'throw'", "'this'", - "'instanceof'", "'!'", "'~'", "'*'", "'/'", "'%'", "'+'", "'-'", "'<<'", - "'>>'", "'>>>'", "'<'", "'<='", "'>'", "'>='", "'=='", "'==='", "'!='", - "'!=='", "'&'", "'^'", "'|'", "'&&'", "'||'", "'?'", "':'", "'?:'", "'::'", - "'->'", "'=~'", "'==~'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", - "'/='", "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, + null, null, null, "'{'", "'}'", "'['", "']'", "'('", "')'", "'.'", "'?.'", + "','", "';'", "'if'", "'in'", "'else'", "'while'", "'do'", "'for'", "'continue'", + "'break'", "'return'", "'new'", "'try'", "'catch'", "'throw'", "'this'", + "'instanceof'", "'!'", "'~'", "'*'", "'/'", "'%'", "'+'", "'-'", "'<<'", + "'>>'", "'>>>'", "'<'", "'<='", "'>'", "'>='", "'=='", "'==='", "'!='", + "'!=='", "'&'", "'^'", "'|'", "'&&'", "'||'", "'?'", "':'", "'?:'", "'::'", + "'->'", "'=~'", "'==~'", "'++'", "'--'", "'='", "'+='", "'-='", "'*='", + "'/='", "'%='", "'&='", "'^='", "'|='", "'<<='", "'>>='", "'>>>='", null, null, null, null, null, null, "'true'", "'false'", "'null'" }; private static final String[] _SYMBOLIC_NAMES = { - null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", - "DOT", "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", "DO", - "FOR", "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", - "THIS", "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", - "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", - "NER", "BWAND", "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", - "REF", "ARROW", "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", - "AMUL", "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", - "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", + null, "WS", "COMMENT", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "LP", "RP", + "DOT", "NSDOT", "COMMA", "SEMICOLON", "IF", "IN", "ELSE", "WHILE", "DO", + "FOR", "CONTINUE", "BREAK", "RETURN", "NEW", "TRY", "CATCH", "THROW", + "THIS", "INSTANCEOF", "BOOLNOT", "BWNOT", "MUL", "DIV", "REM", "ADD", + "SUB", "LSH", "RSH", "USH", "LT", "LTE", "GT", "GTE", "EQ", "EQR", "NE", + "NER", "BWAND", "XOR", "BWOR", "BOOLAND", "BOOLOR", "COND", "COLON", "ELVIS", + "REF", "ARROW", "FIND", "MATCH", "INCR", "DECR", "ASSIGN", "AADD", "ASUB", + "AMUL", "ADIV", "AREM", "AAND", "AXOR", "AOR", "ALSH", "ARSH", "AUSH", + "OCTAL", "HEX", "INTEGER", "DECIMAL", "STRING", "REGEX", "TRUE", "FALSE", "NULL", "TYPE", "ID", "DOTINTEGER", "DOTID" }; public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); @@ -145,9 +133,6 @@ public List statement() { public StatementContext statement(int i) { return getRuleContext(StatementContext.class,i); } - public DstatementContext dstatement() { - return getRuleContext(DstatementContext.class,0); - } public SourceContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @@ -177,7 +162,7 @@ public final SourceContext source() throws RecognitionException { setState(66); function(); } - } + } } setState(71); _errHandler.sync(this); @@ -185,30 +170,19 @@ public final SourceContext source() throws RecognitionException { } setState(75); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,1,_ctx); - while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { - if ( _alt==1 ) { - { - { - setState(72); - statement(); - } - } - } - setState(77); - _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,1,_ctx); - } - setState(79); _la = _input.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << DO) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << IF) | (1L << WHILE) | (1L << DO) | (1L << FOR) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << TRY) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { { - setState(78); - dstatement(); + { + setState(72); + statement(); + } } + setState(77); + _errHandler.sync(this); + _la = _input.LA(1); } - - setState(81); + setState(78); match(EOF); } } @@ -251,13 +225,13 @@ public final FunctionContext function() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(83); + setState(80); decltype(); - setState(84); + setState(81); match(ID); - setState(85); + setState(82); parameters(); - setState(86); + setState(83); block(); } } @@ -307,38 +281,38 @@ public final ParametersContext parameters() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(88); + setState(85); match(LP); - setState(100); + setState(97); _la = _input.LA(1); if (_la==TYPE) { { - setState(89); + setState(86); decltype(); - setState(90); + setState(87); match(ID); - setState(97); + setState(94); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(91); + setState(88); match(COMMA); - setState(92); + setState(89); decltype(); - setState(93); + setState(90); match(ID); } } - setState(99); + setState(96); _errHandler.sync(this); _la = _input.LA(1); } } } - setState(102); + setState(99); match(RP); } } @@ -361,6 +335,7 @@ public DstatementContext dstatement() { return getRuleContext(DstatementContext.class,0); } public TerminalNode SEMICOLON() { return getToken(PainlessParser.SEMICOLON, 0); } + public TerminalNode EOF() { return getToken(PainlessParser.EOF, 0); } public StatementContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @@ -375,8 +350,9 @@ public T accept(ParseTreeVisitor visitor) { public final StatementContext statement() throws RecognitionException { StatementContext _localctx = new StatementContext(_ctx, getState()); enterRule(_localctx, 6, RULE_statement); + int _la; try { - setState(108); + setState(105); switch (_input.LA(1)) { case IF: case WHILE: @@ -384,7 +360,7 @@ public final StatementContext statement() throws RecognitionException { case TRY: enterOuterAlt(_localctx, 1); { - setState(104); + setState(101); rstatement(); } break; @@ -415,10 +391,15 @@ public final StatementContext statement() throws RecognitionException { case ID: enterOuterAlt(_localctx, 2); { - setState(105); + setState(102); dstatement(); - setState(106); - match(SEMICOLON); + setState(103); + _la = _input.LA(1); + if ( !(_la==EOF || _la==SEMICOLON) ) { + _errHandler.recoverInline(this); + } else { + consume(); + } } break; default: @@ -441,7 +422,7 @@ public RstatementContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_rstatement; } - + public RstatementContext() { } public void copyFrom(RstatementContext ctx) { super.copyFrom(ctx); @@ -584,37 +565,37 @@ public final RstatementContext rstatement() throws RecognitionException { int _la; try { int _alt; - setState(170); + setState(167); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,13,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,12,_ctx) ) { case 1: _localctx = new IfContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(110); + setState(107); match(IF); - setState(111); + setState(108); match(LP); - setState(112); + setState(109); expression(0); - setState(113); + setState(110); match(RP); - setState(114); + setState(111); trailer(); - setState(118); + setState(115); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,6,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,5,_ctx) ) { case 1: { - setState(115); + setState(112); match(ELSE); - setState(116); + setState(113); trailer(); } break; case 2: { - setState(117); + setState(114); if (!( _input.LA(1) != ELSE )) throw new FailedPredicateException(this, " _input.LA(1) != ELSE "); } break; @@ -625,15 +606,15 @@ public final RstatementContext rstatement() throws RecognitionException { _localctx = new WhileContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(120); + setState(117); match(WHILE); - setState(121); + setState(118); match(LP); - setState(122); + setState(119); expression(0); - setState(123); + setState(120); match(RP); - setState(126); + setState(123); switch (_input.LA(1)) { case LBRACK: case LBRACE: @@ -666,13 +647,13 @@ public final RstatementContext rstatement() throws RecognitionException { case TYPE: case ID: { - setState(124); + setState(121); trailer(); } break; case SEMICOLON: { - setState(125); + setState(122); empty(); } break; @@ -685,44 +666,44 @@ public final RstatementContext rstatement() throws RecognitionException { _localctx = new ForContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(128); + setState(125); match(FOR); - setState(129); + setState(126); match(LP); - setState(131); + setState(128); _la = _input.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { { - setState(130); + setState(127); initializer(); } } - setState(133); + setState(130); match(SEMICOLON); - setState(135); + setState(132); _la = _input.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { { - setState(134); + setState(131); expression(0); } } - setState(137); + setState(134); match(SEMICOLON); - setState(139); + setState(136); _la = _input.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { { - setState(138); + setState(135); afterthought(); } } - setState(141); + setState(138); match(RP); - setState(144); + setState(141); switch (_input.LA(1)) { case LBRACK: case LBRACE: @@ -755,13 +736,13 @@ public final RstatementContext rstatement() throws RecognitionException { case TYPE: case ID: { - setState(142); + setState(139); trailer(); } break; case SEMICOLON: { - setState(143); + setState(140); empty(); } break; @@ -774,21 +755,21 @@ public final RstatementContext rstatement() throws RecognitionException { _localctx = new EachContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(146); + setState(143); match(FOR); - setState(147); + setState(144); match(LP); - setState(148); + setState(145); decltype(); - setState(149); + setState(146); match(ID); - setState(150); + setState(147); match(COLON); - setState(151); + setState(148); expression(0); - setState(152); + setState(149); match(RP); - setState(153); + setState(150); trailer(); } break; @@ -796,19 +777,19 @@ public final RstatementContext rstatement() throws RecognitionException { _localctx = new IneachContext(_localctx); enterOuterAlt(_localctx, 5); { - setState(155); + setState(152); match(FOR); - setState(156); + setState(153); match(LP); - setState(157); + setState(154); match(ID); - setState(158); + setState(155); match(IN); - setState(159); + setState(156); expression(0); - setState(160); + setState(157); match(RP); - setState(161); + setState(158); trailer(); } break; @@ -816,11 +797,11 @@ public final RstatementContext rstatement() throws RecognitionException { _localctx = new TryContext(_localctx); enterOuterAlt(_localctx, 6); { - setState(163); + setState(160); match(TRY); - setState(164); + setState(161); block(); - setState(166); + setState(163); _errHandler.sync(this); _alt = 1; do { @@ -828,7 +809,7 @@ public final RstatementContext rstatement() throws RecognitionException { case 1: { { - setState(165); + setState(162); trap(); } } @@ -836,9 +817,9 @@ public final RstatementContext rstatement() throws RecognitionException { default: throw new NoViableAltException(this); } - setState(168); + setState(165); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,12,_ctx); + _alt = getInterpreter().adaptivePredict(_input,11,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); } break; @@ -860,7 +841,7 @@ public DstatementContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_dstatement; } - + public DstatementContext() { } public void copyFrom(DstatementContext ctx) { super.copyFrom(ctx); @@ -953,24 +934,24 @@ public final DstatementContext dstatement() throws RecognitionException { DstatementContext _localctx = new DstatementContext(_ctx, getState()); enterRule(_localctx, 10, RULE_dstatement); try { - setState(187); + setState(184); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,14,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,13,_ctx) ) { case 1: _localctx = new DoContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(172); + setState(169); match(DO); - setState(173); + setState(170); block(); - setState(174); + setState(171); match(WHILE); - setState(175); + setState(172); match(LP); - setState(176); + setState(173); expression(0); - setState(177); + setState(174); match(RP); } break; @@ -978,7 +959,7 @@ public final DstatementContext dstatement() throws RecognitionException { _localctx = new DeclContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(179); + setState(176); declaration(); } break; @@ -986,7 +967,7 @@ public final DstatementContext dstatement() throws RecognitionException { _localctx = new ContinueContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(180); + setState(177); match(CONTINUE); } break; @@ -994,7 +975,7 @@ public final DstatementContext dstatement() throws RecognitionException { _localctx = new BreakContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(181); + setState(178); match(BREAK); } break; @@ -1002,9 +983,9 @@ public final DstatementContext dstatement() throws RecognitionException { _localctx = new ReturnContext(_localctx); enterOuterAlt(_localctx, 5); { - setState(182); + setState(179); match(RETURN); - setState(183); + setState(180); expression(0); } break; @@ -1012,9 +993,9 @@ public final DstatementContext dstatement() throws RecognitionException { _localctx = new ThrowContext(_localctx); enterOuterAlt(_localctx, 6); { - setState(184); + setState(181); match(THROW); - setState(185); + setState(182); expression(0); } break; @@ -1022,7 +1003,7 @@ public final DstatementContext dstatement() throws RecognitionException { _localctx = new ExprContext(_localctx); enterOuterAlt(_localctx, 7); { - setState(186); + setState(183); expression(0); } break; @@ -1061,12 +1042,12 @@ public final TrailerContext trailer() throws RecognitionException { TrailerContext _localctx = new TrailerContext(_ctx, getState()); enterRule(_localctx, 12, RULE_trailer); try { - setState(191); + setState(188); switch (_input.LA(1)) { case LBRACK: enterOuterAlt(_localctx, 1); { - setState(189); + setState(186); block(); } break; @@ -1101,7 +1082,7 @@ public final TrailerContext trailer() throws RecognitionException { case ID: enterOuterAlt(_localctx, 2); { - setState(190); + setState(187); statement(); } break; @@ -1151,34 +1132,34 @@ public final BlockContext block() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(193); + setState(190); match(LBRACK); - setState(197); + setState(194); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,16,_ctx); + _alt = getInterpreter().adaptivePredict(_input,15,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(194); + setState(191); statement(); } - } + } } - setState(199); + setState(196); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,16,_ctx); + _alt = getInterpreter().adaptivePredict(_input,15,_ctx); } - setState(201); + setState(198); _la = _input.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << DO) | (1L << CONTINUE) | (1L << BREAK) | (1L << RETURN) | (1L << NEW) | (1L << THROW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { { - setState(200); + setState(197); dstatement(); } } - setState(203); + setState(200); match(RBRACK); } } @@ -1212,7 +1193,7 @@ public final EmptyContext empty() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(205); + setState(202); match(SEMICOLON); } } @@ -1249,20 +1230,20 @@ public final InitializerContext initializer() throws RecognitionException { InitializerContext _localctx = new InitializerContext(_ctx, getState()); enterRule(_localctx, 18, RULE_initializer); try { - setState(209); + setState(206); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,18,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,17,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(207); + setState(204); declaration(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(208); + setState(205); expression(0); } break; @@ -1300,7 +1281,7 @@ public final AfterthoughtContext afterthought() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(211); + setState(208); expression(0); } } @@ -1347,23 +1328,23 @@ public final DeclarationContext declaration() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(213); + setState(210); decltype(); - setState(214); + setState(211); declvar(); - setState(219); + setState(216); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(215); + setState(212); match(COMMA); - setState(216); + setState(213); declvar(); } } - setState(221); + setState(218); _errHandler.sync(this); _la = _input.LA(1); } @@ -1408,25 +1389,25 @@ public final DecltypeContext decltype() throws RecognitionException { int _alt; enterOuterAlt(_localctx, 1); { - setState(222); + setState(219); match(TYPE); - setState(227); + setState(224); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,20,_ctx); + _alt = getInterpreter().adaptivePredict(_input,19,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(223); + setState(220); match(LBRACE); - setState(224); + setState(221); match(RBRACE); } - } + } } - setState(229); + setState(226); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,20,_ctx); + _alt = getInterpreter().adaptivePredict(_input,19,_ctx); } } } @@ -1465,15 +1446,15 @@ public final DeclvarContext declvar() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(230); + setState(227); match(ID); - setState(233); + setState(230); _la = _input.LA(1); if (_la==ASSIGN) { { - setState(231); + setState(228); match(ASSIGN); - setState(232); + setState(229); expression(0); } } @@ -1517,17 +1498,17 @@ public final TrapContext trap() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(235); + setState(232); match(CATCH); - setState(236); + setState(233); match(LP); - setState(237); + setState(234); match(TYPE); - setState(238); + setState(235); match(ID); - setState(239); + setState(236); match(RP); - setState(240); + setState(237); block(); } } @@ -1547,7 +1528,7 @@ public ExpressionContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_expression; } - + public ExpressionContext() { } public void copyFrom(ExpressionContext ctx) { super.copyFrom(ctx); @@ -1723,35 +1704,35 @@ private ExpressionContext expression(int _p) throws RecognitionException { _ctx = _localctx; _prevctx = _localctx; - setState(243); + setState(240); unary(); } _ctx.stop = _input.LT(-1); - setState(295); + setState(292); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,23,_ctx); + _alt = getInterpreter().adaptivePredict(_input,22,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { if ( _parseListeners!=null ) triggerExitRuleEvent(); _prevctx = _localctx; { - setState(293); + setState(290); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,22,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,21,_ctx) ) { case 1: { _localctx = new BinaryContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(245); + setState(242); if (!(precpred(_ctx, 15))) throw new FailedPredicateException(this, "precpred(_ctx, 15)"); - setState(246); + setState(243); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << MUL) | (1L << DIV) | (1L << REM))) != 0)) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(247); + setState(244); expression(16); } break; @@ -1759,16 +1740,16 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BinaryContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(248); + setState(245); if (!(precpred(_ctx, 14))) throw new FailedPredicateException(this, "precpred(_ctx, 14)"); - setState(249); + setState(246); _la = _input.LA(1); if ( !(_la==ADD || _la==SUB) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(250); + setState(247); expression(15); } break; @@ -1776,16 +1757,16 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BinaryContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(251); + setState(248); if (!(precpred(_ctx, 13))) throw new FailedPredicateException(this, "precpred(_ctx, 13)"); - setState(252); + setState(249); _la = _input.LA(1); if ( !(_la==FIND || _la==MATCH) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(253); + setState(250); expression(14); } break; @@ -1793,16 +1774,16 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BinaryContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(254); + setState(251); if (!(precpred(_ctx, 12))) throw new FailedPredicateException(this, "precpred(_ctx, 12)"); - setState(255); + setState(252); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LSH) | (1L << RSH) | (1L << USH))) != 0)) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(256); + setState(253); expression(13); } break; @@ -1810,16 +1791,16 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new CompContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(257); + setState(254); if (!(precpred(_ctx, 11))) throw new FailedPredicateException(this, "precpred(_ctx, 11)"); - setState(258); + setState(255); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LT) | (1L << LTE) | (1L << GT) | (1L << GTE))) != 0)) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(259); + setState(256); expression(12); } break; @@ -1827,16 +1808,16 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new CompContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(260); + setState(257); if (!(precpred(_ctx, 9))) throw new FailedPredicateException(this, "precpred(_ctx, 9)"); - setState(261); + setState(258); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << EQ) | (1L << EQR) | (1L << NE) | (1L << NER))) != 0)) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(262); + setState(259); expression(10); } break; @@ -1844,11 +1825,11 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BinaryContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(263); + setState(260); if (!(precpred(_ctx, 8))) throw new FailedPredicateException(this, "precpred(_ctx, 8)"); - setState(264); + setState(261); match(BWAND); - setState(265); + setState(262); expression(9); } break; @@ -1856,11 +1837,11 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BinaryContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(266); + setState(263); if (!(precpred(_ctx, 7))) throw new FailedPredicateException(this, "precpred(_ctx, 7)"); - setState(267); + setState(264); match(XOR); - setState(268); + setState(265); expression(8); } break; @@ -1868,11 +1849,11 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BinaryContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(269); + setState(266); if (!(precpred(_ctx, 6))) throw new FailedPredicateException(this, "precpred(_ctx, 6)"); - setState(270); + setState(267); match(BWOR); - setState(271); + setState(268); expression(7); } break; @@ -1880,11 +1861,11 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BoolContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(272); + setState(269); if (!(precpred(_ctx, 5))) throw new FailedPredicateException(this, "precpred(_ctx, 5)"); - setState(273); + setState(270); match(BOOLAND); - setState(274); + setState(271); expression(6); } break; @@ -1892,11 +1873,11 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new BoolContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(275); + setState(272); if (!(precpred(_ctx, 4))) throw new FailedPredicateException(this, "precpred(_ctx, 4)"); - setState(276); + setState(273); match(BOOLOR); - setState(277); + setState(274); expression(5); } break; @@ -1904,15 +1885,15 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new ConditionalContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(278); + setState(275); if (!(precpred(_ctx, 3))) throw new FailedPredicateException(this, "precpred(_ctx, 3)"); - setState(279); + setState(276); match(COND); - setState(280); + setState(277); expression(0); - setState(281); + setState(278); match(COLON); - setState(282); + setState(279); expression(3); } break; @@ -1920,11 +1901,11 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new ElvisContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(284); + setState(281); if (!(precpred(_ctx, 2))) throw new FailedPredicateException(this, "precpred(_ctx, 2)"); - setState(285); + setState(282); match(ELVIS); - setState(286); + setState(283); expression(2); } break; @@ -1932,16 +1913,16 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new AssignmentContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(287); + setState(284); if (!(precpred(_ctx, 1))) throw new FailedPredicateException(this, "precpred(_ctx, 1)"); - setState(288); + setState(285); _la = _input.LA(1); if ( !(((((_la - 60)) & ~0x3f) == 0 && ((1L << (_la - 60)) & ((1L << (ASSIGN - 60)) | (1L << (AADD - 60)) | (1L << (ASUB - 60)) | (1L << (AMUL - 60)) | (1L << (ADIV - 60)) | (1L << (AREM - 60)) | (1L << (AAND - 60)) | (1L << (AXOR - 60)) | (1L << (AOR - 60)) | (1L << (ALSH - 60)) | (1L << (ARSH - 60)) | (1L << (AUSH - 60)))) != 0)) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(289); + setState(286); expression(1); } break; @@ -1949,20 +1930,20 @@ private ExpressionContext expression(int _p) throws RecognitionException { { _localctx = new InstanceofContext(new ExpressionContext(_parentctx, _parentState)); pushNewRecursionContext(_localctx, _startState, RULE_expression); - setState(290); + setState(287); if (!(precpred(_ctx, 10))) throw new FailedPredicateException(this, "precpred(_ctx, 10)"); - setState(291); + setState(288); match(INSTANCEOF); - setState(292); + setState(289); decltype(); } break; } - } + } } - setState(297); + setState(294); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,23,_ctx); + _alt = getInterpreter().adaptivePredict(_input,22,_ctx); } } } @@ -1982,7 +1963,7 @@ public UnaryContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_unary; } - + public UnaryContext() { } public void copyFrom(UnaryContext ctx) { super.copyFrom(ctx); @@ -2062,21 +2043,21 @@ public final UnaryContext unary() throws RecognitionException { enterRule(_localctx, 32, RULE_unary); int _la; try { - setState(311); + setState(308); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,24,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,23,_ctx) ) { case 1: _localctx = new PreContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(298); + setState(295); _la = _input.LA(1); if ( !(_la==INCR || _la==DECR) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(299); + setState(296); chain(); } break; @@ -2084,9 +2065,9 @@ public final UnaryContext unary() throws RecognitionException { _localctx = new PostContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(300); + setState(297); chain(); - setState(301); + setState(298); _la = _input.LA(1); if ( !(_la==INCR || _la==DECR) ) { _errHandler.recoverInline(this); @@ -2099,7 +2080,7 @@ public final UnaryContext unary() throws RecognitionException { _localctx = new ReadContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(303); + setState(300); chain(); } break; @@ -2107,14 +2088,14 @@ public final UnaryContext unary() throws RecognitionException { _localctx = new OperatorContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(304); + setState(301); _la = _input.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB))) != 0)) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(305); + setState(302); unary(); } break; @@ -2122,13 +2103,13 @@ public final UnaryContext unary() throws RecognitionException { _localctx = new CastContext(_localctx); enterOuterAlt(_localctx, 5); { - setState(306); + setState(303); match(LP); - setState(307); + setState(304); decltype(); - setState(308); + setState(305); match(RP); - setState(309); + setState(306); unary(); } break; @@ -2150,7 +2131,7 @@ public ChainContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_chain; } - + public ChainContext() { } public void copyFrom(ChainContext ctx) { super.copyFrom(ctx); @@ -2210,30 +2191,30 @@ public final ChainContext chain() throws RecognitionException { enterRule(_localctx, 34, RULE_chain); try { int _alt; - setState(329); + setState(326); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,27,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,26,_ctx) ) { case 1: _localctx = new DynamicContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(313); + setState(310); primary(); - setState(317); + setState(314); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,25,_ctx); + _alt = getInterpreter().adaptivePredict(_input,24,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(314); + setState(311); postfix(); } - } + } } - setState(319); + setState(316); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,25,_ctx); + _alt = getInterpreter().adaptivePredict(_input,24,_ctx); } } break; @@ -2241,25 +2222,25 @@ public final ChainContext chain() throws RecognitionException { _localctx = new StaticContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(320); + setState(317); decltype(); - setState(321); + setState(318); postdot(); - setState(325); + setState(322); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,26,_ctx); + _alt = getInterpreter().adaptivePredict(_input,25,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(322); + setState(319); postfix(); } - } + } } - setState(327); + setState(324); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,26,_ctx); + _alt = getInterpreter().adaptivePredict(_input,25,_ctx); } } break; @@ -2267,7 +2248,7 @@ public final ChainContext chain() throws RecognitionException { _localctx = new NewarrayContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(328); + setState(325); arrayinitializer(); } break; @@ -2289,7 +2270,7 @@ public PrimaryContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_primary; } - + public PrimaryContext() { } public void copyFrom(PrimaryContext ctx) { super.copyFrom(ctx); @@ -2427,18 +2408,18 @@ public final PrimaryContext primary() throws RecognitionException { enterRule(_localctx, 36, RULE_primary); int _la; try { - setState(349); + setState(346); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,28,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,27,_ctx) ) { case 1: _localctx = new PrecedenceContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(331); + setState(328); match(LP); - setState(332); + setState(329); expression(0); - setState(333); + setState(330); match(RP); } break; @@ -2446,7 +2427,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new NumericContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(335); + setState(332); _la = _input.LA(1); if ( !(((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)))) != 0)) ) { _errHandler.recoverInline(this); @@ -2459,7 +2440,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new TrueContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(336); + setState(333); match(TRUE); } break; @@ -2467,7 +2448,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new FalseContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(337); + setState(334); match(FALSE); } break; @@ -2475,7 +2456,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new NullContext(_localctx); enterOuterAlt(_localctx, 5); { - setState(338); + setState(335); match(NULL); } break; @@ -2483,7 +2464,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new StringContext(_localctx); enterOuterAlt(_localctx, 6); { - setState(339); + setState(336); match(STRING); } break; @@ -2491,7 +2472,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new RegexContext(_localctx); enterOuterAlt(_localctx, 7); { - setState(340); + setState(337); match(REGEX); } break; @@ -2499,7 +2480,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new ListinitContext(_localctx); enterOuterAlt(_localctx, 8); { - setState(341); + setState(338); listinitializer(); } break; @@ -2507,7 +2488,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new MapinitContext(_localctx); enterOuterAlt(_localctx, 9); { - setState(342); + setState(339); mapinitializer(); } break; @@ -2515,7 +2496,7 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new VariableContext(_localctx); enterOuterAlt(_localctx, 10); { - setState(343); + setState(340); match(ID); } break; @@ -2523,9 +2504,9 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new CalllocalContext(_localctx); enterOuterAlt(_localctx, 11); { - setState(344); + setState(341); match(ID); - setState(345); + setState(342); arguments(); } break; @@ -2533,11 +2514,11 @@ public final PrimaryContext primary() throws RecognitionException { _localctx = new NewobjectContext(_localctx); enterOuterAlt(_localctx, 12); { - setState(346); + setState(343); match(NEW); - setState(347); + setState(344); match(TYPE); - setState(348); + setState(345); arguments(); } break; @@ -2579,27 +2560,27 @@ public final PostfixContext postfix() throws RecognitionException { PostfixContext _localctx = new PostfixContext(_ctx, getState()); enterRule(_localctx, 38, RULE_postfix); try { - setState(354); + setState(351); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,29,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,28,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(351); + setState(348); callinvoke(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(352); + setState(349); fieldaccess(); } break; case 3: enterOuterAlt(_localctx, 3); { - setState(353); + setState(350); braceaccess(); } break; @@ -2638,20 +2619,20 @@ public final PostdotContext postdot() throws RecognitionException { PostdotContext _localctx = new PostdotContext(_ctx, getState()); enterRule(_localctx, 40, RULE_postdot); try { - setState(358); + setState(355); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,30,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,29,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(356); + setState(353); callinvoke(); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(357); + setState(354); fieldaccess(); } break; @@ -2693,16 +2674,16 @@ public final CallinvokeContext callinvoke() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(360); + setState(357); _la = _input.LA(1); if ( !(_la==DOT || _la==NSDOT) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(361); + setState(358); match(DOTID); - setState(362); + setState(359); arguments(); } } @@ -2740,14 +2721,14 @@ public final FieldaccessContext fieldaccess() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(364); + setState(361); _la = _input.LA(1); if ( !(_la==DOT || _la==NSDOT) ) { _errHandler.recoverInline(this); } else { consume(); } - setState(365); + setState(362); _la = _input.LA(1); if ( !(_la==DOTINTEGER || _la==DOTID) ) { _errHandler.recoverInline(this); @@ -2790,11 +2771,11 @@ public final BraceaccessContext braceaccess() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(367); + setState(364); match(LBRACE); - setState(368); + setState(365); expression(0); - setState(369); + setState(366); match(RBRACE); } } @@ -2814,7 +2795,7 @@ public ArrayinitializerContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_arrayinitializer; } - + public ArrayinitializerContext() { } public void copyFrom(ArrayinitializerContext ctx) { super.copyFrom(ctx); @@ -2890,18 +2871,18 @@ public final ArrayinitializerContext arrayinitializer() throws RecognitionExcept int _la; try { int _alt; - setState(412); + setState(409); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,37,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,36,_ctx) ) { case 1: _localctx = new NewstandardarrayContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(371); + setState(368); match(NEW); - setState(372); + setState(369); match(TYPE); - setState(377); + setState(374); _errHandler.sync(this); _alt = 1; do { @@ -2909,11 +2890,11 @@ public final ArrayinitializerContext arrayinitializer() throws RecognitionExcept case 1: { { - setState(373); + setState(370); match(LBRACE); - setState(374); + setState(371); expression(0); - setState(375); + setState(372); match(RBRACE); } } @@ -2921,32 +2902,32 @@ public final ArrayinitializerContext arrayinitializer() throws RecognitionExcept default: throw new NoViableAltException(this); } - setState(379); + setState(376); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,31,_ctx); + _alt = getInterpreter().adaptivePredict(_input,30,_ctx); } while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ); - setState(388); + setState(385); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,33,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,32,_ctx) ) { case 1: { - setState(381); + setState(378); postdot(); - setState(385); + setState(382); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,32,_ctx); + _alt = getInterpreter().adaptivePredict(_input,31,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(382); + setState(379); postfix(); } - } + } } - setState(387); + setState(384); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,32,_ctx); + _alt = getInterpreter().adaptivePredict(_input,31,_ctx); } } break; @@ -2957,58 +2938,58 @@ public final ArrayinitializerContext arrayinitializer() throws RecognitionExcept _localctx = new NewinitializedarrayContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(390); + setState(387); match(NEW); - setState(391); + setState(388); match(TYPE); - setState(392); + setState(389); match(LBRACE); - setState(393); + setState(390); match(RBRACE); - setState(394); + setState(391); match(LBRACK); - setState(403); + setState(400); _la = _input.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << NEW) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { { - setState(395); + setState(392); expression(0); - setState(400); + setState(397); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(396); + setState(393); match(COMMA); - setState(397); + setState(394); expression(0); } } - setState(402); + setState(399); _errHandler.sync(this); _la = _input.LA(1); } } } - setState(405); + setState(402); match(RBRACK); - setState(409); + setState(406); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,36,_ctx); + _alt = getInterpreter().adaptivePredict(_input,35,_ctx); while ( _alt!=2 && _alt!=org.antlr.v4.runtime.atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - setState(406); + setState(403); postfix(); } - } + } } - setState(411); + setState(408); _errHandler.sync(this); - _alt = getInterpreter().adaptivePredict(_input,36,_ctx); + _alt = getInterpreter().adaptivePredict(_input,35,_ctx); } } break; @@ -3054,42 +3035,42 @@ public final ListinitializerContext listinitializer() throws RecognitionExceptio enterRule(_localctx, 50, RULE_listinitializer); int _la; try { - setState(427); + setState(424); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,39,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,38,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(414); + setState(411); match(LBRACE); - setState(415); + setState(412); expression(0); - setState(420); + setState(417); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(416); + setState(413); match(COMMA); - setState(417); + setState(414); expression(0); } } - setState(422); + setState(419); _errHandler.sync(this); _la = _input.LA(1); } - setState(423); + setState(420); match(RBRACE); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(425); + setState(422); match(LBRACE); - setState(426); + setState(423); match(RBRACE); } break; @@ -3136,44 +3117,44 @@ public final MapinitializerContext mapinitializer() throws RecognitionException enterRule(_localctx, 52, RULE_mapinitializer); int _la; try { - setState(443); + setState(440); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,41,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,40,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(429); + setState(426); match(LBRACE); - setState(430); + setState(427); maptoken(); - setState(435); + setState(432); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(431); + setState(428); match(COMMA); - setState(432); + setState(429); maptoken(); } } - setState(437); + setState(434); _errHandler.sync(this); _la = _input.LA(1); } - setState(438); + setState(435); match(RBRACE); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(440); + setState(437); match(LBRACE); - setState(441); + setState(438); match(COLON); - setState(442); + setState(439); match(RBRACE); } break; @@ -3215,11 +3196,11 @@ public final MaptokenContext maptoken() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(445); + setState(442); expression(0); - setState(446); + setState(443); match(COLON); - setState(447); + setState(444); expression(0); } } @@ -3266,34 +3247,34 @@ public final ArgumentsContext arguments() throws RecognitionException { enterOuterAlt(_localctx, 1); { { - setState(449); + setState(446); match(LP); - setState(458); + setState(455); _la = _input.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << LBRACE) | (1L << LP) | (1L << NEW) | (1L << THIS) | (1L << BOOLNOT) | (1L << BWNOT) | (1L << ADD) | (1L << SUB) | (1L << INCR) | (1L << DECR))) != 0) || ((((_la - 72)) & ~0x3f) == 0 && ((1L << (_la - 72)) & ((1L << (OCTAL - 72)) | (1L << (HEX - 72)) | (1L << (INTEGER - 72)) | (1L << (DECIMAL - 72)) | (1L << (STRING - 72)) | (1L << (REGEX - 72)) | (1L << (TRUE - 72)) | (1L << (FALSE - 72)) | (1L << (NULL - 72)) | (1L << (TYPE - 72)) | (1L << (ID - 72)))) != 0)) { { - setState(450); + setState(447); argument(); - setState(455); + setState(452); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(451); + setState(448); match(COMMA); - setState(452); + setState(449); argument(); } } - setState(457); + setState(454); _errHandler.sync(this); _la = _input.LA(1); } } } - setState(460); + setState(457); match(RP); } } @@ -3334,27 +3315,27 @@ public final ArgumentContext argument() throws RecognitionException { ArgumentContext _localctx = new ArgumentContext(_ctx, getState()); enterRule(_localctx, 58, RULE_argument); try { - setState(465); + setState(462); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,44,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,43,_ctx) ) { case 1: enterOuterAlt(_localctx, 1); { - setState(462); + setState(459); expression(0); } break; case 2: enterOuterAlt(_localctx, 2); { - setState(463); + setState(460); lambda(); } break; case 3: enterOuterAlt(_localctx, 3); { - setState(464); + setState(461); funcref(); } break; @@ -3409,58 +3390,58 @@ public final LambdaContext lambda() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(480); + setState(477); switch (_input.LA(1)) { case TYPE: case ID: { - setState(467); + setState(464); lamtype(); } break; case LP: { - setState(468); + setState(465); match(LP); - setState(477); + setState(474); _la = _input.LA(1); if (_la==TYPE || _la==ID) { { - setState(469); + setState(466); lamtype(); - setState(474); + setState(471); _errHandler.sync(this); _la = _input.LA(1); while (_la==COMMA) { { { - setState(470); + setState(467); match(COMMA); - setState(471); + setState(468); lamtype(); } } - setState(476); + setState(473); _errHandler.sync(this); _la = _input.LA(1); } } } - setState(479); + setState(476); match(RP); } break; default: throw new NoViableAltException(this); } - setState(482); + setState(479); match(ARROW); - setState(485); + setState(482); switch (_input.LA(1)) { case LBRACK: { - setState(483); + setState(480); block(); } break; @@ -3485,7 +3466,7 @@ public final LambdaContext lambda() throws RecognitionException { case TYPE: case ID: { - setState(484); + setState(481); expression(0); } break; @@ -3528,16 +3509,16 @@ public final LamtypeContext lamtype() throws RecognitionException { try { enterOuterAlt(_localctx, 1); { - setState(488); + setState(485); _la = _input.LA(1); if (_la==TYPE) { { - setState(487); + setState(484); decltype(); } } - setState(490); + setState(487); match(ID); } } @@ -3557,7 +3538,7 @@ public FuncrefContext(ParserRuleContext parent, int invokingState) { super(parent, invokingState); } @Override public int getRuleIndex() { return RULE_funcref; } - + public FuncrefContext() { } public void copyFrom(FuncrefContext ctx) { super.copyFrom(ctx); @@ -3616,18 +3597,18 @@ public final FuncrefContext funcref() throws RecognitionException { FuncrefContext _localctx = new FuncrefContext(_ctx, getState()); enterRule(_localctx, 64, RULE_funcref); try { - setState(505); + setState(502); _errHandler.sync(this); - switch ( getInterpreter().adaptivePredict(_input,50,_ctx) ) { + switch ( getInterpreter().adaptivePredict(_input,49,_ctx) ) { case 1: _localctx = new ClassfuncrefContext(_localctx); enterOuterAlt(_localctx, 1); { - setState(492); + setState(489); match(TYPE); - setState(493); + setState(490); match(REF); - setState(494); + setState(491); match(ID); } break; @@ -3635,11 +3616,11 @@ public final FuncrefContext funcref() throws RecognitionException { _localctx = new ConstructorfuncrefContext(_localctx); enterOuterAlt(_localctx, 2); { - setState(495); + setState(492); decltype(); - setState(496); + setState(493); match(REF); - setState(497); + setState(494); match(NEW); } break; @@ -3647,11 +3628,11 @@ public final FuncrefContext funcref() throws RecognitionException { _localctx = new CapturingfuncrefContext(_localctx); enterOuterAlt(_localctx, 3); { - setState(499); + setState(496); match(ID); - setState(500); + setState(497); match(REF); - setState(501); + setState(498); match(ID); } break; @@ -3659,11 +3640,11 @@ public final FuncrefContext funcref() throws RecognitionException { _localctx = new LocalfuncrefContext(_localctx); enterOuterAlt(_localctx, 4); { - setState(502); + setState(499); match(THIS); - setState(503); + setState(500); match(REF); - setState(504); + setState(501); match(ID); } break; @@ -3733,200 +3714,198 @@ private boolean expression_sempred(ExpressionContext _localctx, int predIndex) { } public static final String _serializedATN = - "\3\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd\3V\u01fe\4\2\t\2\4"+ + "\3\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd\3V\u01fb\4\2\t\2\4"+ "\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t\7\4\b\t\b\4\t\t\t\4\n\t\n\4\13\t"+ "\13\4\f\t\f\4\r\t\r\4\16\t\16\4\17\t\17\4\20\t\20\4\21\t\21\4\22\t\22"+ "\4\23\t\23\4\24\t\24\4\25\t\25\4\26\t\26\4\27\t\27\4\30\t\30\4\31\t\31"+ "\4\32\t\32\4\33\t\33\4\34\t\34\4\35\t\35\4\36\t\36\4\37\t\37\4 \t \4!"+ "\t!\4\"\t\"\3\2\7\2F\n\2\f\2\16\2I\13\2\3\2\7\2L\n\2\f\2\16\2O\13\2\3"+ - "\2\5\2R\n\2\3\2\3\2\3\3\3\3\3\3\3\3\3\3\3\4\3\4\3\4\3\4\3\4\3\4\3\4\7"+ - "\4b\n\4\f\4\16\4e\13\4\5\4g\n\4\3\4\3\4\3\5\3\5\3\5\3\5\5\5o\n\5\3\6\3"+ - "\6\3\6\3\6\3\6\3\6\3\6\3\6\5\6y\n\6\3\6\3\6\3\6\3\6\3\6\3\6\5\6\u0081"+ - "\n\6\3\6\3\6\3\6\5\6\u0086\n\6\3\6\3\6\5\6\u008a\n\6\3\6\3\6\5\6\u008e"+ - "\n\6\3\6\3\6\3\6\5\6\u0093\n\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6"+ - "\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\6\6\u00a9\n\6\r\6\16\6\u00aa"+ - "\5\6\u00ad\n\6\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7"+ - "\3\7\5\7\u00be\n\7\3\b\3\b\5\b\u00c2\n\b\3\t\3\t\7\t\u00c6\n\t\f\t\16"+ - "\t\u00c9\13\t\3\t\5\t\u00cc\n\t\3\t\3\t\3\n\3\n\3\13\3\13\5\13\u00d4\n"+ - "\13\3\f\3\f\3\r\3\r\3\r\3\r\7\r\u00dc\n\r\f\r\16\r\u00df\13\r\3\16\3\16"+ - "\3\16\7\16\u00e4\n\16\f\16\16\16\u00e7\13\16\3\17\3\17\3\17\5\17\u00ec"+ - "\n\17\3\20\3\20\3\20\3\20\3\20\3\20\3\20\3\21\3\21\3\21\3\21\3\21\3\21"+ - "\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21"+ + "\2\3\2\3\3\3\3\3\3\3\3\3\3\3\4\3\4\3\4\3\4\3\4\3\4\3\4\7\4_\n\4\f\4\16"+ + "\4b\13\4\5\4d\n\4\3\4\3\4\3\5\3\5\3\5\3\5\5\5l\n\5\3\6\3\6\3\6\3\6\3\6"+ + "\3\6\3\6\3\6\5\6v\n\6\3\6\3\6\3\6\3\6\3\6\3\6\5\6~\n\6\3\6\3\6\3\6\5\6"+ + "\u0083\n\6\3\6\3\6\5\6\u0087\n\6\3\6\3\6\5\6\u008b\n\6\3\6\3\6\3\6\5\6"+ + "\u0090\n\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6"+ + "\3\6\3\6\3\6\3\6\3\6\6\6\u00a6\n\6\r\6\16\6\u00a7\5\6\u00aa\n\6\3\7\3"+ + "\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\3\7\5\7\u00bb\n\7\3"+ + "\b\3\b\5\b\u00bf\n\b\3\t\3\t\7\t\u00c3\n\t\f\t\16\t\u00c6\13\t\3\t\5\t"+ + "\u00c9\n\t\3\t\3\t\3\n\3\n\3\13\3\13\5\13\u00d1\n\13\3\f\3\f\3\r\3\r\3"+ + "\r\3\r\7\r\u00d9\n\r\f\r\16\r\u00dc\13\r\3\16\3\16\3\16\7\16\u00e1\n\16"+ + "\f\16\16\16\u00e4\13\16\3\17\3\17\3\17\5\17\u00e9\n\17\3\20\3\20\3\20"+ + "\3\20\3\20\3\20\3\20\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21"+ "\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21"+ "\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21"+ - "\3\21\3\21\3\21\7\21\u0128\n\21\f\21\16\21\u012b\13\21\3\22\3\22\3\22"+ - "\3\22\3\22\3\22\3\22\3\22\3\22\3\22\3\22\3\22\3\22\5\22\u013a\n\22\3\23"+ - "\3\23\7\23\u013e\n\23\f\23\16\23\u0141\13\23\3\23\3\23\3\23\7\23\u0146"+ - "\n\23\f\23\16\23\u0149\13\23\3\23\5\23\u014c\n\23\3\24\3\24\3\24\3\24"+ - "\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24"+ - "\5\24\u0160\n\24\3\25\3\25\3\25\5\25\u0165\n\25\3\26\3\26\5\26\u0169\n"+ - "\26\3\27\3\27\3\27\3\27\3\30\3\30\3\30\3\31\3\31\3\31\3\31\3\32\3\32\3"+ - "\32\3\32\3\32\3\32\6\32\u017c\n\32\r\32\16\32\u017d\3\32\3\32\7\32\u0182"+ - "\n\32\f\32\16\32\u0185\13\32\5\32\u0187\n\32\3\32\3\32\3\32\3\32\3\32"+ - "\3\32\3\32\3\32\7\32\u0191\n\32\f\32\16\32\u0194\13\32\5\32\u0196\n\32"+ - "\3\32\3\32\7\32\u019a\n\32\f\32\16\32\u019d\13\32\5\32\u019f\n\32\3\33"+ - "\3\33\3\33\3\33\7\33\u01a5\n\33\f\33\16\33\u01a8\13\33\3\33\3\33\3\33"+ - "\3\33\5\33\u01ae\n\33\3\34\3\34\3\34\3\34\7\34\u01b4\n\34\f\34\16\34\u01b7"+ - "\13\34\3\34\3\34\3\34\3\34\3\34\5\34\u01be\n\34\3\35\3\35\3\35\3\35\3"+ - "\36\3\36\3\36\3\36\7\36\u01c8\n\36\f\36\16\36\u01cb\13\36\5\36\u01cd\n"+ - "\36\3\36\3\36\3\37\3\37\3\37\5\37\u01d4\n\37\3 \3 \3 \3 \3 \7 \u01db\n"+ - " \f \16 \u01de\13 \5 \u01e0\n \3 \5 \u01e3\n \3 \3 \3 \5 \u01e8\n \3!"+ - "\5!\u01eb\n!\3!\3!\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\""+ - "\5\"\u01fc\n\"\3\"\2\3 #\2\4\6\b\n\f\16\20\22\24\26\30\32\34\36 \"$&("+ - "*,.\60\62\64\668:<>@B\2\16\3\2 \"\3\2#$\3\2:;\3\2%\'\3\2(+\3\2,/\3\2>"+ - "I\3\2<=\4\2\36\37#$\3\2JM\3\2\13\f\3\2UV\u0237\2G\3\2\2\2\4U\3\2\2\2\6"+ - "Z\3\2\2\2\bn\3\2\2\2\n\u00ac\3\2\2\2\f\u00bd\3\2\2\2\16\u00c1\3\2\2\2"+ - "\20\u00c3\3\2\2\2\22\u00cf\3\2\2\2\24\u00d3\3\2\2\2\26\u00d5\3\2\2\2\30"+ - "\u00d7\3\2\2\2\32\u00e0\3\2\2\2\34\u00e8\3\2\2\2\36\u00ed\3\2\2\2 \u00f4"+ - "\3\2\2\2\"\u0139\3\2\2\2$\u014b\3\2\2\2&\u015f\3\2\2\2(\u0164\3\2\2\2"+ - "*\u0168\3\2\2\2,\u016a\3\2\2\2.\u016e\3\2\2\2\60\u0171\3\2\2\2\62\u019e"+ - "\3\2\2\2\64\u01ad\3\2\2\2\66\u01bd\3\2\2\28\u01bf\3\2\2\2:\u01c3\3\2\2"+ - "\2<\u01d3\3\2\2\2>\u01e2\3\2\2\2@\u01ea\3\2\2\2B\u01fb\3\2\2\2DF\5\4\3"+ - "\2ED\3\2\2\2FI\3\2\2\2GE\3\2\2\2GH\3\2\2\2HM\3\2\2\2IG\3\2\2\2JL\5\b\5"+ - "\2KJ\3\2\2\2LO\3\2\2\2MK\3\2\2\2MN\3\2\2\2NQ\3\2\2\2OM\3\2\2\2PR\5\f\7"+ - "\2QP\3\2\2\2QR\3\2\2\2RS\3\2\2\2ST\7\2\2\3T\3\3\2\2\2UV\5\32\16\2VW\7"+ - "T\2\2WX\5\6\4\2XY\5\20\t\2Y\5\3\2\2\2Zf\7\t\2\2[\\\5\32\16\2\\c\7T\2\2"+ - "]^\7\r\2\2^_\5\32\16\2_`\7T\2\2`b\3\2\2\2a]\3\2\2\2be\3\2\2\2ca\3\2\2"+ - "\2cd\3\2\2\2dg\3\2\2\2ec\3\2\2\2f[\3\2\2\2fg\3\2\2\2gh\3\2\2\2hi\7\n\2"+ - "\2i\7\3\2\2\2jo\5\n\6\2kl\5\f\7\2lm\7\16\2\2mo\3\2\2\2nj\3\2\2\2nk\3\2"+ - "\2\2o\t\3\2\2\2pq\7\17\2\2qr\7\t\2\2rs\5 \21\2st\7\n\2\2tx\5\16\b\2uv"+ - "\7\21\2\2vy\5\16\b\2wy\6\6\2\2xu\3\2\2\2xw\3\2\2\2y\u00ad\3\2\2\2z{\7"+ - "\22\2\2{|\7\t\2\2|}\5 \21\2}\u0080\7\n\2\2~\u0081\5\16\b\2\177\u0081\5"+ - "\22\n\2\u0080~\3\2\2\2\u0080\177\3\2\2\2\u0081\u00ad\3\2\2\2\u0082\u0083"+ - "\7\24\2\2\u0083\u0085\7\t\2\2\u0084\u0086\5\24\13\2\u0085\u0084\3\2\2"+ - "\2\u0085\u0086\3\2\2\2\u0086\u0087\3\2\2\2\u0087\u0089\7\16\2\2\u0088"+ - "\u008a\5 \21\2\u0089\u0088\3\2\2\2\u0089\u008a\3\2\2\2\u008a\u008b\3\2"+ - "\2\2\u008b\u008d\7\16\2\2\u008c\u008e\5\26\f\2\u008d\u008c\3\2\2\2\u008d"+ - "\u008e\3\2\2\2\u008e\u008f\3\2\2\2\u008f\u0092\7\n\2\2\u0090\u0093\5\16"+ - "\b\2\u0091\u0093\5\22\n\2\u0092\u0090\3\2\2\2\u0092\u0091\3\2\2\2\u0093"+ - "\u00ad\3\2\2\2\u0094\u0095\7\24\2\2\u0095\u0096\7\t\2\2\u0096\u0097\5"+ - "\32\16\2\u0097\u0098\7T\2\2\u0098\u0099\7\66\2\2\u0099\u009a\5 \21\2\u009a"+ - "\u009b\7\n\2\2\u009b\u009c\5\16\b\2\u009c\u00ad\3\2\2\2\u009d\u009e\7"+ - "\24\2\2\u009e\u009f\7\t\2\2\u009f\u00a0\7T\2\2\u00a0\u00a1\7\20\2\2\u00a1"+ - "\u00a2\5 \21\2\u00a2\u00a3\7\n\2\2\u00a3\u00a4\5\16\b\2\u00a4\u00ad\3"+ - "\2\2\2\u00a5\u00a6\7\31\2\2\u00a6\u00a8\5\20\t\2\u00a7\u00a9\5\36\20\2"+ - "\u00a8\u00a7\3\2\2\2\u00a9\u00aa\3\2\2\2\u00aa\u00a8\3\2\2\2\u00aa\u00ab"+ - "\3\2\2\2\u00ab\u00ad\3\2\2\2\u00acp\3\2\2\2\u00acz\3\2\2\2\u00ac\u0082"+ - "\3\2\2\2\u00ac\u0094\3\2\2\2\u00ac\u009d\3\2\2\2\u00ac\u00a5\3\2\2\2\u00ad"+ - "\13\3\2\2\2\u00ae\u00af\7\23\2\2\u00af\u00b0\5\20\t\2\u00b0\u00b1\7\22"+ - "\2\2\u00b1\u00b2\7\t\2\2\u00b2\u00b3\5 \21\2\u00b3\u00b4\7\n\2\2\u00b4"+ - "\u00be\3\2\2\2\u00b5\u00be\5\30\r\2\u00b6\u00be\7\25\2\2\u00b7\u00be\7"+ - "\26\2\2\u00b8\u00b9\7\27\2\2\u00b9\u00be\5 \21\2\u00ba\u00bb\7\33\2\2"+ - "\u00bb\u00be\5 \21\2\u00bc\u00be\5 \21\2\u00bd\u00ae\3\2\2\2\u00bd\u00b5"+ - "\3\2\2\2\u00bd\u00b6\3\2\2\2\u00bd\u00b7\3\2\2\2\u00bd\u00b8\3\2\2\2\u00bd"+ - "\u00ba\3\2\2\2\u00bd\u00bc\3\2\2\2\u00be\r\3\2\2\2\u00bf\u00c2\5\20\t"+ - "\2\u00c0\u00c2\5\b\5\2\u00c1\u00bf\3\2\2\2\u00c1\u00c0\3\2\2\2\u00c2\17"+ - "\3\2\2\2\u00c3\u00c7\7\5\2\2\u00c4\u00c6\5\b\5\2\u00c5\u00c4\3\2\2\2\u00c6"+ - "\u00c9\3\2\2\2\u00c7\u00c5\3\2\2\2\u00c7\u00c8\3\2\2\2\u00c8\u00cb\3\2"+ - "\2\2\u00c9\u00c7\3\2\2\2\u00ca\u00cc\5\f\7\2\u00cb\u00ca\3\2\2\2\u00cb"+ - "\u00cc\3\2\2\2\u00cc\u00cd\3\2\2\2\u00cd\u00ce\7\6\2\2\u00ce\21\3\2\2"+ - "\2\u00cf\u00d0\7\16\2\2\u00d0\23\3\2\2\2\u00d1\u00d4\5\30\r\2\u00d2\u00d4"+ - "\5 \21\2\u00d3\u00d1\3\2\2\2\u00d3\u00d2\3\2\2\2\u00d4\25\3\2\2\2\u00d5"+ - "\u00d6\5 \21\2\u00d6\27\3\2\2\2\u00d7\u00d8\5\32\16\2\u00d8\u00dd\5\34"+ - "\17\2\u00d9\u00da\7\r\2\2\u00da\u00dc\5\34\17\2\u00db\u00d9\3\2\2\2\u00dc"+ - "\u00df\3\2\2\2\u00dd\u00db\3\2\2\2\u00dd\u00de\3\2\2\2\u00de\31\3\2\2"+ - "\2\u00df\u00dd\3\2\2\2\u00e0\u00e5\7S\2\2\u00e1\u00e2\7\7\2\2\u00e2\u00e4"+ - "\7\b\2\2\u00e3\u00e1\3\2\2\2\u00e4\u00e7\3\2\2\2\u00e5\u00e3\3\2\2\2\u00e5"+ - "\u00e6\3\2\2\2\u00e6\33\3\2\2\2\u00e7\u00e5\3\2\2\2\u00e8\u00eb\7T\2\2"+ - "\u00e9\u00ea\7>\2\2\u00ea\u00ec\5 \21\2\u00eb\u00e9\3\2\2\2\u00eb\u00ec"+ - "\3\2\2\2\u00ec\35\3\2\2\2\u00ed\u00ee\7\32\2\2\u00ee\u00ef\7\t\2\2\u00ef"+ - "\u00f0\7S\2\2\u00f0\u00f1\7T\2\2\u00f1\u00f2\7\n\2\2\u00f2\u00f3\5\20"+ - "\t\2\u00f3\37\3\2\2\2\u00f4\u00f5\b\21\1\2\u00f5\u00f6\5\"\22\2\u00f6"+ - "\u0129\3\2\2\2\u00f7\u00f8\f\21\2\2\u00f8\u00f9\t\2\2\2\u00f9\u0128\5"+ - " \21\22\u00fa\u00fb\f\20\2\2\u00fb\u00fc\t\3\2\2\u00fc\u0128\5 \21\21"+ - "\u00fd\u00fe\f\17\2\2\u00fe\u00ff\t\4\2\2\u00ff\u0128\5 \21\20\u0100\u0101"+ - "\f\16\2\2\u0101\u0102\t\5\2\2\u0102\u0128\5 \21\17\u0103\u0104\f\r\2\2"+ - "\u0104\u0105\t\6\2\2\u0105\u0128\5 \21\16\u0106\u0107\f\13\2\2\u0107\u0108"+ - "\t\7\2\2\u0108\u0128\5 \21\f\u0109\u010a\f\n\2\2\u010a\u010b\7\60\2\2"+ - "\u010b\u0128\5 \21\13\u010c\u010d\f\t\2\2\u010d\u010e\7\61\2\2\u010e\u0128"+ - "\5 \21\n\u010f\u0110\f\b\2\2\u0110\u0111\7\62\2\2\u0111\u0128\5 \21\t"+ - "\u0112\u0113\f\7\2\2\u0113\u0114\7\63\2\2\u0114\u0128\5 \21\b\u0115\u0116"+ - "\f\6\2\2\u0116\u0117\7\64\2\2\u0117\u0128\5 \21\7\u0118\u0119\f\5\2\2"+ - "\u0119\u011a\7\65\2\2\u011a\u011b\5 \21\2\u011b\u011c\7\66\2\2\u011c\u011d"+ - "\5 \21\5\u011d\u0128\3\2\2\2\u011e\u011f\f\4\2\2\u011f\u0120\7\67\2\2"+ - "\u0120\u0128\5 \21\4\u0121\u0122\f\3\2\2\u0122\u0123\t\b\2\2\u0123\u0128"+ - "\5 \21\3\u0124\u0125\f\f\2\2\u0125\u0126\7\35\2\2\u0126\u0128\5\32\16"+ - "\2\u0127\u00f7\3\2\2\2\u0127\u00fa\3\2\2\2\u0127\u00fd\3\2\2\2\u0127\u0100"+ - "\3\2\2\2\u0127\u0103\3\2\2\2\u0127\u0106\3\2\2\2\u0127\u0109\3\2\2\2\u0127"+ - "\u010c\3\2\2\2\u0127\u010f\3\2\2\2\u0127\u0112\3\2\2\2\u0127\u0115\3\2"+ - "\2\2\u0127\u0118\3\2\2\2\u0127\u011e\3\2\2\2\u0127\u0121\3\2\2\2\u0127"+ - "\u0124\3\2\2\2\u0128\u012b\3\2\2\2\u0129\u0127\3\2\2\2\u0129\u012a\3\2"+ - "\2\2\u012a!\3\2\2\2\u012b\u0129\3\2\2\2\u012c\u012d\t\t\2\2\u012d\u013a"+ - "\5$\23\2\u012e\u012f\5$\23\2\u012f\u0130\t\t\2\2\u0130\u013a\3\2\2\2\u0131"+ - "\u013a\5$\23\2\u0132\u0133\t\n\2\2\u0133\u013a\5\"\22\2\u0134\u0135\7"+ - "\t\2\2\u0135\u0136\5\32\16\2\u0136\u0137\7\n\2\2\u0137\u0138\5\"\22\2"+ - "\u0138\u013a\3\2\2\2\u0139\u012c\3\2\2\2\u0139\u012e\3\2\2\2\u0139\u0131"+ - "\3\2\2\2\u0139\u0132\3\2\2\2\u0139\u0134\3\2\2\2\u013a#\3\2\2\2\u013b"+ - "\u013f\5&\24\2\u013c\u013e\5(\25\2\u013d\u013c\3\2\2\2\u013e\u0141\3\2"+ - "\2\2\u013f\u013d\3\2\2\2\u013f\u0140\3\2\2\2\u0140\u014c\3\2\2\2\u0141"+ - "\u013f\3\2\2\2\u0142\u0143\5\32\16\2\u0143\u0147\5*\26\2\u0144\u0146\5"+ - "(\25\2\u0145\u0144\3\2\2\2\u0146\u0149\3\2\2\2\u0147\u0145\3\2\2\2\u0147"+ - "\u0148\3\2\2\2\u0148\u014c\3\2\2\2\u0149\u0147\3\2\2\2\u014a\u014c\5\62"+ - "\32\2\u014b\u013b\3\2\2\2\u014b\u0142\3\2\2\2\u014b\u014a\3\2\2\2\u014c"+ - "%\3\2\2\2\u014d\u014e\7\t\2\2\u014e\u014f\5 \21\2\u014f\u0150\7\n\2\2"+ - "\u0150\u0160\3\2\2\2\u0151\u0160\t\13\2\2\u0152\u0160\7P\2\2\u0153\u0160"+ - "\7Q\2\2\u0154\u0160\7R\2\2\u0155\u0160\7N\2\2\u0156\u0160\7O\2\2\u0157"+ - "\u0160\5\64\33\2\u0158\u0160\5\66\34\2\u0159\u0160\7T\2\2\u015a\u015b"+ - "\7T\2\2\u015b\u0160\5:\36\2\u015c\u015d\7\30\2\2\u015d\u015e\7S\2\2\u015e"+ - "\u0160\5:\36\2\u015f\u014d\3\2\2\2\u015f\u0151\3\2\2\2\u015f\u0152\3\2"+ - "\2\2\u015f\u0153\3\2\2\2\u015f\u0154\3\2\2\2\u015f\u0155\3\2\2\2\u015f"+ - "\u0156\3\2\2\2\u015f\u0157\3\2\2\2\u015f\u0158\3\2\2\2\u015f\u0159\3\2"+ - "\2\2\u015f\u015a\3\2\2\2\u015f\u015c\3\2\2\2\u0160\'\3\2\2\2\u0161\u0165"+ - "\5,\27\2\u0162\u0165\5.\30\2\u0163\u0165\5\60\31\2\u0164\u0161\3\2\2\2"+ - "\u0164\u0162\3\2\2\2\u0164\u0163\3\2\2\2\u0165)\3\2\2\2\u0166\u0169\5"+ - ",\27\2\u0167\u0169\5.\30\2\u0168\u0166\3\2\2\2\u0168\u0167\3\2\2\2\u0169"+ - "+\3\2\2\2\u016a\u016b\t\f\2\2\u016b\u016c\7V\2\2\u016c\u016d\5:\36\2\u016d"+ - "-\3\2\2\2\u016e\u016f\t\f\2\2\u016f\u0170\t\r\2\2\u0170/\3\2\2\2\u0171"+ - "\u0172\7\7\2\2\u0172\u0173\5 \21\2\u0173\u0174\7\b\2\2\u0174\61\3\2\2"+ - "\2\u0175\u0176\7\30\2\2\u0176\u017b\7S\2\2\u0177\u0178\7\7\2\2\u0178\u0179"+ - "\5 \21\2\u0179\u017a\7\b\2\2\u017a\u017c\3\2\2\2\u017b\u0177\3\2\2\2\u017c"+ - "\u017d\3\2\2\2\u017d\u017b\3\2\2\2\u017d\u017e\3\2\2\2\u017e\u0186\3\2"+ - "\2\2\u017f\u0183\5*\26\2\u0180\u0182\5(\25\2\u0181\u0180\3\2\2\2\u0182"+ - "\u0185\3\2\2\2\u0183\u0181\3\2\2\2\u0183\u0184\3\2\2\2\u0184\u0187\3\2"+ - "\2\2\u0185\u0183\3\2\2\2\u0186\u017f\3\2\2\2\u0186\u0187\3\2\2\2\u0187"+ - "\u019f\3\2\2\2\u0188\u0189\7\30\2\2\u0189\u018a\7S\2\2\u018a\u018b\7\7"+ - "\2\2\u018b\u018c\7\b\2\2\u018c\u0195\7\5\2\2\u018d\u0192\5 \21\2\u018e"+ - "\u018f\7\r\2\2\u018f\u0191\5 \21\2\u0190\u018e\3\2\2\2\u0191\u0194\3\2"+ - "\2\2\u0192\u0190\3\2\2\2\u0192\u0193\3\2\2\2\u0193\u0196\3\2\2\2\u0194"+ - "\u0192\3\2\2\2\u0195\u018d\3\2\2\2\u0195\u0196\3\2\2\2\u0196\u0197\3\2"+ - "\2\2\u0197\u019b\7\6\2\2\u0198\u019a\5(\25\2\u0199\u0198\3\2\2\2\u019a"+ - "\u019d\3\2\2\2\u019b\u0199\3\2\2\2\u019b\u019c\3\2\2\2\u019c\u019f\3\2"+ - "\2\2\u019d\u019b\3\2\2\2\u019e\u0175\3\2\2\2\u019e\u0188\3\2\2\2\u019f"+ - "\63\3\2\2\2\u01a0\u01a1\7\7\2\2\u01a1\u01a6\5 \21\2\u01a2\u01a3\7\r\2"+ - "\2\u01a3\u01a5\5 \21\2\u01a4\u01a2\3\2\2\2\u01a5\u01a8\3\2\2\2\u01a6\u01a4"+ - "\3\2\2\2\u01a6\u01a7\3\2\2\2\u01a7\u01a9\3\2\2\2\u01a8\u01a6\3\2\2\2\u01a9"+ - "\u01aa\7\b\2\2\u01aa\u01ae\3\2\2\2\u01ab\u01ac\7\7\2\2\u01ac\u01ae\7\b"+ - "\2\2\u01ad\u01a0\3\2\2\2\u01ad\u01ab\3\2\2\2\u01ae\65\3\2\2\2\u01af\u01b0"+ - "\7\7\2\2\u01b0\u01b5\58\35\2\u01b1\u01b2\7\r\2\2\u01b2\u01b4\58\35\2\u01b3"+ - "\u01b1\3\2\2\2\u01b4\u01b7\3\2\2\2\u01b5\u01b3\3\2\2\2\u01b5\u01b6\3\2"+ - "\2\2\u01b6\u01b8\3\2\2\2\u01b7\u01b5\3\2\2\2\u01b8\u01b9\7\b\2\2\u01b9"+ - "\u01be\3\2\2\2\u01ba\u01bb\7\7\2\2\u01bb\u01bc\7\66\2\2\u01bc\u01be\7"+ - "\b\2\2\u01bd\u01af\3\2\2\2\u01bd\u01ba\3\2\2\2\u01be\67\3\2\2\2\u01bf"+ - "\u01c0\5 \21\2\u01c0\u01c1\7\66\2\2\u01c1\u01c2\5 \21\2\u01c29\3\2\2\2"+ - "\u01c3\u01cc\7\t\2\2\u01c4\u01c9\5<\37\2\u01c5\u01c6\7\r\2\2\u01c6\u01c8"+ - "\5<\37\2\u01c7\u01c5\3\2\2\2\u01c8\u01cb\3\2\2\2\u01c9\u01c7\3\2\2\2\u01c9"+ - "\u01ca\3\2\2\2\u01ca\u01cd\3\2\2\2\u01cb\u01c9\3\2\2\2\u01cc\u01c4\3\2"+ - "\2\2\u01cc\u01cd\3\2\2\2\u01cd\u01ce\3\2\2\2\u01ce\u01cf\7\n\2\2\u01cf"+ - ";\3\2\2\2\u01d0\u01d4\5 \21\2\u01d1\u01d4\5> \2\u01d2\u01d4\5B\"\2\u01d3"+ - "\u01d0\3\2\2\2\u01d3\u01d1\3\2\2\2\u01d3\u01d2\3\2\2\2\u01d4=\3\2\2\2"+ - "\u01d5\u01e3\5@!\2\u01d6\u01df\7\t\2\2\u01d7\u01dc\5@!\2\u01d8\u01d9\7"+ - "\r\2\2\u01d9\u01db\5@!\2\u01da\u01d8\3\2\2\2\u01db\u01de\3\2\2\2\u01dc"+ - "\u01da\3\2\2\2\u01dc\u01dd\3\2\2\2\u01dd\u01e0\3\2\2\2\u01de\u01dc\3\2"+ - "\2\2\u01df\u01d7\3\2\2\2\u01df\u01e0\3\2\2\2\u01e0\u01e1\3\2\2\2\u01e1"+ - "\u01e3\7\n\2\2\u01e2\u01d5\3\2\2\2\u01e2\u01d6\3\2\2\2\u01e3\u01e4\3\2"+ - "\2\2\u01e4\u01e7\79\2\2\u01e5\u01e8\5\20\t\2\u01e6\u01e8\5 \21\2\u01e7"+ - "\u01e5\3\2\2\2\u01e7\u01e6\3\2\2\2\u01e8?\3\2\2\2\u01e9\u01eb\5\32\16"+ - "\2\u01ea\u01e9\3\2\2\2\u01ea\u01eb\3\2\2\2\u01eb\u01ec\3\2\2\2\u01ec\u01ed"+ - "\7T\2\2\u01edA\3\2\2\2\u01ee\u01ef\7S\2\2\u01ef\u01f0\78\2\2\u01f0\u01fc"+ - "\7T\2\2\u01f1\u01f2\5\32\16\2\u01f2\u01f3\78\2\2\u01f3\u01f4\7\30\2\2"+ - "\u01f4\u01fc\3\2\2\2\u01f5\u01f6\7T\2\2\u01f6\u01f7\78\2\2\u01f7\u01fc"+ - "\7T\2\2\u01f8\u01f9\7\34\2\2\u01f9\u01fa\78\2\2\u01fa\u01fc\7T\2\2\u01fb"+ - "\u01ee\3\2\2\2\u01fb\u01f1\3\2\2\2\u01fb\u01f5\3\2\2\2\u01fb\u01f8\3\2"+ - "\2\2\u01fcC\3\2\2\2\65GMQcfnx\u0080\u0085\u0089\u008d\u0092\u00aa\u00ac"+ - "\u00bd\u00c1\u00c7\u00cb\u00d3\u00dd\u00e5\u00eb\u0127\u0129\u0139\u013f"+ - "\u0147\u014b\u015f\u0164\u0168\u017d\u0183\u0186\u0192\u0195\u019b\u019e"+ - "\u01a6\u01ad\u01b5\u01bd\u01c9\u01cc\u01d3\u01dc\u01df\u01e2\u01e7\u01ea"+ - "\u01fb"; + "\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\3\21\7\21"+ + "\u0125\n\21\f\21\16\21\u0128\13\21\3\22\3\22\3\22\3\22\3\22\3\22\3\22"+ + "\3\22\3\22\3\22\3\22\3\22\3\22\5\22\u0137\n\22\3\23\3\23\7\23\u013b\n"+ + "\23\f\23\16\23\u013e\13\23\3\23\3\23\3\23\7\23\u0143\n\23\f\23\16\23\u0146"+ + "\13\23\3\23\5\23\u0149\n\23\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3"+ + "\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\3\24\5\24\u015d\n\24\3\25"+ + "\3\25\3\25\5\25\u0162\n\25\3\26\3\26\5\26\u0166\n\26\3\27\3\27\3\27\3"+ + "\27\3\30\3\30\3\30\3\31\3\31\3\31\3\31\3\32\3\32\3\32\3\32\3\32\3\32\6"+ + "\32\u0179\n\32\r\32\16\32\u017a\3\32\3\32\7\32\u017f\n\32\f\32\16\32\u0182"+ + "\13\32\5\32\u0184\n\32\3\32\3\32\3\32\3\32\3\32\3\32\3\32\3\32\7\32\u018e"+ + "\n\32\f\32\16\32\u0191\13\32\5\32\u0193\n\32\3\32\3\32\7\32\u0197\n\32"+ + "\f\32\16\32\u019a\13\32\5\32\u019c\n\32\3\33\3\33\3\33\3\33\7\33\u01a2"+ + "\n\33\f\33\16\33\u01a5\13\33\3\33\3\33\3\33\3\33\5\33\u01ab\n\33\3\34"+ + "\3\34\3\34\3\34\7\34\u01b1\n\34\f\34\16\34\u01b4\13\34\3\34\3\34\3\34"+ + "\3\34\3\34\5\34\u01bb\n\34\3\35\3\35\3\35\3\35\3\36\3\36\3\36\3\36\7\36"+ + "\u01c5\n\36\f\36\16\36\u01c8\13\36\5\36\u01ca\n\36\3\36\3\36\3\37\3\37"+ + "\3\37\5\37\u01d1\n\37\3 \3 \3 \3 \3 \7 \u01d8\n \f \16 \u01db\13 \5 \u01dd"+ + "\n \3 \5 \u01e0\n \3 \3 \3 \5 \u01e5\n \3!\5!\u01e8\n!\3!\3!\3\"\3\"\3"+ + "\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\3\"\5\"\u01f9\n\"\3\"\2\3 #\2\4"+ + "\6\b\n\f\16\20\22\24\26\30\32\34\36 \"$&(*,.\60\62\64\668:<>@B\2\17\3"+ + "\3\16\16\3\2 \"\3\2#$\3\2:;\3\2%\'\3\2(+\3\2,/\3\2>I\3\2<=\4\2\36\37#"+ + "$\3\2JM\3\2\13\f\3\2UV\u0233\2G\3\2\2\2\4R\3\2\2\2\6W\3\2\2\2\bk\3\2\2"+ + "\2\n\u00a9\3\2\2\2\f\u00ba\3\2\2\2\16\u00be\3\2\2\2\20\u00c0\3\2\2\2\22"+ + "\u00cc\3\2\2\2\24\u00d0\3\2\2\2\26\u00d2\3\2\2\2\30\u00d4\3\2\2\2\32\u00dd"+ + "\3\2\2\2\34\u00e5\3\2\2\2\36\u00ea\3\2\2\2 \u00f1\3\2\2\2\"\u0136\3\2"+ + "\2\2$\u0148\3\2\2\2&\u015c\3\2\2\2(\u0161\3\2\2\2*\u0165\3\2\2\2,\u0167"+ + "\3\2\2\2.\u016b\3\2\2\2\60\u016e\3\2\2\2\62\u019b\3\2\2\2\64\u01aa\3\2"+ + "\2\2\66\u01ba\3\2\2\28\u01bc\3\2\2\2:\u01c0\3\2\2\2<\u01d0\3\2\2\2>\u01df"+ + "\3\2\2\2@\u01e7\3\2\2\2B\u01f8\3\2\2\2DF\5\4\3\2ED\3\2\2\2FI\3\2\2\2G"+ + "E\3\2\2\2GH\3\2\2\2HM\3\2\2\2IG\3\2\2\2JL\5\b\5\2KJ\3\2\2\2LO\3\2\2\2"+ + "MK\3\2\2\2MN\3\2\2\2NP\3\2\2\2OM\3\2\2\2PQ\7\2\2\3Q\3\3\2\2\2RS\5\32\16"+ + "\2ST\7T\2\2TU\5\6\4\2UV\5\20\t\2V\5\3\2\2\2Wc\7\t\2\2XY\5\32\16\2Y`\7"+ + "T\2\2Z[\7\r\2\2[\\\5\32\16\2\\]\7T\2\2]_\3\2\2\2^Z\3\2\2\2_b\3\2\2\2`"+ + "^\3\2\2\2`a\3\2\2\2ad\3\2\2\2b`\3\2\2\2cX\3\2\2\2cd\3\2\2\2de\3\2\2\2"+ + "ef\7\n\2\2f\7\3\2\2\2gl\5\n\6\2hi\5\f\7\2ij\t\2\2\2jl\3\2\2\2kg\3\2\2"+ + "\2kh\3\2\2\2l\t\3\2\2\2mn\7\17\2\2no\7\t\2\2op\5 \21\2pq\7\n\2\2qu\5\16"+ + "\b\2rs\7\21\2\2sv\5\16\b\2tv\6\6\2\2ur\3\2\2\2ut\3\2\2\2v\u00aa\3\2\2"+ + "\2wx\7\22\2\2xy\7\t\2\2yz\5 \21\2z}\7\n\2\2{~\5\16\b\2|~\5\22\n\2}{\3"+ + "\2\2\2}|\3\2\2\2~\u00aa\3\2\2\2\177\u0080\7\24\2\2\u0080\u0082\7\t\2\2"+ + "\u0081\u0083\5\24\13\2\u0082\u0081\3\2\2\2\u0082\u0083\3\2\2\2\u0083\u0084"+ + "\3\2\2\2\u0084\u0086\7\16\2\2\u0085\u0087\5 \21\2\u0086\u0085\3\2\2\2"+ + "\u0086\u0087\3\2\2\2\u0087\u0088\3\2\2\2\u0088\u008a\7\16\2\2\u0089\u008b"+ + "\5\26\f\2\u008a\u0089\3\2\2\2\u008a\u008b\3\2\2\2\u008b\u008c\3\2\2\2"+ + "\u008c\u008f\7\n\2\2\u008d\u0090\5\16\b\2\u008e\u0090\5\22\n\2\u008f\u008d"+ + "\3\2\2\2\u008f\u008e\3\2\2\2\u0090\u00aa\3\2\2\2\u0091\u0092\7\24\2\2"+ + "\u0092\u0093\7\t\2\2\u0093\u0094\5\32\16\2\u0094\u0095\7T\2\2\u0095\u0096"+ + "\7\66\2\2\u0096\u0097\5 \21\2\u0097\u0098\7\n\2\2\u0098\u0099\5\16\b\2"+ + "\u0099\u00aa\3\2\2\2\u009a\u009b\7\24\2\2\u009b\u009c\7\t\2\2\u009c\u009d"+ + "\7T\2\2\u009d\u009e\7\20\2\2\u009e\u009f\5 \21\2\u009f\u00a0\7\n\2\2\u00a0"+ + "\u00a1\5\16\b\2\u00a1\u00aa\3\2\2\2\u00a2\u00a3\7\31\2\2\u00a3\u00a5\5"+ + "\20\t\2\u00a4\u00a6\5\36\20\2\u00a5\u00a4\3\2\2\2\u00a6\u00a7\3\2\2\2"+ + "\u00a7\u00a5\3\2\2\2\u00a7\u00a8\3\2\2\2\u00a8\u00aa\3\2\2\2\u00a9m\3"+ + "\2\2\2\u00a9w\3\2\2\2\u00a9\177\3\2\2\2\u00a9\u0091\3\2\2\2\u00a9\u009a"+ + "\3\2\2\2\u00a9\u00a2\3\2\2\2\u00aa\13\3\2\2\2\u00ab\u00ac\7\23\2\2\u00ac"+ + "\u00ad\5\20\t\2\u00ad\u00ae\7\22\2\2\u00ae\u00af\7\t\2\2\u00af\u00b0\5"+ + " \21\2\u00b0\u00b1\7\n\2\2\u00b1\u00bb\3\2\2\2\u00b2\u00bb\5\30\r\2\u00b3"+ + "\u00bb\7\25\2\2\u00b4\u00bb\7\26\2\2\u00b5\u00b6\7\27\2\2\u00b6\u00bb"+ + "\5 \21\2\u00b7\u00b8\7\33\2\2\u00b8\u00bb\5 \21\2\u00b9\u00bb\5 \21\2"+ + "\u00ba\u00ab\3\2\2\2\u00ba\u00b2\3\2\2\2\u00ba\u00b3\3\2\2\2\u00ba\u00b4"+ + "\3\2\2\2\u00ba\u00b5\3\2\2\2\u00ba\u00b7\3\2\2\2\u00ba\u00b9\3\2\2\2\u00bb"+ + "\r\3\2\2\2\u00bc\u00bf\5\20\t\2\u00bd\u00bf\5\b\5\2\u00be\u00bc\3\2\2"+ + "\2\u00be\u00bd\3\2\2\2\u00bf\17\3\2\2\2\u00c0\u00c4\7\5\2\2\u00c1\u00c3"+ + "\5\b\5\2\u00c2\u00c1\3\2\2\2\u00c3\u00c6\3\2\2\2\u00c4\u00c2\3\2\2\2\u00c4"+ + "\u00c5\3\2\2\2\u00c5\u00c8\3\2\2\2\u00c6\u00c4\3\2\2\2\u00c7\u00c9\5\f"+ + "\7\2\u00c8\u00c7\3\2\2\2\u00c8\u00c9\3\2\2\2\u00c9\u00ca\3\2\2\2\u00ca"+ + "\u00cb\7\6\2\2\u00cb\21\3\2\2\2\u00cc\u00cd\7\16\2\2\u00cd\23\3\2\2\2"+ + "\u00ce\u00d1\5\30\r\2\u00cf\u00d1\5 \21\2\u00d0\u00ce\3\2\2\2\u00d0\u00cf"+ + "\3\2\2\2\u00d1\25\3\2\2\2\u00d2\u00d3\5 \21\2\u00d3\27\3\2\2\2\u00d4\u00d5"+ + "\5\32\16\2\u00d5\u00da\5\34\17\2\u00d6\u00d7\7\r\2\2\u00d7\u00d9\5\34"+ + "\17\2\u00d8\u00d6\3\2\2\2\u00d9\u00dc\3\2\2\2\u00da\u00d8\3\2\2\2\u00da"+ + "\u00db\3\2\2\2\u00db\31\3\2\2\2\u00dc\u00da\3\2\2\2\u00dd\u00e2\7S\2\2"+ + "\u00de\u00df\7\7\2\2\u00df\u00e1\7\b\2\2\u00e0\u00de\3\2\2\2\u00e1\u00e4"+ + "\3\2\2\2\u00e2\u00e0\3\2\2\2\u00e2\u00e3\3\2\2\2\u00e3\33\3\2\2\2\u00e4"+ + "\u00e2\3\2\2\2\u00e5\u00e8\7T\2\2\u00e6\u00e7\7>\2\2\u00e7\u00e9\5 \21"+ + "\2\u00e8\u00e6\3\2\2\2\u00e8\u00e9\3\2\2\2\u00e9\35\3\2\2\2\u00ea\u00eb"+ + "\7\32\2\2\u00eb\u00ec\7\t\2\2\u00ec\u00ed\7S\2\2\u00ed\u00ee\7T\2\2\u00ee"+ + "\u00ef\7\n\2\2\u00ef\u00f0\5\20\t\2\u00f0\37\3\2\2\2\u00f1\u00f2\b\21"+ + "\1\2\u00f2\u00f3\5\"\22\2\u00f3\u0126\3\2\2\2\u00f4\u00f5\f\21\2\2\u00f5"+ + "\u00f6\t\3\2\2\u00f6\u0125\5 \21\22\u00f7\u00f8\f\20\2\2\u00f8\u00f9\t"+ + "\4\2\2\u00f9\u0125\5 \21\21\u00fa\u00fb\f\17\2\2\u00fb\u00fc\t\5\2\2\u00fc"+ + "\u0125\5 \21\20\u00fd\u00fe\f\16\2\2\u00fe\u00ff\t\6\2\2\u00ff\u0125\5"+ + " \21\17\u0100\u0101\f\r\2\2\u0101\u0102\t\7\2\2\u0102\u0125\5 \21\16\u0103"+ + "\u0104\f\13\2\2\u0104\u0105\t\b\2\2\u0105\u0125\5 \21\f\u0106\u0107\f"+ + "\n\2\2\u0107\u0108\7\60\2\2\u0108\u0125\5 \21\13\u0109\u010a\f\t\2\2\u010a"+ + "\u010b\7\61\2\2\u010b\u0125\5 \21\n\u010c\u010d\f\b\2\2\u010d\u010e\7"+ + "\62\2\2\u010e\u0125\5 \21\t\u010f\u0110\f\7\2\2\u0110\u0111\7\63\2\2\u0111"+ + "\u0125\5 \21\b\u0112\u0113\f\6\2\2\u0113\u0114\7\64\2\2\u0114\u0125\5"+ + " \21\7\u0115\u0116\f\5\2\2\u0116\u0117\7\65\2\2\u0117\u0118\5 \21\2\u0118"+ + "\u0119\7\66\2\2\u0119\u011a\5 \21\5\u011a\u0125\3\2\2\2\u011b\u011c\f"+ + "\4\2\2\u011c\u011d\7\67\2\2\u011d\u0125\5 \21\4\u011e\u011f\f\3\2\2\u011f"+ + "\u0120\t\t\2\2\u0120\u0125\5 \21\3\u0121\u0122\f\f\2\2\u0122\u0123\7\35"+ + "\2\2\u0123\u0125\5\32\16\2\u0124\u00f4\3\2\2\2\u0124\u00f7\3\2\2\2\u0124"+ + "\u00fa\3\2\2\2\u0124\u00fd\3\2\2\2\u0124\u0100\3\2\2\2\u0124\u0103\3\2"+ + "\2\2\u0124\u0106\3\2\2\2\u0124\u0109\3\2\2\2\u0124\u010c\3\2\2\2\u0124"+ + "\u010f\3\2\2\2\u0124\u0112\3\2\2\2\u0124\u0115\3\2\2\2\u0124\u011b\3\2"+ + "\2\2\u0124\u011e\3\2\2\2\u0124\u0121\3\2\2\2\u0125\u0128\3\2\2\2\u0126"+ + "\u0124\3\2\2\2\u0126\u0127\3\2\2\2\u0127!\3\2\2\2\u0128\u0126\3\2\2\2"+ + "\u0129\u012a\t\n\2\2\u012a\u0137\5$\23\2\u012b\u012c\5$\23\2\u012c\u012d"+ + "\t\n\2\2\u012d\u0137\3\2\2\2\u012e\u0137\5$\23\2\u012f\u0130\t\13\2\2"+ + "\u0130\u0137\5\"\22\2\u0131\u0132\7\t\2\2\u0132\u0133\5\32\16\2\u0133"+ + "\u0134\7\n\2\2\u0134\u0135\5\"\22\2\u0135\u0137\3\2\2\2\u0136\u0129\3"+ + "\2\2\2\u0136\u012b\3\2\2\2\u0136\u012e\3\2\2\2\u0136\u012f\3\2\2\2\u0136"+ + "\u0131\3\2\2\2\u0137#\3\2\2\2\u0138\u013c\5&\24\2\u0139\u013b\5(\25\2"+ + "\u013a\u0139\3\2\2\2\u013b\u013e\3\2\2\2\u013c\u013a\3\2\2\2\u013c\u013d"+ + "\3\2\2\2\u013d\u0149\3\2\2\2\u013e\u013c\3\2\2\2\u013f\u0140\5\32\16\2"+ + "\u0140\u0144\5*\26\2\u0141\u0143\5(\25\2\u0142\u0141\3\2\2\2\u0143\u0146"+ + "\3\2\2\2\u0144\u0142\3\2\2\2\u0144\u0145\3\2\2\2\u0145\u0149\3\2\2\2\u0146"+ + "\u0144\3\2\2\2\u0147\u0149\5\62\32\2\u0148\u0138\3\2\2\2\u0148\u013f\3"+ + "\2\2\2\u0148\u0147\3\2\2\2\u0149%\3\2\2\2\u014a\u014b\7\t\2\2\u014b\u014c"+ + "\5 \21\2\u014c\u014d\7\n\2\2\u014d\u015d\3\2\2\2\u014e\u015d\t\f\2\2\u014f"+ + "\u015d\7P\2\2\u0150\u015d\7Q\2\2\u0151\u015d\7R\2\2\u0152\u015d\7N\2\2"+ + "\u0153\u015d\7O\2\2\u0154\u015d\5\64\33\2\u0155\u015d\5\66\34\2\u0156"+ + "\u015d\7T\2\2\u0157\u0158\7T\2\2\u0158\u015d\5:\36\2\u0159\u015a\7\30"+ + "\2\2\u015a\u015b\7S\2\2\u015b\u015d\5:\36\2\u015c\u014a\3\2\2\2\u015c"+ + "\u014e\3\2\2\2\u015c\u014f\3\2\2\2\u015c\u0150\3\2\2\2\u015c\u0151\3\2"+ + "\2\2\u015c\u0152\3\2\2\2\u015c\u0153\3\2\2\2\u015c\u0154\3\2\2\2\u015c"+ + "\u0155\3\2\2\2\u015c\u0156\3\2\2\2\u015c\u0157\3\2\2\2\u015c\u0159\3\2"+ + "\2\2\u015d\'\3\2\2\2\u015e\u0162\5,\27\2\u015f\u0162\5.\30\2\u0160\u0162"+ + "\5\60\31\2\u0161\u015e\3\2\2\2\u0161\u015f\3\2\2\2\u0161\u0160\3\2\2\2"+ + "\u0162)\3\2\2\2\u0163\u0166\5,\27\2\u0164\u0166\5.\30\2\u0165\u0163\3"+ + "\2\2\2\u0165\u0164\3\2\2\2\u0166+\3\2\2\2\u0167\u0168\t\r\2\2\u0168\u0169"+ + "\7V\2\2\u0169\u016a\5:\36\2\u016a-\3\2\2\2\u016b\u016c\t\r\2\2\u016c\u016d"+ + "\t\16\2\2\u016d/\3\2\2\2\u016e\u016f\7\7\2\2\u016f\u0170\5 \21\2\u0170"+ + "\u0171\7\b\2\2\u0171\61\3\2\2\2\u0172\u0173\7\30\2\2\u0173\u0178\7S\2"+ + "\2\u0174\u0175\7\7\2\2\u0175\u0176\5 \21\2\u0176\u0177\7\b\2\2\u0177\u0179"+ + "\3\2\2\2\u0178\u0174\3\2\2\2\u0179\u017a\3\2\2\2\u017a\u0178\3\2\2\2\u017a"+ + "\u017b\3\2\2\2\u017b\u0183\3\2\2\2\u017c\u0180\5*\26\2\u017d\u017f\5("+ + "\25\2\u017e\u017d\3\2\2\2\u017f\u0182\3\2\2\2\u0180\u017e\3\2\2\2\u0180"+ + "\u0181\3\2\2\2\u0181\u0184\3\2\2\2\u0182\u0180\3\2\2\2\u0183\u017c\3\2"+ + "\2\2\u0183\u0184\3\2\2\2\u0184\u019c\3\2\2\2\u0185\u0186\7\30\2\2\u0186"+ + "\u0187\7S\2\2\u0187\u0188\7\7\2\2\u0188\u0189\7\b\2\2\u0189\u0192\7\5"+ + "\2\2\u018a\u018f\5 \21\2\u018b\u018c\7\r\2\2\u018c\u018e\5 \21\2\u018d"+ + "\u018b\3\2\2\2\u018e\u0191\3\2\2\2\u018f\u018d\3\2\2\2\u018f\u0190\3\2"+ + "\2\2\u0190\u0193\3\2\2\2\u0191\u018f\3\2\2\2\u0192\u018a\3\2\2\2\u0192"+ + "\u0193\3\2\2\2\u0193\u0194\3\2\2\2\u0194\u0198\7\6\2\2\u0195\u0197\5("+ + "\25\2\u0196\u0195\3\2\2\2\u0197\u019a\3\2\2\2\u0198\u0196\3\2\2\2\u0198"+ + "\u0199\3\2\2\2\u0199\u019c\3\2\2\2\u019a\u0198\3\2\2\2\u019b\u0172\3\2"+ + "\2\2\u019b\u0185\3\2\2\2\u019c\63\3\2\2\2\u019d\u019e\7\7\2\2\u019e\u01a3"+ + "\5 \21\2\u019f\u01a0\7\r\2\2\u01a0\u01a2\5 \21\2\u01a1\u019f\3\2\2\2\u01a2"+ + "\u01a5\3\2\2\2\u01a3\u01a1\3\2\2\2\u01a3\u01a4\3\2\2\2\u01a4\u01a6\3\2"+ + "\2\2\u01a5\u01a3\3\2\2\2\u01a6\u01a7\7\b\2\2\u01a7\u01ab\3\2\2\2\u01a8"+ + "\u01a9\7\7\2\2\u01a9\u01ab\7\b\2\2\u01aa\u019d\3\2\2\2\u01aa\u01a8\3\2"+ + "\2\2\u01ab\65\3\2\2\2\u01ac\u01ad\7\7\2\2\u01ad\u01b2\58\35\2\u01ae\u01af"+ + "\7\r\2\2\u01af\u01b1\58\35\2\u01b0\u01ae\3\2\2\2\u01b1\u01b4\3\2\2\2\u01b2"+ + "\u01b0\3\2\2\2\u01b2\u01b3\3\2\2\2\u01b3\u01b5\3\2\2\2\u01b4\u01b2\3\2"+ + "\2\2\u01b5\u01b6\7\b\2\2\u01b6\u01bb\3\2\2\2\u01b7\u01b8\7\7\2\2\u01b8"+ + "\u01b9\7\66\2\2\u01b9\u01bb\7\b\2\2\u01ba\u01ac\3\2\2\2\u01ba\u01b7\3"+ + "\2\2\2\u01bb\67\3\2\2\2\u01bc\u01bd\5 \21\2\u01bd\u01be\7\66\2\2\u01be"+ + "\u01bf\5 \21\2\u01bf9\3\2\2\2\u01c0\u01c9\7\t\2\2\u01c1\u01c6\5<\37\2"+ + "\u01c2\u01c3\7\r\2\2\u01c3\u01c5\5<\37\2\u01c4\u01c2\3\2\2\2\u01c5\u01c8"+ + "\3\2\2\2\u01c6\u01c4\3\2\2\2\u01c6\u01c7\3\2\2\2\u01c7\u01ca\3\2\2\2\u01c8"+ + "\u01c6\3\2\2\2\u01c9\u01c1\3\2\2\2\u01c9\u01ca\3\2\2\2\u01ca\u01cb\3\2"+ + "\2\2\u01cb\u01cc\7\n\2\2\u01cc;\3\2\2\2\u01cd\u01d1\5 \21\2\u01ce\u01d1"+ + "\5> \2\u01cf\u01d1\5B\"\2\u01d0\u01cd\3\2\2\2\u01d0\u01ce\3\2\2\2\u01d0"+ + "\u01cf\3\2\2\2\u01d1=\3\2\2\2\u01d2\u01e0\5@!\2\u01d3\u01dc\7\t\2\2\u01d4"+ + "\u01d9\5@!\2\u01d5\u01d6\7\r\2\2\u01d6\u01d8\5@!\2\u01d7\u01d5\3\2\2\2"+ + "\u01d8\u01db\3\2\2\2\u01d9\u01d7\3\2\2\2\u01d9\u01da\3\2\2\2\u01da\u01dd"+ + "\3\2\2\2\u01db\u01d9\3\2\2\2\u01dc\u01d4\3\2\2\2\u01dc\u01dd\3\2\2\2\u01dd"+ + "\u01de\3\2\2\2\u01de\u01e0\7\n\2\2\u01df\u01d2\3\2\2\2\u01df\u01d3\3\2"+ + "\2\2\u01e0\u01e1\3\2\2\2\u01e1\u01e4\79\2\2\u01e2\u01e5\5\20\t\2\u01e3"+ + "\u01e5\5 \21\2\u01e4\u01e2\3\2\2\2\u01e4\u01e3\3\2\2\2\u01e5?\3\2\2\2"+ + "\u01e6\u01e8\5\32\16\2\u01e7\u01e6\3\2\2\2\u01e7\u01e8\3\2\2\2\u01e8\u01e9"+ + "\3\2\2\2\u01e9\u01ea\7T\2\2\u01eaA\3\2\2\2\u01eb\u01ec\7S\2\2\u01ec\u01ed"+ + "\78\2\2\u01ed\u01f9\7T\2\2\u01ee\u01ef\5\32\16\2\u01ef\u01f0\78\2\2\u01f0"+ + "\u01f1\7\30\2\2\u01f1\u01f9\3\2\2\2\u01f2\u01f3\7T\2\2\u01f3\u01f4\78"+ + "\2\2\u01f4\u01f9\7T\2\2\u01f5\u01f6\7\34\2\2\u01f6\u01f7\78\2\2\u01f7"+ + "\u01f9\7T\2\2\u01f8\u01eb\3\2\2\2\u01f8\u01ee\3\2\2\2\u01f8\u01f2\3\2"+ + "\2\2\u01f8\u01f5\3\2\2\2\u01f9C\3\2\2\2\64GM`cku}\u0082\u0086\u008a\u008f"+ + "\u00a7\u00a9\u00ba\u00be\u00c4\u00c8\u00d0\u00da\u00e2\u00e8\u0124\u0126"+ + "\u0136\u013c\u0144\u0148\u015c\u0161\u0165\u017a\u0180\u0183\u018f\u0192"+ + "\u0198\u019b\u01a3\u01aa\u01b2\u01ba\u01c6\u01c9\u01d0\u01d9\u01dc\u01df"+ + "\u01e4\u01e7\u01f8"; public static final ATN _ATN = new ATNDeserializer().deserialize(_serializedATN.toCharArray()); static { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java index 6c8d3a62e065..dc5c164244d9 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/antlr/Walker.java @@ -261,10 +261,6 @@ public ANode visitSource(SourceContext ctx) { statements.add((AStatement)visit(statement)); } - if (ctx.dstatement() != null) { - statements.add((AStatement)visit(ctx.dstatement())); - } - return new SSource(scriptClassInfo, settings, sourceName, debugStream, (MainMethodReserved)reserved.pop(), location(ctx), functions, globals, statements); } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java index 0b13694524b0..25866c8d668a 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicAPITests.java @@ -129,4 +129,8 @@ public void testPublicMemberAccess() { assertEquals(5, exec("org.elasticsearch.painless.FeatureTest ft = new org.elasticsearch.painless.FeatureTest();" + "ft.z = 5; return ft.z;")); } + + public void testNoSemicolon() { + assertEquals(true, exec("def x = true; if (x) return x")); + } } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexTests.java index 8143c39ce6f6..81c139662e7c 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexTests.java @@ -278,6 +278,6 @@ public void testBogusRegexFlag() { IllegalArgumentException e = expectScriptThrows(IllegalArgumentException.class, () -> { exec("/asdf/b", false); // Not picky so we get a non-assertion error }); - assertEquals("invalid sequence of tokens near ['b'].", e.getMessage()); + assertEquals("unexpected token ['b'] was expecting one of [{, ';'}].", e.getMessage()); } } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java index f2d93aa759d0..79d2fe0c53de 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java @@ -255,7 +255,7 @@ public void testRCurlyNotDelim() { // We don't want PICKY here so we get the normal error message exec("def i = 1} return 1", emptyMap(), emptyMap(), null, false); }); - assertEquals("invalid sequence of tokens near ['}'].", e.getMessage()); + assertEquals("unexpected token ['}'] was expecting one of [{, ';'}].", e.getMessage()); } public void testBadBoxingCast() { From 84b61d073895e45fc2717812798d7f100f48402d Mon Sep 17 00:00:00 2001 From: Mark Tozzi Date: Tue, 28 Aug 2018 15:48:23 -0400 Subject: [PATCH 199/283] Scroll queries asking for rescore are considered invalid (#32918) This PR changes our behavior from silently ignoring rescore in a scroll query to instead report to the user that such a query is invalid. Closes #31775 --- .../reference/migration/migrate_7_0/search.asciidoc | 7 +++++++ .../elasticsearch/action/search/SearchRequest.java | 4 ++++ .../elasticsearch/search/SearchRequestTests.java | 13 +++++++++++++ 3 files changed, 24 insertions(+) diff --git a/docs/reference/migration/migrate_7_0/search.asciidoc b/docs/reference/migration/migrate_7_0/search.asciidoc index 76367115e130..a7d32896e972 100644 --- a/docs/reference/migration/migrate_7_0/search.asciidoc +++ b/docs/reference/migration/migrate_7_0/search.asciidoc @@ -54,6 +54,13 @@ Setting `request_cache:true` on a query that creates a scroll (`scroll=1m`) has been deprecated in 6 and will now return a `400 - Bad request`. Scroll queries are not meant to be cached. +==== Scroll queries cannot use `rescore` anymore +Including a rescore clause on a query that creates a scroll (`scroll=1m`) has +been deprecated in 6.5 and will now return a `400 - Bad request`. Allowing +rescore on scroll queries would break the scroll sort. In the 6.x line, the +rescore clause was silently ignored (for scroll queries), and it was allowed in +the 5.x line. + ==== Term Suggesters supported distance algorithms The following string distance algorithms were given additional names in 6.2 and diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index e560e53ed7b6..dd7f68729438 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -184,6 +184,10 @@ public ActionRequestValidationException validate() { if (source != null && source.size() == 0 && scroll != null) { validationException = addValidationError("[size] cannot be [0] in a scroll context", validationException); } + if (source != null && source.rescores() != null && source.rescores().isEmpty() == false && scroll != null) { + validationException = + addValidationError("using [rescore] is not allowed in a scroll context", validationException); + } return validationException; } diff --git a/server/src/test/java/org/elasticsearch/search/SearchRequestTests.java b/server/src/test/java/org/elasticsearch/search/SearchRequestTests.java index 95a9ae9d707d..36d2ef2c4db3 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchRequestTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchRequestTests.java @@ -28,7 +28,9 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.ArrayUtils; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.rescore.QueryRescorerBuilder; import java.io.IOException; import java.util.ArrayList; @@ -123,6 +125,17 @@ public void testValidate() throws IOException { assertEquals(1, validationErrors.validationErrors().size()); assertEquals("[size] cannot be [0] in a scroll context", validationErrors.validationErrors().get(0)); } + { + // Rescore is not allowed on scroll requests + SearchRequest searchRequest = createSearchRequest().source(new SearchSourceBuilder()); + searchRequest.source().addRescorer(new QueryRescorerBuilder(QueryBuilders.matchAllQuery())); + searchRequest.requestCache(false); + searchRequest.scroll(new TimeValue(1000)); + ActionRequestValidationException validationErrors = searchRequest.validate(); + assertNotNull(validationErrors); + assertEquals(1, validationErrors.validationErrors().size()); + assertEquals("using [rescore] is not allowed in a scroll context", validationErrors.validationErrors().get(0)); + } } public void testEqualsAndHashcode() throws IOException { From 5697d93cbf078dca7a112aa02e222504a492a861 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 28 Aug 2018 20:53:31 +0100 Subject: [PATCH 200/283] HLRC: Move ML protocol classes into client ml package (#33203) * HLRC: Move ML protocol classes into client ml package * Do not use log4j deprecation handler * JavaDoc should refer to correct Job path --- .../client/MLRequestConverters.java | 12 +++---- .../client/MachineLearningClient.java | 32 ++++++++--------- .../client}/ml/AbstractResultResponse.java | 2 +- .../client}/ml/CloseJobRequest.java | 2 +- .../client}/ml/CloseJobResponse.java | 2 +- .../client}/ml/DeleteJobRequest.java | 2 +- .../client}/ml/DeleteJobResponse.java | 2 +- .../client}/ml/GetBucketsRequest.java | 8 ++--- .../client}/ml/GetBucketsResponse.java | 4 +-- .../client}/ml/GetJobRequest.java | 7 ++-- .../client}/ml/GetJobResponse.java | 4 +-- .../client}/ml/OpenJobRequest.java | 7 ++-- .../client}/ml/OpenJobResponse.java | 2 +- .../client}/ml/PutJobRequest.java | 4 +-- .../client}/ml/PutJobResponse.java | 7 ++-- .../client}/ml/datafeed/ChunkingConfig.java | 2 +- .../client}/ml/datafeed/DatafeedConfig.java | 4 +-- .../client}/ml/datafeed/DatafeedUpdate.java | 4 +-- .../client}/ml/job/config/AnalysisConfig.java | 2 +- .../client}/ml/job/config/AnalysisLimits.java | 2 +- .../config/CategorizationAnalyzerConfig.java | 2 +- .../ml/job/config/DataDescription.java | 2 +- .../config/DefaultDetectorDescription.java | 2 +- .../client}/ml/job/config/DetectionRule.java | 2 +- .../client}/ml/job/config/Detector.java | 2 +- .../ml/job/config/DetectorFunction.java | 2 +- .../client}/ml/job/config/FilterRef.java | 2 +- .../client}/ml/job/config/Job.java | 4 +-- .../client}/ml/job/config/MlFilter.java | 2 +- .../ml/job/config/ModelPlotConfig.java | 2 +- .../client}/ml/job/config/Operator.java | 2 +- .../client}/ml/job/config/RuleAction.java | 2 +- .../client}/ml/job/config/RuleCondition.java | 2 +- .../client}/ml/job/config/RuleScope.java | 15 ++++++-- .../client}/ml/job/process/DataCounts.java | 6 ++-- .../ml/job/process/ModelSizeStats.java | 8 ++--- .../client}/ml/job/process/ModelSnapshot.java | 6 ++-- .../client}/ml/job/process/Quantiles.java | 4 +-- .../client}/ml/job/results/AnomalyCause.java | 2 +- .../client}/ml/job/results/AnomalyRecord.java | 4 +-- .../client}/ml/job/results/Bucket.java | 4 +-- .../ml/job/results/BucketInfluencer.java | 4 +-- .../ml/job/results/CategoryDefinition.java | 4 +-- .../client}/ml/job/results/Influence.java | 2 +- .../client}/ml/job/results/Influencer.java | 4 +-- .../client}/ml/job/results/OverallBucket.java | 4 +-- .../client}/ml/job/results/Result.java | 2 +- .../client}/ml/job/util/PageParams.java | 2 +- .../client}/ml/job/util/TimeUtil.java | 2 +- .../client/MLRequestConvertersTests.java | 20 +++++------ .../client/MachineLearningGetResultsIT.java | 12 +++---- .../client/MachineLearningIT.java | 28 +++++++-------- .../MlClientDocumentationIT.java | 36 +++++++++---------- .../client}/ml/CloseJobRequestTests.java | 2 +- .../client}/ml/CloseJobResponseTests.java | 2 +- .../client}/ml/DeleteJobRequestTests.java | 4 +-- .../client}/ml/DeleteJobResponseTests.java | 2 +- .../client}/ml/GetBucketsRequestTests.java | 4 +-- .../client}/ml/GetBucketsResponseTests.java | 6 ++-- .../client}/ml/GetJobRequestTests.java | 2 +- .../client}/ml/GetJobResponseTests.java | 6 ++-- .../client}/ml/OpenJobRequestTests.java | 4 +-- .../client}/ml/OpenJobResponseTests.java | 2 +- .../client}/ml/PutJobRequestTests.java | 6 ++-- .../client}/ml/PutJobResponseTests.java | 4 +-- .../ml/datafeed/ChunkingConfigTests.java | 2 +- .../ml/datafeed/DatafeedConfigTests.java | 2 +- .../ml/datafeed/DatafeedUpdateTests.java | 2 +- .../ml/job/config/AnalysisConfigTests.java | 2 +- .../ml/job/config/AnalysisLimitsTests.java | 2 +- .../CategorizationAnalyzerConfigTests.java | 2 +- .../ml/job/config/DataDescriptionTests.java | 4 +-- .../ml/job/config/DetectionRuleTests.java | 2 +- .../client}/ml/job/config/DetectorTests.java | 2 +- .../client}/ml/job/config/FilterRefTests.java | 2 +- .../client}/ml/job/config/JobTests.java | 2 +- .../client}/ml/job/config/MlFilterTests.java | 2 +- .../ml/job/config/ModelPlotConfigTests.java | 2 +- .../ml/job/config/RuleConditionTests.java | 2 +- .../client}/ml/job/config/RuleScopeTests.java | 2 +- .../ml/job/process/DataCountsTests.java | 4 +-- .../ml/job/process/ModelSizeStatsTests.java | 5 +-- .../ml/job/process/ModelSnapshotTests.java | 2 +- .../ml/job/process/QuantilesTests.java | 2 +- .../ml/job/results/AnomalyCauseTests.java | 2 +- .../ml/job/results/AnomalyRecordTests.java | 2 +- .../ml/job/results/BucketInfluencerTests.java | 2 +- .../client}/ml/job/results/BucketTests.java | 2 +- .../job/results/CategoryDefinitionTests.java | 2 +- .../ml/job/results/InfluenceTests.java | 2 +- .../ml/job/results/InfluencerTests.java | 2 +- .../ml/job/results/OverallBucketTests.java | 2 +- .../client}/ml/util/PageParamsTests.java | 4 +-- .../protocol/xpack/ml/package-info.java | 24 ------------- 94 files changed, 213 insertions(+), 228 deletions(-) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/AbstractResultResponse.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/CloseJobRequest.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/CloseJobResponse.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/DeleteJobRequest.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/DeleteJobResponse.java (97%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/GetBucketsRequest.java (97%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/GetBucketsResponse.java (95%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/GetJobRequest.java (93%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/GetJobResponse.java (96%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/OpenJobRequest.java (93%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/OpenJobResponse.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/PutJobRequest.java (95%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/PutJobResponse.java (88%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/datafeed/ChunkingConfig.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/datafeed/DatafeedConfig.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/datafeed/DatafeedUpdate.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/AnalysisConfig.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/AnalysisLimits.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/CategorizationAnalyzerConfig.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/DataDescription.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/DefaultDetectorDescription.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/DetectionRule.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/Detector.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/DetectorFunction.java (97%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/FilterRef.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/Job.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/MlFilter.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/ModelPlotConfig.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/Operator.java (97%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/RuleAction.java (95%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/RuleCondition.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/config/RuleScope.java (89%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/process/DataCounts.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/process/ModelSizeStats.java (97%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/process/ModelSnapshot.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/process/Quantiles.java (96%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/AnomalyCause.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/AnomalyRecord.java (99%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/Bucket.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/BucketInfluencer.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/CategoryDefinition.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/Influence.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/Influencer.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/OverallBucket.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/results/Result.java (95%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/util/PageParams.java (98%) rename {x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/main/java/org/elasticsearch/client}/ml/job/util/TimeUtil.java (97%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/CloseJobRequestTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/CloseJobResponseTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/DeleteJobRequestTests.java (93%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/DeleteJobResponseTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/GetBucketsRequestTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/GetBucketsResponseTests.java (90%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/GetJobRequestTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/GetJobResponseTests.java (90%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/OpenJobRequestTests.java (93%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/OpenJobResponseTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/PutJobRequestTests.java (89%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/PutJobResponseTests.java (92%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/datafeed/ChunkingConfigTests.java (97%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/datafeed/DatafeedConfigTests.java (99%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/datafeed/DatafeedUpdateTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/AnalysisConfigTests.java (99%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/AnalysisLimitsTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/CategorizationAnalyzerConfigTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/DataDescriptionTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/DetectionRuleTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/DetectorTests.java (99%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/FilterRefTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/JobTests.java (99%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/MlFilterTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/ModelPlotConfigTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/RuleConditionTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/config/RuleScopeTests.java (97%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/process/DataCountsTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/process/ModelSizeStatsTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/process/ModelSnapshotTests.java (99%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/process/QuantilesTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/AnomalyCauseTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/AnomalyRecordTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/BucketInfluencerTests.java (99%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/BucketTests.java (99%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/CategoryDefinitionTests.java (98%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/InfluenceTests.java (96%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/InfluencerTests.java (97%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/job/results/OverallBucketTests.java (97%) rename {x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack => client/rest-high-level/src/test/java/org/elasticsearch/client}/ml/util/PageParamsTests.java (92%) delete mode 100644 x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/package-info.java 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 6c1cc2057010..3a8fcd534ab6 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 @@ -24,13 +24,13 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.RequestConverters.EndpointBuilder; +import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteJobRequest; +import org.elasticsearch.client.ml.GetBucketsRequest; +import org.elasticsearch.client.ml.GetJobRequest; +import org.elasticsearch.client.ml.OpenJobRequest; +import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.common.Strings; -import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; -import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; -import org.elasticsearch.protocol.xpack.ml.GetJobRequest; -import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; -import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; -import org.elasticsearch.protocol.xpack.ml.PutJobRequest; import java.io.IOException; 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 c4dcc1eaffc5..34ad9c0d81a4 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,18 +19,18 @@ package org.elasticsearch.client; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; -import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; -import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; -import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; -import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; -import org.elasticsearch.protocol.xpack.ml.GetBucketsResponse; -import org.elasticsearch.protocol.xpack.ml.GetJobRequest; -import org.elasticsearch.protocol.xpack.ml.GetJobResponse; -import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; -import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; -import org.elasticsearch.protocol.xpack.ml.PutJobRequest; -import org.elasticsearch.protocol.xpack.ml.PutJobResponse; +import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteJobRequest; +import org.elasticsearch.client.ml.DeleteJobResponse; +import org.elasticsearch.client.ml.GetBucketsRequest; +import org.elasticsearch.client.ml.GetBucketsResponse; +import org.elasticsearch.client.ml.GetJobRequest; +import org.elasticsearch.client.ml.GetJobResponse; +import org.elasticsearch.client.ml.OpenJobRequest; +import org.elasticsearch.client.ml.OpenJobResponse; +import org.elasticsearch.client.ml.PutJobRequest; +import org.elasticsearch.client.ml.PutJobResponse; import java.io.IOException; import java.util.Collections; @@ -56,9 +56,9 @@ public final class MachineLearningClient { * For additional info * see ML PUT job documentation * - * @param request The PutJobRequest containing the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} settings + * @param request The PutJobRequest containing the {@link org.elasticsearch.client.ml.job.config.Job} settings * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return PutJobResponse with enclosed {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} object + * @return PutJobResponse with enclosed {@link org.elasticsearch.client.ml.job.config.Job} object * @throws IOException when there is a serialization issue sending the request or receiving the response */ public PutJobResponse putJob(PutJobRequest request, RequestOptions options) throws IOException { @@ -75,7 +75,7 @@ public PutJobResponse putJob(PutJobRequest request, RequestOptions options) thro * For additional info * see ML PUT job documentation * - * @param request The request containing the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} settings + * @param request The request containing the {@link org.elasticsearch.client.ml.job.config.Job} 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 */ @@ -98,7 +98,7 @@ public void putJobAsync(PutJobRequest request, RequestOptions options, ActionLis * @param request {@link GetJobRequest} Request containing a list of jobId(s) and additional options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return {@link GetJobResponse} response object containing - * the {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} objects and the number of jobs found + * the {@link org.elasticsearch.client.ml.job.config.Job} objects and the number of jobs found * @throws IOException when there is a serialization issue sending the request or receiving the response */ public GetJobResponse getJob(GetJobRequest request, RequestOptions options) throws IOException { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/AbstractResultResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/AbstractResultResponse.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/AbstractResultResponse.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/AbstractResultResponse.java index 64f350933c9c..1b609797dd6f 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/AbstractResultResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/AbstractResultResponse.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/CloseJobRequest.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/CloseJobRequest.java index 38f924163061..19f3df8e4320 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/CloseJobRequest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/CloseJobResponse.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/CloseJobResponse.java index 1b8ff3ca7d4d..2ac1e0faee34 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/CloseJobResponse.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobRequest.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobRequest.java index 9f265fd20a8c..a355f7ec659b 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobRequest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java similarity index 97% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java index 795eb784aaff..86cafd9e0931 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.common.xcontent.XContentParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetBucketsRequest.java similarity index 97% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequest.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetBucketsRequest.java index 4957f9b6ff6e..f50d92d58dda 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetBucketsRequest.java @@ -16,17 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +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.results.Result; +import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.results.Result; -import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import java.io.IOException; import java.util.Objects; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetBucketsResponse.java similarity index 95% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponse.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetBucketsResponse.java index 4350661f68b3..de8736b86d92 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetBucketsResponse.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.results.Bucket; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; import java.io.IOException; import java.util.List; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobRequest.java similarity index 93% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobRequest.java index c3c14726505c..3de7037e5c8f 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobRequest.java @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +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.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; @@ -32,7 +33,7 @@ import java.util.Objects; /** - * Request object to get {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} objects with the matching `jobId`s or + * Request object to get {@link Job} objects with the matching `jobId`s or * `groupName`s. * * `_all` explicitly gets all the jobs in the cluster @@ -66,7 +67,7 @@ public static GetJobRequest getAllJobsRequest() { } /** - * Get the specified {@link org.elasticsearch.protocol.xpack.ml.job.config.Job} configurations via their unique jobIds + * Get the specified {@link Job} configurations via their unique jobIds * @param jobIds must not contain any null values */ public GetJobRequest(String... jobIds) { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobResponse.java similarity index 96% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobResponse.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobResponse.java index 4db542dc1526..0cdf08c6c24a 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/GetJobResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobResponse.java @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.util.List; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/OpenJobRequest.java similarity index 93% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequest.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/OpenJobRequest.java index 658c7d38503e..5b8e68cd72dc 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/OpenJobRequest.java @@ -16,19 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +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.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.util.Objects; @@ -95,7 +94,7 @@ public ActionRequestValidationException validate() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(Job.ID.getPreferredName(), jobId); if (timeout != null) { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/OpenJobResponse.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponse.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/OpenJobResponse.java index 3a1e47798043..2536aeeaf78b 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/OpenJobResponse.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutJobRequest.java similarity index 95% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobRequest.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutJobRequest.java index bc3fd778c1bd..de8529de6bb8 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutJobRequest.java @@ -16,14 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +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.Strings; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.util.Objects; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutJobResponse.java similarity index 88% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutJobResponse.java index 3fa1b30dd3eb..6e6cce52e58c 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/PutJobResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutJobResponse.java @@ -16,13 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; -import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.util.Objects; @@ -47,7 +46,7 @@ public Job getResponse() { } @Override - public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { job.toXContent(builder, params); return builder; } diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/ChunkingConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/ChunkingConfig.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/ChunkingConfig.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/ChunkingConfig.java index 0b9d9f120461..10e7b3f97494 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/ChunkingConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/ChunkingConfig.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.datafeed; +package org.elasticsearch.client.ml.datafeed; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedConfig.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedConfig.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedConfig.java index 929d4dacb90f..752752b10388 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedConfig.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.datafeed; +package org.elasticsearch.client.ml.datafeed; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -27,7 +28,6 @@ import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.builder.SearchSourceBuilder; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedUpdate.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdate.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedUpdate.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdate.java index 787bdf06e5ec..184d5d51481f 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedUpdate.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdate.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.datafeed; +package org.elasticsearch.client.ml.datafeed; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -26,7 +27,6 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.builder.SearchSourceBuilder; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/AnalysisConfig.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfig.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/AnalysisConfig.java index 7baaae52a8bf..9b759599dda3 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/AnalysisConfig.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.unit.TimeValue; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisLimits.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/AnalysisLimits.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisLimits.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/AnalysisLimits.java index f69b9ccbf9ff..22d26f06fd8c 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisLimits.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/AnalysisLimits.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/CategorizationAnalyzerConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/CategorizationAnalyzerConfig.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/CategorizationAnalyzerConfig.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/CategorizationAnalyzerConfig.java index dc7f047b8040..3a2243d6548f 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/CategorizationAnalyzerConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/CategorizationAnalyzerConfig.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DataDescription.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DataDescription.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DataDescription.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DataDescription.java index a3f8c2563b2d..636b8c6ad501 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DataDescription.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DataDescription.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DefaultDetectorDescription.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DefaultDetectorDescription.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DefaultDetectorDescription.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DefaultDetectorDescription.java index 081e685fc741..25b4fbbb2a7e 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DefaultDetectorDescription.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DefaultDetectorDescription.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.Strings; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectionRule.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DetectionRule.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectionRule.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DetectionRule.java index 9a73afe885b1..bcba8a7d74a6 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectionRule.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DetectionRule.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Detector.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Detector.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Detector.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Detector.java index 042d48b70068..e1af60269b52 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Detector.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Detector.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectorFunction.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DetectorFunction.java similarity index 97% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectorFunction.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DetectorFunction.java index 5d9a06948d0f..932782101ba7 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectorFunction.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/DetectorFunction.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import java.util.Arrays; import java.util.Collections; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/FilterRef.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/FilterRef.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/FilterRef.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/FilterRef.java index 9afbdf4876fd..b686ad92ae53 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/FilterRef.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/FilterRef.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Job.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Job.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Job.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Job.java index 59840cfec2ae..aff74271f1c0 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Job.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Job.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; +import org.elasticsearch.client.ml.job.util.TimeUtil; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.unit.TimeValue; @@ -25,7 +26,6 @@ import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.util.TimeUtil; import java.io.IOException; import java.util.Collections; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/MlFilter.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/MlFilter.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/MlFilter.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/MlFilter.java index bcbc0c295c2d..e0d1bd0849b3 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/MlFilter.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/MlFilter.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/ModelPlotConfig.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/ModelPlotConfig.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/ModelPlotConfig.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/ModelPlotConfig.java index 59b0252a7660..b39db054b308 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/ModelPlotConfig.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/ModelPlotConfig.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Operator.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Operator.java similarity index 97% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Operator.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Operator.java index c3dc52e5a3cb..37d627520356 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/Operator.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/Operator.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleAction.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleAction.java similarity index 95% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleAction.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleAction.java index 9e2364b4fd96..05b6ef6e1975 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleAction.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleAction.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import java.util.Locale; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleCondition.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleCondition.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleCondition.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleCondition.java index ec19547fe13b..14389809bd2f 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleCondition.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleCondition.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleScope.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleScope.java similarity index 89% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleScope.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleScope.java index aa12d5ea2a2b..8b6886d58252 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleScope.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleScope.java @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ContextParser; -import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -50,7 +50,7 @@ public static ContextParser parser() { Map value = (Map) entry.getValue(); builder.map(value); try (XContentParser scopeParser = XContentFactory.xContent(builder.contentType()).createParser( - NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, Strings.toString(builder))) { + NamedXContentRegistry.EMPTY, DEPRECATION_HANDLER, Strings.toString(builder))) { scope.put(entry.getKey(), FilterRef.PARSER.parse(scopeParser, null)); } } @@ -59,6 +59,15 @@ public static ContextParser parser() { }; } + private static final DeprecationHandler DEPRECATION_HANDLER = new DeprecationHandler() { + + @Override + public void usedDeprecatedName(String usedName, String modernName) {} + + @Override + public void usedDeprecatedField(String usedName, String replacedWith) {} + }; + private final Map scope; public RuleScope() { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/DataCounts.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/DataCounts.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/DataCounts.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/DataCounts.java index e07312d12e1f..7afef0785fe3 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/DataCounts.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/DataCounts.java @@ -16,15 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.util.TimeUtil; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.util.TimeUtil; import java.io.IOException; import java.util.Date; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSizeStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/ModelSizeStats.java similarity index 97% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSizeStats.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/ModelSizeStats.java index 50f655b4dd7f..c9a34fe5c98d 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSizeStats.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/ModelSizeStats.java @@ -16,16 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.results.Result; +import org.elasticsearch.client.ml.job.util.TimeUtil; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.results.Result; -import org.elasticsearch.protocol.xpack.ml.job.util.TimeUtil; import java.io.IOException; import java.util.Date; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshot.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/ModelSnapshot.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshot.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/ModelSnapshot.java index ea5f01699310..603bff0d9065 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshot.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/ModelSnapshot.java @@ -16,16 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; import org.elasticsearch.Version; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.util.TimeUtil; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.util.TimeUtil; import java.io.IOException; import java.util.Date; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/Quantiles.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/Quantiles.java similarity index 96% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/Quantiles.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/Quantiles.java index 1c047d6c3028..795028847a0b 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/process/Quantiles.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/process/Quantiles.java @@ -16,14 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.util.Date; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyCause.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyCause.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyCause.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyCause.java index 7ad57b24fcbd..4fbe5ac1ff38 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyCause.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyCause.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyRecord.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyRecord.java similarity index 99% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyRecord.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyRecord.java index 4747f3a48bdc..db4483fef4bf 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyRecord.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyRecord.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -25,7 +26,6 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser.Token; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.time.format.DateTimeFormatter; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Bucket.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Bucket.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Bucket.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Bucket.java index cbaf83abbad4..2dfed4c38340 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Bucket.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Bucket.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -25,7 +26,6 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser.Token; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.time.format.DateTimeFormatter; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketInfluencer.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/BucketInfluencer.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketInfluencer.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/BucketInfluencer.java index 29d8447cd6a3..6fc2a9b8b2d5 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketInfluencer.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/BucketInfluencer.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -25,7 +26,6 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser.Token; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.time.format.DateTimeFormatter; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/CategoryDefinition.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/CategoryDefinition.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/CategoryDefinition.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/CategoryDefinition.java index 59b59006b33a..dd65899e67e1 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/CategoryDefinition.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/CategoryDefinition.java @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.util.ArrayList; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Influence.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Influence.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Influence.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Influence.java index 53607479d66f..bfcc545362d3 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Influence.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Influence.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.ConstructingObjectParser; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Influencer.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Influencer.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Influencer.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Influencer.java index 51c88883608b..28ceb243bf6b 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Influencer.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Influencer.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -25,7 +26,6 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser.Token; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.time.format.DateTimeFormatter; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/OverallBucket.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/OverallBucket.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/OverallBucket.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/OverallBucket.java index 4f13b4b26646..eaf050f8be9f 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/OverallBucket.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/OverallBucket.java @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.Job; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.xcontent.ConstructingObjectParser; @@ -25,7 +26,6 @@ import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import java.io.IOException; import java.time.format.DateTimeFormatter; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Result.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Result.java similarity index 95% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Result.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Result.java index cce5fa65ebb4..a7f8933a0a13 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/results/Result.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/Result.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/PageParams.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/util/PageParams.java similarity index 98% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/PageParams.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/util/PageParams.java index 2e20e84d7b81..52d54188f700 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/PageParams.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/util/PageParams.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.util; +package org.elasticsearch.client.ml.job.util; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/TimeUtil.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/util/TimeUtil.java similarity index 97% rename from x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/TimeUtil.java rename to client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/util/TimeUtil.java index 549b19694914..4c21ffb2175b 100644 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/job/util/TimeUtil.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/util/TimeUtil.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.util; +package org.elasticsearch.client.ml.job.util; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.xcontent.XContentParser; 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 9065cda9cd6f..43f3ef41a8d7 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 @@ -23,19 +23,19 @@ 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.DeleteJobRequest; +import org.elasticsearch.client.ml.GetBucketsRequest; +import org.elasticsearch.client.ml.GetJobRequest; +import org.elasticsearch.client.ml.OpenJobRequest; +import org.elasticsearch.client.ml.PutJobRequest; +import org.elasticsearch.client.ml.job.config.AnalysisConfig; +import org.elasticsearch.client.ml.job.config.Detector; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; -import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; -import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; -import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; -import org.elasticsearch.protocol.xpack.ml.GetJobRequest; -import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; -import org.elasticsearch.protocol.xpack.ml.PutJobRequest; -import org.elasticsearch.protocol.xpack.ml.job.config.AnalysisConfig; -import org.elasticsearch.protocol.xpack.ml.job.config.Detector; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import org.elasticsearch.test.ESTestCase; import java.io.ByteArrayOutputStream; 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 a4f83c347ad1..4b3d22b451da 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 @@ -21,13 +21,13 @@ import org.elasticsearch.action.bulk.BulkRequest; 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.PutJobRequest; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.results.Bucket; +import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; -import org.elasticsearch.protocol.xpack.ml.GetBucketsResponse; -import org.elasticsearch.protocol.xpack.ml.PutJobRequest; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; -import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import org.junit.After; import org.junit.Before; 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 cec5dd7ccf8f..cb9dbea129d2 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 @@ -19,21 +19,21 @@ package org.elasticsearch.client; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; +import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteJobRequest; +import org.elasticsearch.client.ml.DeleteJobResponse; +import org.elasticsearch.client.ml.GetJobRequest; +import org.elasticsearch.client.ml.GetJobResponse; +import org.elasticsearch.client.ml.OpenJobRequest; +import org.elasticsearch.client.ml.OpenJobResponse; +import org.elasticsearch.client.ml.PutJobRequest; +import org.elasticsearch.client.ml.PutJobResponse; +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.common.unit.TimeValue; -import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; -import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; -import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; -import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; -import org.elasticsearch.protocol.xpack.ml.GetJobRequest; -import org.elasticsearch.protocol.xpack.ml.GetJobResponse; -import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; -import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; -import org.elasticsearch.protocol.xpack.ml.PutJobRequest; -import org.elasticsearch.protocol.xpack.ml.PutJobResponse; -import org.elasticsearch.protocol.xpack.ml.job.config.AnalysisConfig; -import org.elasticsearch.protocol.xpack.ml.job.config.DataDescription; -import org.elasticsearch.protocol.xpack.ml.job.config.Detector; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; import org.junit.After; import java.io.IOException; 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 683f91dae2eb..8e86ffb4d641 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 @@ -27,26 +27,26 @@ import org.elasticsearch.client.MlRestTestStateCleaner; 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.DeleteJobRequest; +import org.elasticsearch.client.ml.DeleteJobResponse; +import org.elasticsearch.client.ml.GetBucketsRequest; +import org.elasticsearch.client.ml.GetBucketsResponse; +import org.elasticsearch.client.ml.GetJobRequest; +import org.elasticsearch.client.ml.GetJobResponse; +import org.elasticsearch.client.ml.OpenJobRequest; +import org.elasticsearch.client.ml.OpenJobResponse; +import org.elasticsearch.client.ml.PutJobRequest; +import org.elasticsearch.client.ml.PutJobResponse; +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.job.results.Bucket; +import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.protocol.xpack.ml.CloseJobRequest; -import org.elasticsearch.protocol.xpack.ml.CloseJobResponse; -import org.elasticsearch.protocol.xpack.ml.DeleteJobRequest; -import org.elasticsearch.protocol.xpack.ml.DeleteJobResponse; -import org.elasticsearch.protocol.xpack.ml.GetBucketsRequest; -import org.elasticsearch.protocol.xpack.ml.GetBucketsResponse; -import org.elasticsearch.protocol.xpack.ml.GetJobRequest; -import org.elasticsearch.protocol.xpack.ml.GetJobResponse; -import org.elasticsearch.protocol.xpack.ml.OpenJobRequest; -import org.elasticsearch.protocol.xpack.ml.OpenJobResponse; -import org.elasticsearch.protocol.xpack.ml.PutJobRequest; -import org.elasticsearch.protocol.xpack.ml.PutJobResponse; -import org.elasticsearch.protocol.xpack.ml.job.config.AnalysisConfig; -import org.elasticsearch.protocol.xpack.ml.job.config.DataDescription; -import org.elasticsearch.protocol.xpack.ml.job.config.Detector; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; -import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import org.junit.After; import java.io.IOException; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/CloseJobRequestTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/CloseJobRequestTests.java index 435504b52983..cf5f5ca3c0fb 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/CloseJobRequestTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/CloseJobResponseTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/CloseJobResponseTests.java index d161fde536ec..04389a3af7e1 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/CloseJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/CloseJobResponseTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobRequestTests.java similarity index 93% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequestTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobRequestTests.java index fb8a38fa0c68..d3ccb98eeb68 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobRequestTests.java @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; -import org.elasticsearch.protocol.xpack.ml.job.config.JobTests; +import org.elasticsearch.client.ml.job.config.JobTests; import org.elasticsearch.test.ESTestCase; public class DeleteJobRequestTests extends ESTestCase { diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java index a73179a08983..2eb4d51e1918 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/DeleteJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetBucketsRequestTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequestTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetBucketsRequestTests.java index 6364ad339b12..d63798869122 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetBucketsRequestTests.java @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetBucketsResponseTests.java similarity index 90% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetBucketsResponseTests.java index 889c3e93bc70..7b1934c2dfac 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetBucketsResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetBucketsResponseTests.java @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.results.Bucket; +import org.elasticsearch.client.ml.job.results.BucketTests; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.results.Bucket; -import org.elasticsearch.protocol.xpack.ml.job.results.BucketTests; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobRequestTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobRequestTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobRequestTests.java index b94b704fbf6e..77b2109dd7c6 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobRequestTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobResponseTests.java similarity index 90% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobResponseTests.java index 79d4d678b929..181804c9676f 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/GetJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobResponseTests.java @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.config.JobTests; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.config.JobTests; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/OpenJobRequestTests.java similarity index 93% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequestTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/OpenJobRequestTests.java index 242f0cf4e8a5..c6ce34364462 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/OpenJobRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/OpenJobRequestTests.java @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.config.JobTests; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.JobTests; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/OpenJobResponseTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/OpenJobResponseTests.java index aadfb236d3a9..7f177c6e1ef8 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/OpenJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/OpenJobResponseTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/PutJobRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutJobRequestTests.java similarity index 89% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/PutJobRequestTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutJobRequestTests.java index 165934224b90..b58d849de1fb 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/PutJobRequestTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutJobRequestTests.java @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.config.JobTests; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.Job; -import org.elasticsearch.protocol.xpack.ml.job.config.JobTests; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/PutJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutJobResponseTests.java similarity index 92% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/PutJobResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutJobResponseTests.java index ed91e33635b2..1f435783d0f1 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/PutJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutJobResponseTests.java @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml; +package org.elasticsearch.client.ml; +import org.elasticsearch.client.ml.job.config.JobTests; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.JobTests; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/ChunkingConfigTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/ChunkingConfigTests.java similarity index 97% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/ChunkingConfigTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/ChunkingConfigTests.java index c835788bb1c9..c1c0daaa9384 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/ChunkingConfigTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/ChunkingConfigTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.datafeed; +package org.elasticsearch.client.ml.datafeed; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedConfigTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedConfigTests.java similarity index 99% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedConfigTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedConfigTests.java index f45d88d318e0..462eb1466511 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedConfigTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedConfigTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.datafeed; +package org.elasticsearch.client.ml.datafeed; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.elasticsearch.common.settings.Settings; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedUpdateTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdateTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedUpdateTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdateTests.java index edbef8461e05..3dddad3c0167 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/datafeed/DatafeedUpdateTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/datafeed/DatafeedUpdateTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.datafeed; +package org.elasticsearch.client.ml.datafeed; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfigTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/AnalysisConfigTests.java similarity index 99% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfigTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/AnalysisConfigTests.java index 34f12fc067e7..7b76688f4d31 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisConfigTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/AnalysisConfigTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisLimitsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/AnalysisLimitsTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisLimitsTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/AnalysisLimitsTests.java index 5003da10780d..cb14d19300d9 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/AnalysisLimitsTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/AnalysisLimitsTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/CategorizationAnalyzerConfigTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/CategorizationAnalyzerConfigTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/CategorizationAnalyzerConfigTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/CategorizationAnalyzerConfigTests.java index 36fb51ed10e7..889926e00d6f 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/CategorizationAnalyzerConfigTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/CategorizationAnalyzerConfigTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DataDescriptionTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DataDescriptionTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DataDescriptionTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DataDescriptionTests.java index 8ca2dc494f3c..9c1f361ce0e8 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DataDescriptionTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DataDescriptionTests.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.config.DataDescription.DataFormat; import org.elasticsearch.test.AbstractXContentTestCase; +import static org.elasticsearch.client.ml.job.config.DataDescription.DataFormat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectionRuleTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DetectionRuleTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectionRuleTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DetectionRuleTests.java index bc70a404894a..32c6ca426ca0 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectionRuleTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DetectionRuleTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectorTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DetectorTests.java similarity index 99% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectorTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DetectorTests.java index 0b1ba892acd3..7801447e724f 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/DetectorTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/DetectorTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/FilterRefTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/FilterRefTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/FilterRefTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/FilterRefTests.java index 00862e5307b9..cdc79b83524a 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/FilterRefTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/FilterRefTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/JobTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/JobTests.java similarity index 99% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/JobTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/JobTests.java index 61931743403e..1946f70a230d 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/JobTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/JobTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.elasticsearch.common.settings.Settings; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/MlFilterTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/MlFilterTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/MlFilterTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/MlFilterTests.java index 6c595e2d6da1..5e218a8dce7d 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/MlFilterTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/MlFilterTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/ModelPlotConfigTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/ModelPlotConfigTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/ModelPlotConfigTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/ModelPlotConfigTests.java index 23f13c732123..50f1b49f4144 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/ModelPlotConfigTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/ModelPlotConfigTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleConditionTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/RuleConditionTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleConditionTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/RuleConditionTests.java index 4348ea194d01..3386d3fdc52e 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleConditionTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/RuleConditionTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleScopeTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/RuleScopeTests.java similarity index 97% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleScopeTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/RuleScopeTests.java index ac97e457ac47..2231b913251e 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/config/RuleScopeTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/config/RuleScopeTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.config; +package org.elasticsearch.client.ml.job.config; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/DataCountsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/DataCountsTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/DataCountsTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/DataCountsTests.java index 2232e8c88d92..7c261e8d4c91 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/DataCountsTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/DataCountsTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; @@ -113,7 +113,7 @@ private void assertAllFieldsGreaterThanZero(DataCounts stats) throws Exception { private static DataCounts createCounts( long processedRecordCount, long processedFieldCount, long inputBytes, long inputFieldCount, - long invalidDateCount, long missingFieldCount, long outOfOrderTimeStampCount, + long invalidDateCount, long missingFieldCount, long outOfOrderTimeStampCount, long emptyBucketCount, long sparseBucketCount, long bucketCount, long earliestRecordTime, long latestRecordTime, long lastDataTimeStamp, long latestEmptyBucketTimeStamp, long latestSparseBucketTimeStamp) { diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSizeStatsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/ModelSizeStatsTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSizeStatsTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/ModelSizeStatsTests.java index e3341123fb00..4a12a75f2b17 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSizeStatsTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/ModelSizeStatsTests.java @@ -16,15 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; -import org.elasticsearch.protocol.xpack.ml.job.process.ModelSizeStats.MemoryStatus; import java.util.Date; +import static org.elasticsearch.client.ml.job.process.ModelSizeStats.MemoryStatus; + public class ModelSizeStatsTests extends AbstractXContentTestCase { public void testDefaultConstructor() { diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshotTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/ModelSnapshotTests.java similarity index 99% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshotTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/ModelSnapshotTests.java index 8c6a9bd83c91..9669f9bfa4f4 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/ModelSnapshotTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/ModelSnapshotTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; import org.elasticsearch.Version; import org.elasticsearch.common.unit.TimeValue; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/QuantilesTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/QuantilesTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/QuantilesTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/QuantilesTests.java index 77ae21bc6f89..24c70f6d68f6 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/process/QuantilesTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/process/QuantilesTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.process; +package org.elasticsearch.client.ml.job.process; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyCauseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyCauseTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyCauseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyCauseTests.java index 070b9f18b4dc..3ac6a0b6ec4d 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyCauseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyCauseTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyRecordTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyRecordTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java index d4cadb19796b..88abcea86377 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/AnomalyRecordTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketInfluencerTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketInfluencerTests.java similarity index 99% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketInfluencerTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketInfluencerTests.java index 7e4c166d1fd6..7b8ba1383984 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketInfluencerTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketInfluencerTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketTests.java similarity index 99% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketTests.java index 0eb988d8eb82..6a0a5d3c6444 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/BucketTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/CategoryDefinitionTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/CategoryDefinitionTests.java similarity index 98% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/CategoryDefinitionTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/CategoryDefinitionTests.java index 28ef4a5ecb24..27e15a1600d3 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/CategoryDefinitionTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/CategoryDefinitionTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/InfluenceTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/InfluenceTests.java similarity index 96% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/InfluenceTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/InfluenceTests.java index b029997d015f..89b2e5dbcbb6 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/InfluenceTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/InfluenceTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/InfluencerTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/InfluencerTests.java similarity index 97% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/InfluencerTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/InfluencerTests.java index 8125a1a5c725..ef83af39958d 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/InfluencerTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/InfluencerTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentHelper; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/OverallBucketTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/OverallBucketTests.java similarity index 97% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/OverallBucketTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/OverallBucketTests.java index babd7410d57c..9ee6a2025b69 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/job/results/OverallBucketTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/OverallBucketTests.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.job.results; +package org.elasticsearch.client.ml.job.results; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/util/PageParamsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/util/PageParamsTests.java similarity index 92% rename from x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/util/PageParamsTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/util/PageParamsTests.java index 6bd51e93c6f3..f74cedf1437d 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/ml/util/PageParamsTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/util/PageParamsTests.java @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.elasticsearch.protocol.xpack.ml.util; +package org.elasticsearch.client.ml.util; +import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.protocol.xpack.ml.job.util.PageParams; import org.elasticsearch.test.AbstractXContentTestCase; public class PageParamsTests extends AbstractXContentTestCase { diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/package-info.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/package-info.java deleted file mode 100644 index b1e4c6c0d4e5..000000000000 --- a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/ml/package-info.java +++ /dev/null @@ -1,24 +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. - */ - -/** - * Request and Response objects for the default distribution's Machine - * Learning APIs. - */ -package org.elasticsearch.protocol.xpack.ml; From 1cbde721dc1caf67f4903624549572e135cb365f Mon Sep 17 00:00:00 2001 From: lcawl Date: Tue, 28 Aug 2018 14:34:39 -0700 Subject: [PATCH 201/283] [DOCS] Fixes command page titles --- docs/reference/commands/certgen.asciidoc | 4 ++-- docs/reference/commands/saml-metadata.asciidoc | 2 +- docs/reference/commands/users-command.asciidoc | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/reference/commands/certgen.asciidoc b/docs/reference/commands/certgen.asciidoc index 3a8b15fbd28c..956a4637ed31 100644 --- a/docs/reference/commands/certgen.asciidoc +++ b/docs/reference/commands/certgen.asciidoc @@ -1,9 +1,9 @@ [role="xpack"] [testenv="gold+"] [[certgen]] -== certgen +== elasticsearch-certgen -deprecated[6.1,Replaced by <>.] +deprecated[6.1,Replaced by <>.] The `elasticsearch-certgen` command simplifies the creation of certificate authorities (CA), certificate signing requests (CSR), and signed certificates diff --git a/docs/reference/commands/saml-metadata.asciidoc b/docs/reference/commands/saml-metadata.asciidoc index 069c7135c014..5309f83288f8 100644 --- a/docs/reference/commands/saml-metadata.asciidoc +++ b/docs/reference/commands/saml-metadata.asciidoc @@ -1,7 +1,7 @@ [role="xpack"] [testenv="gold+"] [[saml-metadata]] -== saml-metadata +== elasticsearch-saml-metadata The `elasticsearch-saml-metadata` command can be used to generate a SAML 2.0 Service Provider Metadata file. diff --git a/docs/reference/commands/users-command.asciidoc b/docs/reference/commands/users-command.asciidoc index e53e0815c5d7..cf678f2138df 100644 --- a/docs/reference/commands/users-command.asciidoc +++ b/docs/reference/commands/users-command.asciidoc @@ -1,10 +1,7 @@ [role="xpack"] [testenv="gold+"] [[users-command]] -== Users Command -++++ -users -++++ +== elasticsearch-users If you use file-based user authentication, the `elasticsearch-users` command enables you to add and remove users, assign user roles, and manage passwords. From 1be3dd5504a362cef24a13dafb86d4d7db15fa26 Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Tue, 28 Aug 2018 17:23:41 -0500 Subject: [PATCH 202/283] HLRC: create base timed request class (#33216) There are many requests that allow the user to set a few timeouts on. This class will allow requests impl'd in HLRC to extend from, and allow users to set those values without significant work to add them to every request. --- .../elasticsearch/client/TimedRequest.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java new file mode 100644 index 000000000000..af8fbe3e72b3 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/TimedRequest.java @@ -0,0 +1,56 @@ +/* + * 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; + +import org.elasticsearch.common.unit.TimeValue; + +/** + * A base request for any requests that supply timeouts. + * + * Please note, any requests that use a ackTimeout should set timeout as they + * represent the same backing field on the server. + */ +public class TimedRequest implements Validatable { + + private TimeValue timeout; + private TimeValue masterTimeout; + + public void setTimeout(TimeValue timeout) { + this.timeout = timeout; + + } + + public void setMasterTimeout(TimeValue masterTimeout) { + this.masterTimeout = masterTimeout; + } + + /** + * Returns the request timeout + */ + public TimeValue timeout() { + return timeout; + } + + /** + * Returns the timeout for the request to be completed on the master node + */ + public TimeValue masterNodeTimeout() { + return masterTimeout; + } +} From 41c7fc8d375f89045f6a9c16bb8af6a2ce4af3e2 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 29 Aug 2018 10:54:58 +0700 Subject: [PATCH 203/283] [CCR] Introduce leader index name & last fetch time stats to stats api response (#33155) --- .../xpack/ccr/action/ShardFollowNodeTask.java | 77 ++++++++++++++++--- .../ShardFollowNodeTaskStatusTests.java | 6 +- .../rest-api-spec/test/ccr/stats.yml | 5 ++ 3 files changed, 76 insertions(+), 12 deletions(-) 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 80d6ed4cb4ab..00e3aaaae2a8 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 @@ -67,6 +67,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { private static final Logger LOGGER = Loggers.getLogger(ShardFollowNodeTask.class); + private final String leaderIndex; private final ShardFollowTask params; private final TimeValue retryTimeout; private final TimeValue idleShardChangesRequestDelay; @@ -90,6 +91,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { private long numberOfSuccessfulBulkOperations = 0; private long numberOfFailedBulkOperations = 0; private long numberOfOperationsIndexed = 0; + private long lastFetchTime = -1; private final Queue buffer = new PriorityQueue<>(Comparator.comparing(Translog.Operation::seqNo)); private final LinkedHashMap fetchExceptions; @@ -112,6 +114,12 @@ protected boolean removeEldestEntry(final Map.Entry params.getMaxConcurrentReadBatches(); } }; + + if (params.getLeaderClusterAlias() != null) { + leaderIndex = params.getLeaderClusterAlias() + ":" + params.getLeaderShardId().getIndexName(); + } else { + leaderIndex = params.getLeaderShardId().getIndexName(); + } } void start( @@ -235,6 +243,9 @@ private void sendShardChangesRequest(long from, int maxOperationCount, long maxR private void sendShardChangesRequest(long from, int maxOperationCount, long maxRequiredSeqNo, AtomicInteger retryCounter) { final long startTime = relativeTimeProvider.getAsLong(); + synchronized (this) { + lastFetchTime = startTime; + } innerSendShardChangesRequest(from, maxOperationCount, response -> { synchronized (ShardFollowNodeTask.this) { @@ -411,7 +422,15 @@ public ShardId getFollowShardId() { @Override public synchronized Status getStatus() { + final long timeSinceLastFetchMillis; + if (lastFetchTime != -1) { + timeSinceLastFetchMillis = TimeUnit.NANOSECONDS.toMillis(relativeTimeProvider.getAsLong() - lastFetchTime); + } else { + // To avoid confusion when ccr didn't yet execute a fetch: + timeSinceLastFetchMillis = -1; + } return new Status( + leaderIndex, getFollowShardId().getId(), leaderGlobalCheckpoint, leaderMaxSeqNo, @@ -431,13 +450,15 @@ public synchronized Status getStatus() { numberOfSuccessfulBulkOperations, numberOfFailedBulkOperations, numberOfOperationsIndexed, - new TreeMap<>(fetchExceptions)); + new TreeMap<>(fetchExceptions), + 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"); @@ -458,20 +479,21 @@ public static class Status implements Task.Status { 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( - (int) args[0], - (long) args[1], + (String) args[0], + (int) args[1], (long) args[2], (long) args[3], (long) args[4], (long) args[5], - (int) args[6], + (long) args[6], (int) args[7], (int) args[8], - (long) args[9], + (int) args[9], (long) args[10], (long) args[11], (long) args[12], @@ -481,10 +503,12 @@ public static class Status implements Task.Status { (long) args[16], (long) args[17], (long) args[18], + (long) args[19], new TreeMap<>( - ((List>) args[19]) + ((List>) args[20]) .stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))))); + .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"; @@ -494,6 +518,7 @@ public static class Status implements Task.Status { 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); @@ -514,6 +539,7 @@ public static class Status implements Task.Status { 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"); @@ -527,6 +553,12 @@ public static class Status implements Task.Status { FETCH_EXCEPTIONS_ENTRY_EXCEPTION); } + private final String leaderIndex; + + public String leaderIndex() { + return leaderIndex; + } + private final int shardId; public int getShardId() { @@ -647,7 +679,14 @@ 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, @@ -667,7 +706,9 @@ public NavigableMap fetchExceptions() { final long numberOfSuccessfulBulkOperations, final long numberOfFailedBulkOperations, final long numberOfOperationsIndexed, - final NavigableMap fetchExceptions) { + final NavigableMap fetchExceptions, + final long timeSinceLastFetchMillis) { + this.leaderIndex = leaderIndex; this.shardId = shardId; this.leaderGlobalCheckpoint = leaderGlobalCheckpoint; this.leaderMaxSeqNo = leaderMaxSeqNo; @@ -688,9 +729,11 @@ public NavigableMap fetchExceptions() { 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(); @@ -711,6 +754,7 @@ public Status(final StreamInput in) throws IOException { this.numberOfFailedBulkOperations = in.readVLong(); this.numberOfOperationsIndexed = in.readVLong(); this.fetchExceptions = new TreeMap<>(in.readMap(StreamInput::readVLong, StreamInput::readException)); + this.timeSinceLastFetchMillis = in.readZLong(); } @Override @@ -720,6 +764,7 @@ public String getWriteableName() { @Override public void writeTo(final StreamOutput out) throws IOException { + out.writeString(leaderIndex); out.writeVInt(shardId); out.writeZLong(leaderGlobalCheckpoint); out.writeZLong(leaderMaxSeqNo); @@ -740,12 +785,14 @@ public void writeTo(final StreamOutput out) throws IOException { 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); @@ -791,6 +838,10 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa } } builder.endArray(); + builder.humanReadableField( + TIME_SINCE_LAST_FETCH_MILLIS_FIELD.getPreferredName(), + "time_since_last_fetch", + new TimeValue(timeSinceLastFetchMillis, TimeUnit.MILLISECONDS)); } builder.endObject(); return builder; @@ -805,7 +856,8 @@ 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 shardId == that.shardId && + return leaderIndex.equals(that.leaderIndex) && + shardId == that.shardId && leaderGlobalCheckpoint == that.leaderGlobalCheckpoint && leaderMaxSeqNo == that.leaderMaxSeqNo && followerGlobalCheckpoint == that.followerGlobalCheckpoint && @@ -829,12 +881,14 @@ public boolean equals(final Object o) { * keys. */ fetchExceptions.keySet().equals(that.fetchExceptions.keySet()) && - getFetchExceptionMessages(this).equals(getFetchExceptionMessages(that)); + getFetchExceptionMessages(this).equals(getFetchExceptionMessages(that)) && + timeSinceLastFetchMillis == that.timeSinceLastFetchMillis; } @Override public int hashCode() { return Objects.hash( + leaderIndex, shardId, leaderGlobalCheckpoint, leaderMaxSeqNo, @@ -858,7 +912,8 @@ public int hashCode() { * messages. Note that we are relying on the fact that the fetch exceptions are ordered by keys. */ fetchExceptions.keySet(), - getFetchExceptionMessages(this)); + getFetchExceptionMessages(this), + timeSinceLastFetchMillis); } private static List getFetchExceptionMessages(final Status status) { 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 234b7334e64f..8368a818e006 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 @@ -32,6 +32,7 @@ protected ShardFollowNodeTask.Status doParseInstance(XContentParser parser) thro protected ShardFollowNodeTask.Status createTestInstance() { // if you change this constructor, reflect the changes in the hand-written assertions below return new ShardFollowNodeTask.Status( + randomAlphaOfLength(4), randomInt(), randomNonNegativeLong(), randomNonNegativeLong(), @@ -51,12 +52,14 @@ protected ShardFollowNodeTask.Status createTestInstance() { randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong(), - randomReadExceptions()); + randomReadExceptions(), + randomLong()); } @Override protected void assertEqualInstances(final ShardFollowNodeTask.Status expectedInstance, final ShardFollowNodeTask.Status newInstance) { assertNotSame(expectedInstance, newInstance); + assertThat(newInstance.leaderIndex(), equalTo(expectedInstance.leaderIndex())); assertThat(newInstance.getShardId(), equalTo(expectedInstance.getShardId())); assertThat(newInstance.leaderGlobalCheckpoint(), equalTo(expectedInstance.leaderGlobalCheckpoint())); assertThat(newInstance.leaderMaxSeqNo(), equalTo(expectedInstance.leaderMaxSeqNo())); @@ -87,6 +90,7 @@ protected void assertEqualInstances(final ShardFollowNodeTask.Status expectedIns anyOf(instanceOf(ElasticsearchException.class), instanceOf(IllegalStateException.class))); assertThat(entry.getValue().getCause().getMessage(), containsString(expected.getCause().getMessage())); } + assertThat(newInstance.timeSinceLastFetchMillis(), equalTo(expectedInstance.timeSinceLastFetchMillis())); } @Override diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml index c64cbe7690f6..431629b1d23b 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/ccr/stats.yml @@ -19,11 +19,15 @@ index: bar body: leader_index: foo + - is_true: follow_index_created + - is_true: follow_index_shards_acked + - is_true: index_following_started # we can not reliably wait for replication to occur so we test the endpoint without indexing any documents - do: ccr.stats: index: bar + - match: { bar.0.leader_index: "foo" } - match: { bar.0.shard_id: 0 } - gte: { bar.0.leader_global_checkpoint: -1 } - gte: { bar.0.leader_max_seq_no: -1 } @@ -44,6 +48,7 @@ - match: { bar.0.number_of_failed_bulk_operations: 0 } - match: { bar.0.number_of_operations_indexed: 0 } - length: { bar.0.fetch_exceptions: 0 } + - gte: { bar.0.time_since_last_fetch_millis: -1 } - do: ccr.unfollow_index: From 2dc4a5bb56070db97446ef324e30b7aef1671471 Mon Sep 17 00:00:00 2001 From: Stuart Cam Date: Wed, 29 Aug 2018 14:34:54 +1000 Subject: [PATCH 204/283] Update MSI documentation (#31950) --- .../msi_installer_configuration.png | Bin 59799 -> 61890 bytes .../msi_installer/msi_installer_help.png | Bin 35696 -> 37742 bytes .../msi_installer_installing.png | Bin 24590 -> 25020 bytes .../msi_installer/msi_installer_locations.png | Bin 52300 -> 54618 bytes .../msi_installer_no_service.png | Bin 36374 -> 37896 bytes .../msi_installer/msi_installer_plugins.png | Bin 82356 -> 243912 bytes .../msi_installer_selected_plugins.png | Bin 87640 -> 243912 bytes .../msi_installer/msi_installer_service.png | Bin 52124 -> 54393 bytes .../msi_installer/msi_installer_success.png | Bin 53234 -> 53152 bytes .../msi_installer/msi_installer_uninstall.png | Bin 27431 -> 32250 bytes .../msi_installer_upgrade_configuration.png | Bin 58311 -> 60423 bytes .../msi_installer_upgrade_notice.png | Bin 59350 -> 61376 bytes .../msi_installer_upgrade_plugins.png | Bin 79430 -> 243447 bytes .../msi_installer/msi_installer_xpack.png | Bin 64072 -> 94815 bytes docs/reference/setup/install/windows.asciidoc | 82 +++++++++--------- 15 files changed, 40 insertions(+), 42 deletions(-) diff --git a/docs/reference/images/msi_installer/msi_installer_configuration.png b/docs/reference/images/msi_installer/msi_installer_configuration.png index 058df78792fbb2be8a846eddd70a93c41624a249..36bae6cc5199de7d2a4946e898710d3bee17bca6 100644 GIT binary patch literal 61890 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 literal 59799 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

7L<{n0(uLbOV`_3h`EK72u1RJ}!);2Cu1Y6aq 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 literal 35696 zcmafbby!tju=Y_z>6Y$pknWOB>Fy5cZl$|HTDkAbg^T4+c{_eehew)YV zoU>!CS!-s^df%DZhJBKkK!n4E1A#z@Qj(%dAP`g_2=rP5<~4ApuBamt_<*&S)N}%Y zka{8iUM0~X;ekNJASqE{759vz6)$g$t2V)>ag!P;*-t{Yj=$kx@#2b;sM#!OunhD1 zWXPyXOt%};~(YQrtykfw7A?$iz%jMeuQ$sf1OrV`k9kUY$g?nRVx(x z28PlreT^tPbpi6Xu(FW3GJ1ITcOrfzb=<}EVr-YC=O?`kAlQ9bb2Cp(1n76=cA75m z8wA>O(;)n+2>B+-i0c+CLZTMf9%O(4Ie|bKm?Z)#btV6Iof(A4AB3&4s|h3i3b;xv ze+K>C=)do8yI+5z?V3i3giL}zBilW~+km&Lco<#OkcNuUkiSxHK3)F1Sx8ut+@pyY z8ydJWD5Bk9tLkOFSZles`}XxK@Q1y3MBdCE=O+A2w8IllD3HfZ+6_ zMA2zLm%#>a0;NJ0J~u`wD;fnh4n%~#5Rsf%W{k93LJ!To62bpHt*()Lb>}U$LQI7I zCJ!P8P12yiP2u3i$7 zz4WIkrh%81c7zci54ILZeVJJkSkI|XA5UfzS^iBW3*kt0H^I>;qm0eJsW;(BHchBi z7J&ZmC#~mFVIl*v+}nRcz@d?H`>F8ZzaNx+58DZFFDeqE9EyIW`07Q`{=Z_O=|ss$ z00sFwPtVM_Yz@YtQ_0_Yx3{+oh)C+a9Eyq)zjHpmL&WW6j$AN(BUwU^hw>hx5Pvo_ zgtxprJneToc)0%ISJ&5QB0Rjj9k(0sWivdETZ0pM61&^msfx1xiMcl@h;Lq-VUg27 z#1(4hvi~Ebt(`u;OBT4by^V;+Ve|J-7xc>!7`ys%7!O1f@HiJYG~A3Zbv*8*+lj{z z`dlxV8z*8^Yfxjry?MRBNlYum3@pzb__qOG*GJsWdnhC(zrPFj_4O5H`!*k8 z^y3s16aZ~mTU+x2LOp>WV(_^gpYC>zLouq+2vA19n|t-Yg3QggU%MB_i*<*$XPate zSIbW01Ondf)5`L^$F;ENl=%Ff&f{795;24;RmKt?9v+5Qai)3xI|B z`%Tx0#LUrde58IX*t_Pevi`yYC@Y#DSQD zL9_w${Oqha4EN2l)B6;!lf^o~Riv#);d7ri?Pe*<(lRlf0^(8ydY|=ST_Sw~;e;@v zT|I!ov}hC%V?;Oy4=5Y5 z{B+ataDV?65f{TlaAId?XK`_{Va+%FsTgn?ff$&WNVsf(sR4E{e~Si1{%)lD{~Fyp zoWEQQ(eq9P8}WEGvj?Gt>j)6hVV}U3o-pj&3t|WNv0|F++G>?YHIV*MkZW1BX zvmhAL@RUtX%%k1~yZmkBjyWqLd{SobLFhPSdKZ<{Ix} z9`2wjPx-S9aOAW2q+n3(a@XoVFOB;>IXRF^!)R6+^GpH{~;c^b-v=*C8>b*bb;+p z^8hY~&HUf~QeNqH@BtS|V8;B};E&R6qNt{ol}5v!Lj|4f=aDwm89LWY@<4=dm-=`d zkNfIaajyj1PYWPU#(g^)Heh2o8%3o3`PR=i&0#G?6}fHtb_f0@iAeLbb*n!Z>7;E% zLCm=3z#`?$jvjLvHZJ`o(d`x1mAGPtd->vjD8f=YMy@$|5;;(p;%H=*x? zy84XA2+{pR;R9usbfa;_F4`c0)9%hVraYeTcwKhNg~$H!xb3a;Szv@b&E45HMZIyE zl-@-Cev1AR(bttm}d)&v)0QVw)HW zPbX0lZslt;jtu&7BgdQOnO=tk9s2e|tk+%P=Dpj=+O~i%utSTHcr~fh(H4||3oOsr zsMq>!E%!645!v@H>{vpmA}~XEPTMQd4u>m;PkPTaW#ahDmyZzxl>AjYdID(g@$4s} zB18puqFUYWxz2yJGa<)EB<78OyATX$=hzGPs=$Kl%n&f#iJVfiJv@R%Ga3@~nJ!*7 z_VaS`dKe$49U}`w8KEiC2n|M^6Rg?6$}?rUu6sI*ZV3WaJ`hztj<*xhKW2~gK#$t~6nbi3zcecrXTr#rV0-u44DL?+x= zL99q5x`s>&9%8$B&1d20$?u$Qmq+&lv+8Tg#$KE4BPYczeiKLGH4$va9aCcuL)$6( z)Lv)3zX^&^NwBX7?l)GkKx#z1&zkkt2?&lTfT|n;jH*;<7M4;%&Zpl}>OU!rR`N=Kd zVlHfmh{I41e8_&|^K>$)S#$DGV7)VNBNyssm4RZ!eRVn2zLuoT0zS{)Yv=Wws~AUm zJLDJWgoyvNXZLVE)_(Ehjap=u=RrweEC6l9+Eo=5g*O#F*B-uYOneMMLKVTtW`Z*y z70t!{nNA1@e4=@c$B5{2`-d!QgJG?Ge9g0a#dG$kj1qw|-{44s%F#k$9{2R!F7(N{ieBiW-E(WyGDOPB}u&B`R65nX?z z)WMInOzQj>t_WW1AG6D!rt1G97R{fo>fg066g^sWv1z}C#`$0rIAe8uaG(?^K_!=_ zqOI+RQc=_~E^&>0^x!qAznf@|8Pv9-m zqwz6s&%tVA56AW-5xtk=#G_aA3O6`%ne5qq?unvdT3o9;HACsK$04VIu*}Eimz;6? zJ~s1`M~U;XiS6ZM-oe7vV50uf56?e4*;#Q4-q{^boe7`&+(yeJZJv2pz13EnoYtCb zSE0S|EX%`AJ42!nmwaz;cUrbbc6GPCmlb`r{tgN*-|%0~RY)PXdG<8H zM*c<%HXH+%LS_4XXx|ubD%4op&j47f8HBzA;Oayv{X*E^S-+sm05n~-aT73#A0%{ z00*DhEv#We=Rr|2co=S1q&=KuPHDKW`nCD+hkYk|riL5)(P(sJ?bm2LMz)r)EZmn+ z=~sBy&>`#HN3DPOj_^m@Nz$p5{5noO>~&B_Q>iF868^-hc-+T#eZxA%#XFmKFGKVB zYVqUEC}Fm}RDUGS#c{)$tE(#^zo*JIHKkQ|AZ%1r6pu2m$NAP5sCU@GRSRf1RnW|6 zuJrp|2Ad=RMdW!bI#_SOYI`~xQ6FgetfvgnJuk0^@4^8#97_P|-IOnCX<0hFa&>We zj|DjfoEZu1gBq*oWsG6Zp2MHJm?LkA5XJ+m3o)~03%XoDnz@OP~dB zztsG&Pt<{uDjf(XmH^Rx4fPHQU(nav*Y^p4H|eWO=a05-Z?FwZ97b7$t{bUT`_ z3X{DIKtL?=7SOk|s~q2jfAhLuJU2Y>9R}>(+kkt!hge1n2BqIRnotmqXi(kV-Ax4E zlOXcBRCIH@i6-C`kH+UV`}M6`q#Q5j-n;$!`Woo>C%rZ>)b$`ND-sb2C$jNva$$M&0s_EDBPq!M|>R2artt+cw8i-)?I@ZX3N}03*#Fmw>6#DhH?+0JQ;^3D868 z6Ps+9W&(kaDIAkg2VaEzkrE4sD(WsZS7RMsnC4=Y20Z}40mhfV06ipvLl8Xt`<4j< zE|oBTLtH-Wg+cw+hnYxUfc~Hqo06VxJ&Txw5=QwNVh=*sBR}eL9AJV(1hQi)DC8U@ z!wYg@MIe+jc69Q7r_JcV#ZSpjU68(F2IEYb$ zszdCNj6Rxd%+rl>_BPsz`~Vol!w4?*EiT-X|Phe1yfNcvJXZHshOl!PEBUq40C%zfnDDm{L@dL64cQIrb znHn&MOHSLrL|O*PyKCkEgwt<;-QUTcXW`NFuS@K2nK4k9k%inZvf@A~G2TWexSr6 zI?|K&`&Rf4M7CRr#C%>fU_(d@s#;TBeQqd$>f++Uv@)B| zk9n3yAx*&~4F((unjDHNQ-L64xUr++k=P%MdSPm0P>Y0!{Xx{i{i`O^#rZ({BILAa5mNPV@eeV@!d|+S7o)bGNLvQDXCU+hwy0g_g#kZ<4Rf!v z)ZP^pUm!k2hM~&_l+FJ2q!|g0u~0gU6-P{@R5xqxR+{L=X)(Z9i6hs_?0r7K%opZ* zwLf-P@x#iYuA07O#W3P_LYC0Hq^32@RCCspvwsxChvaPC+E&bF9y3xTIjJA70TteUO%SpHNEy;Xi_h#p2d$a>*%5^YXX>Ez&kmT3YL{O=Sbt(5 zn^bRmpt`)mQD9_vvhG6rPF=gS`z+K*6NV&^jW;C@D`@*`cla8h*&RF(>8PqyJD4R<%O9p93zEXN@&YMtL&T9_0P4p=bt*_gwpnMB=O3; z_mi-m;gu%YPpSosZ5AAF2G8XFsRf&#?s~e$yRcXD;Ig;~wID_XNo7>C{DDm%At_fu z3!Fu3?$l_S#ORU4m`70BFx80k=`pmFO}{Rn5wAzeLyarl(xPx-s2U-NFlJXCfdA$+Y6&B^6?@**RM?EUNFN=PXAE){tbHG?#A9KwUd? zwH7r-w|FXBpuAL;MHe-gP8EErl-@((ojqShknf@Jj-)#2tE>7nA{pCvtM}1)MUrZF zXWT&VVTWP@%ISG}w+y*Tn9`RL9gb$phH{S`#}T^m-xNwksJ}M8ao`Lwl50CLJ+b%b zhK~2$_2Rcqr`uWjd@vwU@9(Ni^IeY8>gqV0#0BFXzIb&gu$aS_o{uhLG`jHbsYpwi z6b;F!rvzR8&wIvM1t^7WBa8xrn(<0v2E86XLAsqEt3IU+!s(iqQr0f2_RngqTXp+K zY|)FVxy#;@7SmIElx%EMCnlP~>sQGi{ABw6!cHCdZA(ii=v3>dJV9GJwNgJYV)&(m zLKJ;liKdX((!Gx2do?IJANpN z2vk7mPx-Rddw86~qA!3$fm2SIq+|yqu<2~m+plc8t6qrm7Pu;jYKu^^A~#L_teDU?qK-xMv1Zpw^q_q9P~LxIuLM&Csf zN0Qn18}uLGpt!c5c0)puIq>2WOhXBO*xCk$nAaDmC$g6~23&m!8R7~AT(JaYdP9A! zxzjXgd{1W@Bv?;Ny#3XC zzvxk6IYaUb)3&#A6{YA)TlDVJrA5+qZ@6-)MPr7euzB<#6{#0mwCR&Zrp;}Nn} zL3DD(?X<{1DL)%#!RN(|D|hXUaIu7Ixy&(UdMjd{Tw4{Zy%dPZ1iF-3Ra*|hRGED8 z1#w8}t)kWD;>lGf1RbsZ99!T9}!sIK=%sD2hKyMg=-Vs>7r+ zB7LeNe&|*imGAZYrwts^7STQ!rEpnRdI|HPn*nySIHWkK-qudas&Q0`a=LWRRkjQV zzrc1_-V*D{$Sjp@5j(9oi|je|+wVJ@^~{N~HEgr3VrvwoclrLll4i)k5!(vQFq1K0 znd`RB9N{6bn=NR@&&iVyuv0($tt@NNVp>~@AK@nt^*Zf#C*#!X@)k6=$#wo()+BNA zmSv3cJe$N`H$E{%+I%sDfUZ&EDjS3F48vM(J!jOX(0;44{Dhy=Mb5{MYQE(D`g}#q zs+Mk5a!Bj>v?9++cBs{rq;kub-!3H!ofYbO3U!lVhEpl%XLnGPOG`$p+SoIQd+6!V z!DBxccF`NsL*L=E#muS8UzLU>=n3gXTHET|y;9}nhKo9+lU6L1OxSqy-K^~ji^b|D zB;}KM`p^27ZVyJ{s|`!770W$=PrhkdHzoCfmr;ORwbhdpOqD-sKf1W4VMgtc1p@BR zwn3Xk7JDrT`=KFmRpFZ}w2B3J1y=wggU|lrUHQHG5O7amU_z}rFN~rtH@-)G{=gMr zrmzSRxDg*1(I~zZDzLmw5daBwY`Lf>pOh$!T+NrLs;is3qQa2mS5_i-#33f81Bdmm z1Rss$EI+58$JfVNTI@i`9J_rJ~C%QSgGg9enISgJKLajKCkZGo#&94~;=teYSc z!Tirx#mAD^*DugkHh}xyq!ZuA>;(=R;6^r6K)EuRV!qs7*oEJ=4zPTbNV9(@b~-(K zO(4((5@P~&;^%yVsfu2YB*5%To4|PC76wRfc!ms))I9UzUn8bsMtZI!$rdbGyM6#< zL<`m!uy0)Pp&YJ-JWfpar%-;#|J@fe(qn_^O_MkBF}DPjElTU=ALYWq|d%|H77 z;`rQO@lF*$jOnT=6vj@|ypREUcood$VvH||;LpT2PU#&3Tb=9}6bOh<7HhK@bXibN z!iWQS2X_4D#nefa(_hvbJ{~Jex3i1u^@4oqN?=+{@MK?6L1nci=1d1DD&(berD5#+ zl1NcytGpT7exTE4-37v2@UiRTEKHcIoFq=cPJJ;L{Z zQeSET_z50kAY0MI8106GS`(d4#PQ|YJS3z1+wRYT430EEb{)hk_%@jv*SV*yo)|%t z=ZiyKQh(@^M6;qi8dX;hBO}#=7oDh=kh5N7k4}|t>}B2fP4>N|5j&?4FxYR%U*uT~ zq6I$guRPv#ZaQ5Il5Pt$pnExf)EY#ogb>e?+!~^Dc;^lNc zZa0B3K)=La7F?`V|62uvqo34YCoujy0pk`KWWbj9Uw@I2xbX1ethg_!=s^R7odrDPXjjimvREgfW?KG)BYvU^8qpP099y$UWh-W zr8mI92wr;p);XWa2~Mc`z+XR^4xJ6eO+e@d_G}KobpCe(h%7IR^%^M;MttD%mF(YTmMeop>#y)UvIE9%Z2_GJut{bF<^Ah$ zy~}1gAccrsM71C`H4SMxk2oS%G?a0JrL$5GG)UerAXKd1by8pxZ|(YOL&b4qwv!t}bWW7df>A-ULM;2J>B6Abr@g%eHTp0>I@oMmr%B$uW zet3`8R14iy%LDy;ODXusi~4wLYwJybFyNIvA`!)_yoPjc0}uc|%GTJW><#!E8zV`5 z$BVm+zi)iqv$C_7cUKX!iBpe}i70%jR)Z9@lQF2rR>F&1XUwm}B;-7;{65{k%_i2} z-PTkh9wyHyZ=KF{`zEDuywsZS-((_2q{%))%NS;|qp2d0cB-G9o;oYu=D{m!8$0zJN?ouXR8 zgzhepN8&N9HZ1X6ML_FJTB!w11y43_1631R^!Z7)=^`neA@|f&F5eMcHmzMg-O!F6 zsx|^>irlW)?L9-q;@-u!(#h8N!pY`#cC?iCA2aMas%>$mij4;@)BIzH z*dxUlAHJ2uzz39HYKjiL`@Wz#9GJwDPI2m)fuOvN*|AX(&%jDyjTJcVsIID}7OY^e zekNQrKqI%Ln>5%wo#~d@(thw}q?-O=bz{Yi!9Dn)d5bbUfPSR<=t(tO*HRTWV@%V1 zOxxG>Lb41q7s>0LZGTa2#ImPqMgTh@Tog+lG z3pbd%2|;*Z`jJVTYG7>r!K|wt2Q_r%8*9+Kr}yQj+(aYQe)hUG)PAaYRF7phZ7G7( z9XX{c%h?nuI)Y8x1%+y!_sJc6OA@ZfBkzT9^0oO2^dC*d;#F4bB*z!Pof89{#TPOw-+q1>{1@ zohKum9Lp(+Wsn&ToV%_p9Sl>tWf*@voNMzw#a#6{Ayq8cQdcrD(?Y~FoS{GDIq3wK z!JT5>WFB$#jj3C5R3PNGr?AIuKm*=W>k?B>41vfX={m9mqOqq1I6o4d$H8&6OPZo` z$1g)2@_`4%&WPm)S2~*WH<%sF5qe2+>TS`@hwaf(5BTOy%aB7SF#p2`X6!!vwtckL zOMnyFiolfE*G<|LQoe&QboJ1UK{J+)B+~^ob!c!^Q>)aaal{;*^x#Bqb$9xSIC74D zGOv*yMx%XRg#NYL!<6#fH=7Yww+!!%zNMyKXGgAudyI^9OL#a!J?(s2ZDCG%%b=~wu9Tz_4{7hLyybhN9XcFk;zw?lVG zC{~wH!^|8lrH_jy1$;$_){KNhZxlbcRP2t|65YG0ft#y~BiVYCN_Qf8WQaE;YqH)? z-GG#6`hwdMLoZQrh+Zc@n&#r`?Wc+3#kAC?=Ba5hxwRg#T zChR`7$^3@D!$$WYx_WaPirEv@y75_PI+6-3kkQ-*1 zcmCzH?ygJ5Z_ba;-ORGKwzfc-ld!O`o?h0BRha1M}-9m_4~>Qxhp7-)Z2` zJqWzgU`g_g+E>K&e*y@X#5;6|LTC-Z4^5YtOJai3Xrw2!XSZcsN%JuJg=n82+@O1_ z;GM!I?M0WKbiV}6xMo`41^EZ}b2?W?;I_o##Z08;vWbw3;>r(OsX7i_CEQum9s|pw zQIiB5Uxn_t@8$rSe058fNc0kM!?dY;)Ix+WS7;T@LF(_#nggR0R zof0H3@sgxSnpVdq<2w-D9K{aW;l3(r6jHPvO8LE|F5%>jFLS#H z+D9w15+Q{s^gFY?0RRZoV@AwajgF6Fy;ep@8^*>fA;V_ZHJWRfTQZXUpn_(#I%Oz( zPwLDe-TRv%e`8RRj*mr%3->c%N9|PId8njQmAN%{pHJJCZz4*S@Z7@bF;$B?c=|cZ zglTLwl{zEeQ^Kvm#|S1ZSF;Q4#xsO1qgmZ2v@Jj8xl3g@t}m%6jF<*`9D#9rTnd#x zb9}bKigctaUF+%URUNS}j#Rz8RRx~vZui>0-3WMkK z>zO7^{u#wVa?hH0jUDZ)LhPjcpg#T*rBsWAl!$Kipo>g@$EImGrV`(!gY+4<8ESLP zbk+64&m-~K<)Ji~s&*U!|%^9639LKTW4O`1$t*P3ane+h`dd zj};U?quR|W^UB@5R=(p2Gcvyox6<~x$i3;QD$Z}oqs00ch4_FEq~K)tS(IrxIUDpD zSlM0a+7zXPm(d9K>RU`kBlzWrwzv)%9vVm5pO7#oQsma58eB0)R%JoRqBZ zRU`BodpyQiuH2JkJFFB1QmG#oAye-yatTd&FcUT?s$LU}WhMT6C}I~ri6|Qfo3dQJ zqnQJDSh|Wf%2|op!0NZ=?GaOqxgVdPKJNre(t>rd5MqZ{p?15k&`#F+7;%~+kmsOE zV~!aSgy(m27ldhBu*$^Q?<(OGWj4hpbNn^ThN8}8>>gea;ucSLc%HG=1aH(8aW=uA zh&?6;gf?8t&$OCnc^INshvea5bZeP^VRZMnUDt=GM90Hpb+4o~`!$Wh*~X!GJ4z!bHpKfPA zFH`?XrZ9zH>+sktd^7MSLEFgR#2CBZ2*?j&iog&szJKD}%>7_|%ADpfGcFx!%%Tjz zvh?19z$Qy{VGCjr#Ll)BoO8r_-t5L!|Ml908~59!pSh8N)t^CX%Dt&Kvyj5pj^uJ{ zabvLnMF$%z*DwrbgoKn7a}_>ZfQP+W_W9+F%?N;dkZlT2<{K>An@rn>?4L;jbT5Pm z1WU4$hkn1YuH;lyU*$#f-&qFdMwQm*p&y;o0F}{TO{07v5B_=^QFq<-MTy6L-6sV< z@~^Oh2Wa@>SuKHlh67NG80fx|RAr7eCk6Bt%j`*~yxx_sR>rOJ5XiG8 z9z`o|EO_U2w`XMFzsb{-vNoAMG1~MP<|7eh*|0>5Oa++Cw=XBdMP!8le{}xMc802K z{7&bCT(-bV9ug!)zWPi~$QZ?r&*rLuL6)~qwwR{4?L7p@kLW-DDD;zI<4((jZ!{R&q<2Z?oS4(r-42vL<>ZEOV5_1ZX+8=> z+0d*i%GyRI9h;<9zWo>RD1U@yP|f|ukwKu{Q8c{p!@ha6D)Y08YAl>O-3Sx=Um(?1 ziiwp%xukMJ&+b72aA_ey@f*R3PBuZo>=VDqP_C*Z0qwqXiQv4jx5p4H0o_7RC6B?^semX*s+B~2p?6bsRy-p zPqFsM7#vqn3P7~L{C?$GG$j=i0|O|89(t_|BoZU~P$J<-iULO!Pc2-N59TY#f5oNH zMw^AZw=pau@D>dd`1UA%o)ge!ms&`@wvtHv22jRM?3_>3*a)O=C#CA2!&W~GcV14Q zTK4&GV*%mmBu4!e8ep6_h2by>cRb>(1dl3wIwZuPClL zlW@t_EY-x&&Z`bgVKGo>@kOX}_X5b9Lcf0Husl?4!;FpbS~+_h3+Bz8Rd_Iklon<30^U&o1BPd>TBxfhQ@v?s5*5htYP)f`%1=FT#To7uOD) z$yCh`%a+4Rj%SfaUSSZmyX<9#m-M-`^lXVh%Y+s}l$tpdjP=7aIJHXJjpast}Q+UACtz(KLr- zqd&gH35+li-kS0zC*C?waNJ>_zbh22{e?kR z_@>K3>*dn-pw+z9Bc)!2)?aKAy+@9z{=xfZDX+I)91XS1uE*VNsCvtA;rT<)cbcyg zmw}LArI7IKnv-7rvy|%;b&X64hURkmzu1lc8y_?!4#ZG#F(~DGJD_61wD7;sDpz;U z#=`|l8a?x9A+Pv^DJ=M4JTlTB(whHAJ5xJXnn}s6{>R96V+_O+)W|}-mmz~4tOSrY zzzQ4dlL(9Hc2yeHUNrWUFM@+JHOH@iS%f1~$~|``Ldr1`00R*$7^uIsVRZ z!%=tG_;TnkY9%HZdbwj+hintG*#<*_hnc`N0pe$x^S+d@Uebu9qB69fcK+8E>)=VQUFf9;n@Y!jp}OGpfIMgG#4oTSKmN(t z-}%Sl`Xzu3N+rkXqJAqO_o6fq3+!8o8iaq{FbA(~h6w~r&$9^^McUkCWxALaje8vn zIVv43XsY)o@->b$PLI0e8%8h8eE88b`NMAVyCo|;E&?c$l#gXv=vxVAgc|dK14~>+ z)-=Wsb93EPk?UL4_Rd14BBsX4?oFG0N+)dJ}6$)#?j}QGO{V}KJuxtpRy@bf3 z0HeZH3?AGzp#DG%v%CWCKE8$4lk33JnM#Wbm?;oD;Be;OoH>6|?w#HMoRt8f3n#D{ z_8wNKr1jo#0p71a7bqQga#^VDU^es2H$*}ommGBK&iHw0bB54yU!1>JJhj_)Uw(-w zAk4$OYx#|ASd{HO16`ea{QadzrOz^R2oBGw%jA<#QzLmiG)(UKgtz{!j4nH{&}PC4qa6knF;9~M^zw2POO~3Q%~urFAL|{X0@>nu40=o?`Nj%0Tc6#M z?lIcu=)kJeNAyu{NZBZkKow#0U=T1~dO2A*17JYlFkK7J-B1t81cM#g1Nl#kG%)xUROpDiS z&T&_&0-FyK2~*a3=;a^2CyYSufid?s8u*$r($}FE!$I${-Ab8kx}lA$9(s*!ok!F!x2aT-L z!x?7fLT=jR_jSx{W_vJLP)mQ~&~I*FPBsUYz*2*l?N=DVO$?<;sK`SYb#0>(ZQ6_q zjf_bLomt^R?PR<>>e#W48;-86y^+N6Z5Ml`@8~x1$k$`rLEhI!T;70@dGv_zShZBL&gWk1=h>En-!Ne{kF7dgXHU2sAaFIq1aR zvCK%og-kaF-p6FKyFE^<9KVCvw_IzmVU?-e{tCDMb$X<}6O`uwnY~DFxkGCl$AsfkGVp zy)MT{LwPy_t#qUNN3*d^pTrhNG7A?pkPtP7(V6pagcOS-rZi6`7Y=J8x~!>6X=mY* zzjW%kMLuf+JUs(Aw7B7M_+lXe&fC2qsBEPu16oj&4p5~$%8BBkfs#E^n&sjB<7p!j zdHi0S6{o6_9mVd;T)00kYfm2v50d*DoQfb55kXC*NRZ#BSP|Vyf1j2_w%VCrJtz4s z8*x{I?d;lg3a^4&`4kScjj1jx){+Kix_&@p%UXV-YO57SqQ0!I{a2`I2ct#=6EXq@ zxYkRZ5FRau!Cy@xY`E>xxq7Ku(`m#kiXH_)cW&kI;TFzlAW(V+-XCLowj`s^*5h!v zj%q-*KXgu4Dqt8+z0ATZVK`NuQOku5mMl+-WMQ-%6}2y9j3~TUps-Ax3-@@T-9fS6sMJ)5V=)V-frWPQ;v;BtO}47VkUECy9*O*7wsQt5C4 zfJBEfO=0~t*B$C;_?;_Whf!_!Ofyi?qa#S$SB~w%R>kPGb+4EgC;^ujM>w<8u08HA zBYXxutenDg5yhw9T&tXQ0N8*ob~M}jfVVu}^4+wAgOiGiBK$>qe#(-<|~0yT0H{CA_tMPkY|gki&H;n%}nb@<82J zFKWHqs5wou9(5J7qJh_hp`w%tKgZVkoiJkZOGPlAUwb@nE?}2_Y|FP7iytSNy$LbD{Tnp%}sT zq)~`>V&2P%C2X6oQhc?Z>vo)PVpE-%ynF99l5i7BAgf32Ej(1t%a_o0imLxYBWNP| zc;86P!$YQjpiOcgo=qSJEw$}3wBvs~?I;eie(&*6f|+IKP@pnhTB4Us%|PK?srQU| zb33qXPu!812w!4SfbaP%#!zZaIn66y^`1^``P?T-%K9`6CAWs6zZ#~Q(u{5jr34k|9n6Z1OgjrGl1tx;o^A>u6Xh*r4@l@~d zgbCzT4It4PW`YpC@%DBIxC|ttD_w|qZCz1`34%n923dpApZ34A^&`Fzxue{IIv{nS z(c=aIy}v7?umye{kqYtfxc_AaziWHtOO26>cMT5@@9?b1PCo(^jswNEQ(D@Qd_V;O z$RZ~o?~BkM;Ef_-^3pGc-<0LKDQ3HjU+^kYw==ELO z8=pVrx8Ic8e%I4UB@fbeOf@3ay7JrTYQ%+Y3%x?7cC71)ca3T8ZiY_wP zsR&iR45IWYF}Sr@5~4eEyAjKnTH2-RP16@@j>+VcioU|k;P9069rz;9k$pL#G0o#W z-FX7Vmh;!0Iz`!th{Rtk%wc7BGmiswYRq7EAnIZG1Lj} zygX0?UV)Cd&4=pVQ>uMNi_3s`U%(;pNeG4{sK>H7(3uE@!lq_nlH}iO!ywQkR%W!| z<*6+X(VEvDbrcnNMIz9{y^K9O_Tg#E^~N?b`sW}feCAk~bt(et<88I9@cOj~G$bZi$$$yP zU{lSS5ugo&w158UPHS3|&-^!9mJwAdW(2$Lajz4gRO>D&MwOak5E3~{!`8XT6) z^r=?s?JVD0OpeVh*7B2b*eh$+%F|hC!!#vvnJVB}&oClqF7DdL-|Fk|WIQDXQpyJK zycW_j#O^;LH3`-rDo(6;sm2iE85$WmJU(V`!Pu{8XyD}JtS=fJz=PB>?F zj57mOO+5sAm&KV>T8Hlw$o`j8{S5xcsiH0s_v85$dTbOab4x5VOZpO~0lN9wt;fb} zzLisQ3B0`#FNsMUIxKPFvp(9)5^UJdlUe-%C&<*kIwk)YGQYC_mzi&EH=7a8h;0-< zpW`K4Qdotl#uWUX)hu0UiO+EAR`9DfU2Qpe!jq=(1J{06by5bP0|khp1_dzSa)1BC zmCILo--pqAoR>#yl?|n-REtMCbqLU24g;m*)jY(x45{%s3z4K!%VaK3y1uJ&=>7da zIX$GFK}{buKwKqoIlc?le`Xeqv&gDh)fsJ}bT>$h4)RGH2iUf6j4 zsD2ko1U_W~cHptG?*s7Adp6(rZB|d-HAMnkRE|~4_H4m#hT{7qBZkpzBSD727l)C1^Sa0MpJd95Kn=onX$viLT`s2v&Z+-nd`)CaCbt zC|s(kv7OAfCTh0n_~ZnkxVa6QF8j-W2U=4!k}IF{OOWf1^m&9eo*JBA&v~0zm^@q! zK73l-5G8p(s;B(F6wKi=-u9#Uv<`nv2bU+n@H$dz%h!F6mY4tb@1y(ihR52G-p2~+ zZg;D+pkctF#6ietNyK^M^}daL#caiw+1>BJJTjA%yv$acur%ecGId&`_js75O9CUf zHjB>Oyv$P%P8AQMpjSF2dyAGBu!E3H?&~ft!FRE0Z?jxW7sUMYM}~%mMn^}7q?|?m zUu$0(P-T?0i;YStDcvpIsdOXV-QAsvbV`SSba%JXjdX)_AG+)AgERBZz2DsUanBz( z?|FBvU2Co9*=w`$LO+WUgfBs;5jqSwU%s+IOQTRW*N%!W&!fXg>uE{>Q4W>>K=!l* zn&%9x1YhNARaIgQrb+!b`vI42`CQqtp$%2hHJ;j4AhwA`2Y(f^t<<1 ziFc4hk!iSYce|Q-zgJkCgOy8umJAqn6NU3KoSN@5eldqr|3TQD-pXHm&q4d0Uee~6 zgJ8uCPd4|{kd)HxM55B-1~Pe~^AMhI`l#yswHc%_O^Lc_0R697$Ma9K&U4BY@TECe zSBl^O|MX);E2rR`M^@yNiybjQ&_jsa-DnY zZGM?5b}UL>U8_rpIWAB0deM-t#mu3^b~I=ULUQXjm|K@fWj3@Bx^s<ooMx+ghxz8CoHOz(Yx^Y)s$T3U$Fha05+tNw z1=OGE8xQ0=Y^Oef5g~(XQ!bilCmJRO{Q=H-0lb2hHEt`ePgZXq1Gw6e_yRI!q_hCt zRSAhtxZ1NTLqbV9x~KTDbGuCyfVV-N2Wmmtc%<@O3F>tw^Lt^Pxi|06=BuGC z{z@LHtJ5j?@8%$bA+dympB;7LraDUz@Yb9TDp1t`am|d&lfNc0{c8-G4>~Ve-e~cb$K4QlTb3np&BC@7dqU_mo3CD<*Z4Qva9$?tZX08)5W0)S>mW*G1|}ngiXaxb zBpXQY#@|Qk2Wl2Z%gGdHyQ;v&8q*3=V)B|ny0dv%@iyn?O0U=ZNa2!Y9vY(=!>ej9 za?+Yz)53OBxSz;4YC)eRHa=3@4x^0?4)>{);lPU?A05G|$VX7s*|1`d96svjab_vT^wIva-E&*m7}i&k;Gc{V&>IDE1Td`oJ> zPb~zbkmL4s5O?Bl#;7BiP^(OZsDcOkPed}~sM@OVFnr3@U%?#hksF&qJ zs0b3FyC#{VK+;P15jz{DO9^ybk$g%wRv1;%tJw*B@Pz6hRe+Gn=9m4JAq}~hfrRT8vpNK(c zRQ=wanILByRBm$B)M5XR-VdCGxNOQFHu&dq6~6u+3XQ7GIbTBF3Q-2 zuC}i?EY=j!a;7VO!FVI_`LF}3ACjNWCZf$dj-Jp<3YfE>9#$_nG{32b ze~>)Hb;k>kzXcW>l6)0mr4t;iK`c@s$+VkT$-slFx$bE>SVOYVj;%>GUAjb55(7?$ zZG*rOS-$h_xDT{>s3y=gvrY6j-isX^(-`K$JF1WGpB)ul5#x~kRvty-WlfVo%>V#%UBu>Q^z+y>FmF726w&TPeW=#m1miM(R<+wi%aI@<;E8< z+4PfQUIUSS3+$pod@DvU3GjH2V19>(1sZ~AEamq8at!&uXh~5O{S<38+jEVvH@VSK zYlffb=YUN#E-;mb*#Vd5G0czG$$f)~7ZPf*d)8vE&!Eh3(oMaI2PK!zWDh>6j2!+d zDxH4iRaN*87@tqnH#=O$ccCVT*_PpOwV~ zI=^XEw)vBY-Rd*Z0a(uOC=Z+jfMZ2PMJ*=8`^Q2cGmYkLVMgNiBo zd5JM)KJB&_^HOeFIt#a{X^xglzJPb{yo$b*m-nOU9JYvivu=;SDg zWH>Mq6u;$uXx!rAcVg>c9SM0D2oh0gHC58Mn31n)ehmGHV1itK_?r`Q=7nMUyElQa z7;HwH=)^F05Xk;E&NQ#3$ivi<)U?pEyzSvUKTIY{XaQ2-c3d6j_pko*X4rv$F+TsW z!0e;yv+Tvz9`aU%9MC~kPHcZ0jPgdPPc}+}MKJx#uG>hH$&L|C`}A3*nAgN)(w>&1Vk?lYRkB zXN2Kbce%&IC>|TVqc_`>!vNQ&ezU4sLVn3~q5LTGg{zvIK)T-#Gem$iKQH?5W5{)& zuP;jDIR{OE^;EYgmmy$lizg4XhNlPt)e|rcSes8S7Afx+*Pz46ZafdA+Gx`O#F>Tk(&Pw2^X zBQTaGO@N1pfH?GR2p|>{Az+0rofcYo3l2O6^D|tH+WVz5+BWH z&i9Wr>=!V^^?M>|wg6_6de?T|HWE2UOoA9Oyg48s=cwk^vY46z2Ep6i* zD%C3J`Ihn^1eKB8Z>F{wibw<(l?}?k29BFAg`R}@>CUt+VnCeu5sWizcu@}?Eo`S0 z)@M#uUZ+pMY08_6HAi9R?h(kVu8=v^p@1znPs7EnH#mHYXZ2LRWr|Ly8Vr;i>aj3w zbW(_M0$H=-%UXA6Daf%Y?^BkP@M2JK91f$3%15)r&$!N0DE|u`TqZkjcG}PDAM28e z*eD@KiW4Y@`3R+%HxwAIGs4+bj8b69Dq!42HW6ENooyJc2fw}G`d&~eiBdrH{x!Fz zR>d~Hl!!Xf<<2bOUwig{K?qT63@SB)9AJ}qn?ZkGlCWBx-5x_2%qm$v?LBKk-&i;& z$4&vD%uIng>NUZ-1md9K$%`kXGI=1?`WRzJ$aigjhw*X^v6 z3-!}Ev7^UIg-H`V<@Vb)VDwAfTH-h@T#v@ZCFG;wC=PpYmV2zzr#5lYVaC8mPhO>H&bV>fp@+;m0U8`-l6j1yuT6`??}B{HkrIV zS&6&v6&#seaD_a*B)iwUo>01TY^HO#x-m+RD-ns~maflH$Q$#mZ7AJWGcWr%O+XWaRQJfFH4mp=Hlap&?nYlxWbgQR zcwLn<>Z=4xnhxIV@orK4ymYFgYECZ_EEfg_a6`3;RIiioHG2O4)|0YNpNy2uB-kgpb#9u79_JrKk#_a~~Fn*FBj{E%7of z-fXn)Wa_wABh+tPmhyd7#jABc#xA{k`6Ttjp=3cy@*4zsSOV5@_S)lYYE||XlC;e| zHLc58_*S>Zzr7&Mg9yU$RoA3&8l~}H9PUWp69j3{x_InZX zB$`sM=8X1O!B*Q7x)1 zYT8ov^q1qP2{)xPt%xy8o-Fg1S{kZ3?&GPG6p6$+oXgFJo-s}FrsR3$C_g$5!y@d_ zS|Cp=i!E)s*bBzXaG5nJ%=o@3zmP|L)Bez(cn>hCF>#w*K4divWy^ESpZo4 zr}6l(se#I}3faJikYKdrT&$O3U7zxo?f&C_oLhR z4qZ_j;zSB6BFU~Gmie4<*XO5Zf*6DLN`d>-+YU+`+^tCiX54ch$XnW(AhV2zs~_oF z@P9fl(dQax3VYmc@!E$}M=%cEemh#t9CRldnp9^}b{{S0ob=bj==kA_aBe=`jw1t= zKEz_a+-lN=BOQBuTL4ACsX}P9`81{_nKtotSoc=8q{r^{xj-t@`2S)LE@uu$_QJni z>Bt-6C84O^UnIGV4>Xz`=lybZEh>=KSKV|v_p;58UohPx#9nI@W{dKa6~{RS!Y#ri(P@-hhGUZhJmtInSlp zWuV!ovERECW(c+>dZ(b%XtCK;>v#D$Mx6oD5rb>sOvpakDPXo zGYMer^>x;Ya+q|IKsO)eoQ51Q`mY+k8{G>L5y#uESv=_$Y(<)lrn18N@ZT`R4bmL0 ztLlisIfz>dLd`YV`O#YZ)qK5XvBK(Jj;eEWM~tjY0q}sNS4RJCx+3{4&hb{O(za9l z=GI?cRo3-OCTET(tC^u^d%-a6U~Xm+OU-qjlK@tHkd^8w^?QfP@E=0FTg^Q2d5c9m zk|U9rr{OXuDKH{dC(qaO0tKe;oYbyYMg6Zo&~M$7`F4r=Yv4%MQhB1xY3Loe!Ii@l z8;T@6K!{)aJ2TJFq^K=P6Pe(eo_R<~^~T=FQVd9tmCU|t*2wGXnKz7H)IiL?yFY_$ zRD~9599&S`mZCaXMnjrQB7c3Q-H@Ke z^NOu%d57&hxg0U+L3V{x-n&_^OQw=LMtqe@Lv40yHsqLaYeM8E0)}C&8yu-E_t^Qe zx~zm0=@5QbL89&#%y;`(@f*+Q8e47-_wQGH?s;mDH)*|YuD89eobE5bPQYa@EHe(H z@FZ-W)nWD;R=j3wHraiLV}yM)b7k#yxw2SdcE6r_cj|SU%6hg*{>Sk4Zt?DVU+6gG zek<pE{=x8Wp)=p@pq&n3mV34}`xY{?bn}2lwd?O|@67a&kjqB8D;?zFP{A?2#mCnut zXUrTd4TzvosdbzSx%qO3KKJS~Fxr2Qg1$`m+A}`4-yE%iy+63IjUpqUg{BA8_JGV^ z%cYQ8HM|{mY9>0?;T?Tp;Il@*@PYNWe3u zYt`GcL7wCMPAVt+@Ez2|CSxoKGLKFk0Sbj7=oTm=*R!f81Nmx?A1b?1r?`!BL(8SI z_?1A8i0s>IgAGloIQa#SRE#u86k28>Ij&ha6FxKz>LHy>=F^7+Mu&*VP4)GJ$iB27 z#ZNIauNyB>e{ZJk^JLon#iWbUA)>3c0lGsUAhyyG+-R>jdsvZ-^L;PuYXKfi;7Fq> z6n5ES3me^Y#OX@dUV?j_SOLz3mX6VY(8P0f{G^zCqm`Ua5Q$kA31~}^UMCo1k50UW z77p{4ANt6<{h?L(^v9w?SlQWo%8re`7G`93t$jNAw=HF`{X$A0WR6C9gr^s0s%JQ4E!q92y^lTm(0Xid7|9iXZ-!|Aenvxu7+RvGE zU|7jLhm4icMXMle!hSqC@yp_cyw>JzcC%hb_i8=W;eu<2>wiiz2V;{M&kYN(gpb8t zi|u0mD8nlqgVgb~>rlbt(;r3-`{kK1i^CSI^BQ%Jp3{9-%`{0HEsE}Fe8wWNI35$u zT=52H^v8>zPK-wlem7#C*l(-q*KqpIrVpGK`=&6XPM0TT^wc5p z;cL9Tui#!8m(pfbxrZ<5-v_C__(;zxu`dR~Lf)?#61iEnQVLFMsia)rIwL2VU^RD5 zgnE_NxghS8uDMc?;4iR!qab)HNLMxz=76TyNcQ7r$<4VH(%{J?jFp7x@sSzEXA6$b z9uC~p`Y2>{zdw3emaMK$w@E2pHcLi7yhM4FDr>t;XQ)PVRXe374j9{R9lOfMB^i*O z!kx_Qn@=`4THO#ejK8CvHVJ)ChK_yHa-&EyXVwyRUr;!*?fe?IFt*gW#~29L-w#RX zP%sqWU6|-tS}@kR%5u7`nN_#=$^FAKmIQ`gZ=Qx5gTi?L9JFS10^v?ZU?4=Zi zTnR*>z3jc0k!F%)R!9U6=8;y$;S9?|S9&p3mTWGY zVbR6eu5k_>ZkqoGcrHj;;$AN?KX!8AHzU7>0?@MFsZ^U1grB__nGwjT03Jv` zP8)w8jo15>LF=lXxE_y_Tc%O%?Q;4ga?@a!W!k3*FMFOU!6i+ZSmM#q%0wrIe5{gH zUXQje%qF=dRy;({l0m6NpGV9lW>YG%9-1Y}Re4?Zk6BiZlRpV1%oV+w<-xT-?^tW` zv%o{_tbM+0@BPxoainlpVSJILsb1-&#G^2+-7)HwPg}z;3&Fsh(~=_i`uo&kF|~Xf zLXKsDt?VO`6EfXbF%P4I6@Mf3(kX~^RV!?$tO$Z>nvF_!x4GB;XH=1&Fkmr+=m{o# zi4fD$PuQF=QP?3%-|8(45+Rn~-m~_PYp^_ej?G*@cS!CYT)e|@hV6V3ZdYj7$Pns_ z;bG`(LxzHs>0vp0=Aste?08KzyHRbC>d{z}7Z=>-DIE?Gln%IY{bKDndOtzN0TKn||mfImaLQ{)i=E)DJc3I8_kM~o4z3Ug^Gk1mHS=GidA?&f|n z5zWpF3+Wl^XfRbUA~an)@JoxlyMF|mJLK||saFW=MXZ;TY~q|bvTD3wrGgi{+^7&y z{V$JNR$LwHq-K?mv!RWvm#j_i9El(V0WCk2r(#}yfV16n&luxzifSM6!>+kcn@6_g z@|4iwrWI&*F2S!yPo=5X?bvcvJ zCuZA_PbIm|UC*k=VHbW5Nj=^$dj5Qjt#r(U)-Hg~Q6hh*dW%9je{&Ck70;#Tn5iju zSswBZ;~c47%es?F&YohDml39R)!Kc>zZyH-i7fhThPQ6z;PR#(Y0ptD8iHmZoOkVC z;9L_eazo!PE!5;Q_tCsBWOQRp-s+93AwrY@7<7jc##Zrdoctcw${D(^rux~2=;AH7 zu|nf}9@Jrb3ezA+kRwt@X(POjbj{N z^+R1AwyB=%3@bJ=*sn`{p84x(YZLm4O80vzf(DGn&)diK^(`syFIWg>XAXCIF&8_v z!F>!=n1%yEj04P{lp7zFJ*yelwk6e;oKF7nYpT?n%VI(8D_O4do6c|(_l`d-I6L~u zcbP#`mM0`bC0oz-{g4PZ;o#VxYTAS=+)&BY)}_AtnIFl&v0))Ib8w=J)`#U|U#T36 zlLMmK8@-oVz+UiX>Si*yN}ye!f~EiNvb`X_PvQJYgrjq7g)9RymK#uZuIKbzRucPp z=!N{Yf4CMLnRwvU);G6FUx{o##&}IGK&6_yR8YHi>s?-=e`aB*Ugc>F$9f>qDEl0( z>5;`AZ|t;rx8Ln#?nLr1DU-#H{v>bo(n}HN>%s~Ri$KmPyLXyW*#}&wEc2zvJNs6PDM$3}lXC2(P$(^4p+~Ul(>LZt9T@QFm zX$`TM^3gCUCNtYO54>59aQ4xCZCOCj*kD(R#pa0nHo~&;orhiN4!dKDAr@on6uqxs zZuM3?*$y0;!i8f7tqYG*$)Dw~upCd?5Pm5a(nEzeDj#Ea%lJf@`*MWHBU2sQlc^gg z*x-E7iG7{NhN|(#xwMwr;-IShi)`seRHjMT3NcRG00vk2SMlZO=hEK;*%7{U z`Q_p4W}61hkuRKd_;tiX;M&z2-7guokRpWk7w?rRZ$qzotXgi*I?%O%5ae~{h2zZiHe*lOU;m^%$?cZlbTYK)~!2E44n(s3^jbj4I(VjaSumh`h)v7;8hp%+5CH8OQV%oi;z4 z)bf(#2&&w1Y^YPW%vZ=(!xS&lG4(8|C?1mzH*}>XL(0~%Oqlf_lu>{sz! z-EN@9S+#PN*Ot+Yw(POfmC64B^OUUqynK2WghS35T@n=_Cq^F-z>j<&;sxaVqjlrMyby zWTu?tlrd%7Fm+#!$@#scl)=}SB=h=ZLSxphn)b8q+emQ)-q$pqh1EcQoD>#mpC>7>X=9Vqgd|ClB; z>YlCNthRo(r0te-x{|mtvxDZUTB6J>IjYIU<8UwUB?VMlsThVYp3jA@9j`8Y7qR;u z6_93dGn_;$H&r>PF2+8I1xj`2QZD5(M6U_gS-qHc6DD_@G3cnW8AmnIVh^f7Ot&Rs zK>mPAWDz6YX>|vr^x!ys=Wnw$V$RP@MTeq=vPw!d9@@O}Qc5p7 z(+$t5NfJ*{XM@j5&NYhBG~3X}gCe+=QSmnpm#=c$%Ls)?;LIonKNl;lXasOI(3xAN z1RITMwl|RLhV(znX*$%^1Y?_!aTp9krjw^nWUU)YcuJInRKn+{tT0Pf5xu9G5QKTV z$FI^@xnB_l!`_@(UyRClXyO?nSD&`#lb3v$3}tnY&X&n*`R2RbNu_r)q@x`=nT_E| ze-^`MDXy}+N#~rel65zCoA#O-j4}HRh|;5P*SYfQzikp;EI#?sX@VEryE5?G;~llh zo_T_9r8vkKGo}~?@1lc_um%$ChxX3jvXLQL7xP6dm+CMo4j=a)+#~$(voufD67sZ= z)&dA-x{K|)mV4u~f>W7;@uncQYT>f*#NwHs{26kgN?~*IA=U4OO>b(DkEyXX*kN^I z-rye{t!V|~8j(qs6*UalBJnq_pzrgoin9-`>|3)1cV_8`rd3`@oNR9g zNuX@K896!KPDwfix<~e|O8MT(N4%(ldb9S+cSyR?A0!Udk zdvIY;U>)oSC(J3r8t{MaxE@n%+zZ>wP-VXBGQe#`498e?URPZGw0KwXUT$pM%?_%? z+Q8qpVDs`#YtzH1}u*p|F3v0@#uuSV%*XmZ^Fvt zKJ^e>`)2vs+9hVc*H#N#`Arst@``i%q4Uu{&hv@4VIVLe44f;1D-|@qt=i;G`1=GC zR178Z$&+&WQ9^z!|D;obzgbqb2`L}${LI`kxMIRAA&CXw=V$+@Q}uVXW94wB-w&EW z)Nkqro1Mef=5vywdU%`9G%2j%6SIqoir(W3N8^;S*1wEP3ZyZ#`{15-e>57Z9*n1S2)~iE|CA-HXS;;dS#A z=fnH8)RC6h>{8vUA-xMpL=JvA2eWOtw4{;};qc)ZSB*4hSk=bZ%9gaU`ho??;zndP zS2M&`uo=ui>E#&5S1x?E*Xb#T!_Z=BuX*RMOS7?9dr(2W@G(A*fsj^9E_VWF!=8%))3a zaQDFn?L)n)HBU$w+fa2lPnW?PtAxW6X07r1jW8mp4mtOBoNkTvvlfV6@QJdcIwhe^ zokUWH8+w0r!o3V!D&n2XMT)~HCOkxyrJMWzmE1Z39B&;vwAE8Tfg`_|Uo zGH#a{WiC;2peqt@xKH%wHNn^1F+{u;jKP=kMroZv{3Vc=qg-bwO^D>Rw; zWqI>i%PMUO`uNWB?UUO z5PlBdA<;J$>^O{7;!#%pbP8d;&ZCIDO!tp20KRAG5nnAk(@z&^IncL12>IwqD(koM zw(E~h$ZEO?@K}|cW_;w$1>22rzcNFD1Zh-{1m_#Mu+t%fAG~2;>}i(N#9KU?M7h{! z1g2i)dppj-L*t*NLo?G{V5lVcP!DB>e&| z_FLR<(0b2?yw>SLP4wWvbX&Zlhbo`9&BtrEZpe6NTh0iqZ5pl*I?dUSn+B+Q)#Sx{ zSAxQhmjV2j=2!b8z*9K8#}URj9%YYy;te7McxO3+;3gY`I1Ix`iP`gYc1AOUD+Xdh zZJN&0bZf!pXt_EI7dxzSeDkAEDP0ZTFT&=5%n}2^^6)TpQ3i>O`B=hX*S#BMtgwnZTzc!juNWe7 zev!2TucUz$m<)?o=wf24{D z#b?W?r61`!Py*&E=L z9I193d+fuKG4b6AI4W^H(CHd1m0v7uAK!R=diVL7RVH;I0}JDJ72QXg(6LkQ>wO4k z>J#?r*S~yP4r_RB(%4M6UTqFcq)Gg)Gd33vec-akhXz zE>h_aFhvV}hO251XUs86;51-oXQpQsu?lTed5iPn%nKY#ci6)0^f62yIqJ1g2zjv} zP?r@UE+9-nRWW4737{miGkvAIA*<-I;Etl=fV%dugmlz6+t{tcRvJ0-5+qY&{yGXV zKuot-BuFaANX4b`U30=xz)J3NG+s+~9m&ow8>myxJT{5DFp-6NrIJRdLwGMWhsR>) zPnnxFd_gv;UdD>&)rH`|D>!0fqh-`3Cw$<|-fcCxtwnR^m6)0!D`>$i8282FXjx~Q z(EM6jgszx%*T^AgQeDNEU&7xqycOVjSQ50U>oa8gFE8iwWP@)Hn%JAyc72lNT^T`a zSxp~PCTa{qLn!qRWRyd%y9h&RB(Iz$3#!oU>z}|BlMuhNXk3udTV{52N{66 zSLhrIY*A&r7}fb%HC-Yne(hIapjeytE%tSKj?xKv&2;&+g79zM*iNpftZ>Y)>ck<^ z8dX5oxhN`9I;R|ns#sc3fNQ%w-pvGRFM{6DVu(dk>DT~QiJNWAu+qw+M44MoMxKM! z9AEmmbdmIYZa4y@nBaK>xg5nBIqszqJpm`9#v97I_05>0?FGpShsEvi;q7#UWG}lD z!wg3~uirw;agL57*tf`qs^>kO>DwJOl<7h6+s408^X&N8g^B3We{M9;#OQ#VvRKD#hxq1}s3= zCkuCU=jY&Vl1t^wCML%n;G!S3++~d^3cJG3$+V<0k9)j%b@nHxvpSs0Z76jg-mwPg z0d=|ehV9d$qsNR*Yg?(`T~bj}_O`F8C5SW5t=wlbWV(jajT6Ep!%$RLH+o~DZWO&9)X3g#CoxC0nxc+bz1>-E+% zlEXtNIYj7l?dfML^7vM1+>heGSW8xblg&y`Mi@^wC{|f}_ry)Hm4QAl zZ=gvR8%%06b#BIb)}g6@2Z2ltJ};P_)=BjUU{x%m!8bUGjR;!jHse=5V^qq^=nRqtpt zu-<)}--e6bt?Z1AdW~?WOpEt^WJHnW%@Fh47 zhtsFZ$2Oj@sNgV;KgA#S;4Al5B1nWs zi3M(Mqs(xwa)cH(+iGyNI|)e(202BYsu|S36e#ANgjcFzl{mRV4WpnX-`nwbf_d3DN(*9qAGxL%G-zSf^@C{fSxq zMI%uCQ5#pex4#CgS7R=>)dtTU-8O+$pirP9hN(SnsyMeeInWg|fDDFW;*nb>s=n}} zJDtNO8nt#G%$9$1b}G+<)1^lz`ieV)Y4nK|RSxA?H+FeoHPIc*BD)Xa9{-(8&oWM= z*m#Ju>jyun-~`W{0;DG}N0GCT2<+pOv&kk^UM9a$tJFZ&6`#${rGluUC4K3(l`dAAp+c8O0@%_{fbj#ITU(N{HBJ|sum}X)8sjwalG;EVw=sA?UYjLwCX<;V#mAX z(*CC*r*9R^S5<56dH$FDY%%+CuL$4p>`c!P>-}cgi-sH>~TD!9;G%G{t+Nc8M>VSTSNngUJ;{>4D6_FLB(PKDLXuuktuTl~$`+ zg5zoC@bx)wte97Z=h3WHiwC&Vm9@MK06r>a9{jZEf;k4MHR4!Taom@~aZOH8|E9Zq zuaoN8rJ#!)TQ$#>T(Kr))vPW*ZE&myjh`1>*BWqlQ#hZDH+tONxByM-?x^7QdG;{B zvt`cMZmN+g6%0&(I6_yAr*Yq*vlE|5$!!!TLh4Y->tV|p!7WP}$LM;m+8`(Kz;?Vg zhNdJ>1kClYTlXpHDY}~eY#Zlj_hJ@SlWE@B8J8kF_^Xc=W;uom+c~T0FfdQeQfDmy z=`AbNR_0`;Z76sxRQ9^@l)vF3x6lgKtyWoQOjH$MEtU_i*#Ju?x?}mXfS|H3{E;A{!`fugfyA6xH&kCZ`nJ$83X`l-GE_^ zs_(x*^RaB4uDaS<5!0RcUvLR%g!B@KFdrB;wVyno@Gx(oECBHR-Lr@9q6Giw3L2n5L?slLME$G&e?kAQ`MWhg>6-K!OhFOxoekksoy9y~?{aC?lTPADk6Ka6OYU=b>Di^flHx z`PzN%ip~c+d^bjc9^4dMOg`7=q-wM$Lqqj-v+fyLWh(MRD@6hy5LPlys()J~l?b%>$imCbdSKLtdbI;!?LT(X#3J4vsUp$XD-yKpIM zlrc>O>j^qu%qhGMBD$QVFTmekvff7HWX=ETms>k?%lb1M?TvC0W(tKAo`BAExjZr$ zA;1q^Xt^(wzrs3=TJ*f$1NUo-P^OR^T^?^#RaIT|F@g#6Urdqz?X1P8{|`UaC7k(+ zJv<)yF_~wlgT|tySl9fmLcd3WmJ3r$?vYP{a=1}84XLHNEnz`N1_5BH}?Z$~h3_+;E&~%X)d0jy(ATAxg z=t+of2XtCT5J$=svOyyw{ThSLp~TiUNaF;xL?iz2c|AFLcBGBCkVBG89=kW&nw9mq+8Aps8YXV*5At%R@P zEX}R>$^r+*dyL>+$M%mRP!$d?Ym@>XKLnT$+jO$w;~)7!qEJvgq46dt{teNo@|#Lq zP`x01lmc%>@S51+Pnp8U4^<9Q^3g?w0zna)_F>BtP-+)`(ahw&&33^lyaw#_xP#8M_u&K|$da~SOefT(~`u^tHRX@gda(^`XvEEuNE0TP&* z0kaOJTzQl5g94Esia~T@Sep0&rc9|jlVpY+O3f2!jl?OE1|q|k(H@9hDmG-umX99Z zMg?#GeHaKH#^Ma1gUmXUB>=lh77oUHEq1{0`u1*oJmy-oWw8aoJam~S!1-+B8tvAk zSu@+z*l3S!^hoAC`Xyo-;BT!SU+zRhd+Fy$)Mc(jyuPj~HCkN_MZgN%yyc|L_XDXl z&60C4Td&XeH*8nM?*L9Tvsb;sW@cu_h~5faDa56lqP9Q)hrwh{{d59=E!k~I>2KK7 z%#t9(#|;?nFSgu+T~;#(30i;4C#Pi@`sp?T6C+T+ka4C2X@=CBghvW+Fh4Eu$qwkN zY{GK;AKW3{YG1m-bl*XzgtZ$OWn2FNG$n)+_tFUGROFGoc{U`=$nHIpE zkb55HyThJ`f3JTrNH(6gHM|1AC_h$EF(&zYMA3=Muws^!(ljaO; zNUC@oS++8;!BZ5rM9Dqf1_D`su~THC|VD=g0|}i5hozcrBMZHH%HR zT3#+k{;BsjO;gTSh|($Ur>i}qGFSD~Cb{iUxyJ`TY*q$5)0bmtzDGiJ_)v}z?6BEA z^lMy4;P~^rO@0O$5Q+9^wdZn{*W=!9ldt8zZ500SHuZi@+G>b3<*qGqEd?1mLVr3` z4Oz_cSOt$vGGy&O>}(i!n65%0ctXy)LN|d3D;0Vij_$ zxohVI4zsnG!|U<0>zqZd%d-aFTNnOIbMv>GLyPxYdv!>B?)Mw}_XpP`L&957RpNLY zG&H?3ZKuB2D)x2yb=hbi`aY__b>nz~GKZF{GQx$a!tEVm`6A=bha4_D;fsd!dJeiDYJRNbadgcZ$&fsRnAd8jHC`ICF{ILR zQ+sil^=K;83;SaG-SbY&%5b>eOscFJsqXv2llFe zXm|fF+SC6*vHJI)+>H~V^^uwLgl<=Jl!@6SIIXq;5=<4-)IG+;T{a8}W@}M@e*@Eb z-ja2Te581GZ{4=;z~Gf{E4V}*sDXX`%DkbKtn1 z^66Iq3>&Mte`Y3eg$;U1ZGF9}guFOeR1eClXV5ACr>wb|Ms5UaWam_on73go@vC+` zn2QorAlj`hJ891HRrxeERCUreWq1cTtGZ$&h8ZRR1sM>Ong`u}5HnzCz*A4ie-@u1 zLAoL}`@p+p1Z>^J?4Ic8>nx-w3I6#Pe9(o$d)g!s0NlWjT{208Czz>j>8_uG6%VTH zV8D<7;^3O`++f!vWt)KZM%fAc9ad%4)ucHQwo1tIkS4L_#Rs-<-8Z{O24cOr-0kgf z!w)=L?UXr{%sJzhWqt7g_fb^n?2d2gN6I)WpeW}b69h{rjAc;2h~ma+8d4djfCf~q ze+<`-FM$E!;v$N^wYNU_JAPZC?BRz_P#t$3aN>`ambR{J>+IWkUtgT9WWxi^Sovu} zi9SODh&Ld#XB8zuO2jE(q?Qb}4H)XMY!llt&4noe+AZ0r`}zQ0?lE*|Rg{@rGCUsc zw&nU}|89T;J_b6cKQPR7@qUY>`Np1|_CU=Qc}uZX9`A-p`5>&-9(Vac5)u;0BrGw0 z5@=sVph1pDfFS@_zEKD-CY$7V5FQ@Wn2L$fnh+Kr&;2%eNXisRVL}tPl6~zv%p{4> zwP>ug2{lfB7CC@Bm)j6|FFa4dt*hW3>StX()3u|b*Zlffc5yP)&ZD6D-fBC)d|rU- z0K9H@w7PiiXIpM(Um)TVzkO@Cj(~lRD*B@8L|>cc9C@RYvU=dbld7eq z6=&5XEu9#HTOI`1>$1v%Ct!S8w&<4C*40h#oQh@1j+Fg*-Xogz<;{%_S?(-qqCJ}q zROnPocemGoYu#bPSgL^4F1_)={Xv_vHmf@aXcKmqjh)=(y{Itj3rob@YhF=dCT6IN zOW)3gNZwJ%$+FUGq|c4*-N`!{xje{Pfq0bd{4&w9B`-sS=>vh^OMtr6g?OYcK}&<_ zyp|+!xsT#p92@wwk(ASWGa||1^QG=Vae=w%v8N-akav?FKe4A)z}e}uzqdagyzFR* azT;JknJ{EBA^=knD5ru_0>ymVU;YOXp5|o$ diff --git a/docs/reference/images/msi_installer/msi_installer_installing.png b/docs/reference/images/msi_installer/msi_installer_installing.png index 498d927ce2f5e0e1fb374699c2c10708206cc887..590c52371a74eeec9496553ec2b705525955cf2a 100644 GIT binary patch literal 25020 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ʧrb+yP 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 literal 24590 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 diff --git a/docs/reference/images/msi_installer/msi_installer_locations.png b/docs/reference/images/msi_installer/msi_installer_locations.png index a8c816089748d2e56aa7c3f178ce59700518fccf..ba7151e3714de40eec5ea62b7565ec79ec8a3262 100644 GIT binary patch literal 54618 zcmX`S1ymee(=|E}g1fr~5AN>n?(PyCf=h40?!=cI{mqsjMi41dk670)dcZq{UT1pbsG+5TrINB=AX7X?G0p2InNL;|c;H5B&Rs zNcoIR00O})*@%fLD_c9dIl5XqI+4nViIFgTQSF_cu)YXo#1@1R*MCBp^ zljR&$v0zD6MWX{T6R7CO;80~F$nsaPRC>`QB%laJ@*+OO#s?b)DTX1=9nIm@3Vt!V!pAE=r8{CwO>ZXls{I_!~Th z69^Ty!JN$X^%Df>!JnW1GkFh87YM?04gnU_qmb3ZOc?%-ax9vm4-psw(c_lDEe9Kj z2@>{>6)gb?OF{%@r&Fnc@}NM*V`ip1AY}%SF?GQH94IjRA;Sj(q?b&F1(BBkBE_(X z5C>WFfvTo7qa;Au%pgolxd9%~G6RTNM$1wf^s@!jGl_~&4}wJmF{?y`(t@CTK*l4V zK6!#dGC-J;S6cj+pKDML=zx{VsON8^;F0*I2g~3Lqou`2OfxBi^BI%V1kxl^n6b|* z1CR9^JIbHyaS$jk0SlPz^{eM3a?RuCW5URHc(J2(;zqKl4V< z)PNtz2OVhtRzP_MWo?L%=YAh;S&t&z1j;*F)w*{2w>A>_@og(Bd%L^KvIC-eCcm}( z-mLnJdbD1@Jo@v$+~5A`+NSu%YVb`8>fuk{_>Fuq;Z!1Ai0S%%oYYGT!ut!+9QlBZ zX`413=8g)wbF6q~%r$E^h`mR}IxNWv znXyl!6m6UsxB)Pz_>T3cRQaap>Il#qfQZ556S?n8)5A2oSWnOq}P zJKEy_t|==^xVAL?r$zKQ3?@{c2(bY!3iL=ZR_eBRNSV4K^;z^;$yo>0Pe=6SS?tpI zpOQvb&F$)8c#`Q!KaYg&X7Bdzvh5P=l3nS+XPXK;7d@zL)2RNomBrgb-DBT_wn%3c z(J0JR-6+dfN8^ao6|K&1R;^VDFT-GW&*Yg1#+H#Qt}aNK!?(tStVkuV&S>Y?5GjHgUV|q+nH^GPnEN-50jj!* z72mIP9qF$`9nk_6<@r2fY_e=x4XW44mO?N)>CO*7+!WysXiC$r{i8N9&uO=52};=I z+gL30?J82!Qnf3kD^)6`PC7YjPD`^(Iwd-Ep1&cB49imMQ%hOJO@7QiP+L*kD1|S@ zRuJI5tzoRND<;VA5clYIC_PKh=`sw=k#{WJ^y)l#BJ^W=Re!aAD!r9L)P``EOtgWGI&>&fzT}`>zQQc9BR@$bRq*$4=Xx0DovU#+*yan&)-ZJ!B zO|yNopNW+zj#bNGP5)U^XOdsL-}hGm7^LtOqJIxqlv;#KTYFTqqRybB|uEEz4(UEdNV~lBnj9COj8zY^&vTnD|23y{< zx7k?@S$(HvYqe*bXEtZy>$&6XxwJVtY_8M&BVvD4_GW|-a#NV?z*YWAVoKkvnhriO znK313RWJQ)DfXziH#x@S-Bi8B^+a%rOcDiv@goH-Gnj zp8=HyQbJgLi2NqpljBV7%r!c z(};nSSN>$*O=u~0IU_nfZ>q3-gd%tHg$OEL8Fmci6@~|8EWB4AQJ)n#(+uZK#Il$^ z9kJq-G@XKX;@^Z{99|TIY{m3@Z6Va<@xSDeY&}KemIcs^ezsZoJhn!lGwx5RVfV8sOhy>Pb zJiWb>SYt|?pjqn(ZWe1R?B;L%Wl=ghIl(8SUZj4b{-n-%HoMVevh?6Eb4K9v>yG{J z!kXT%`&Yryt~Ct^4TS~E1&e2y=Z1Tsn#hWyhty|#gK9;4=hNX2vmahzJ|Q3YjP_h3NcMAG>FV`%%6lepLJ(LEgao*0RD^=phz)fUVg$yXvP zA|_kW%&SbvEVayMJyX4QrE$TT#p6xy7pXN8lppN;I$pBZ>Z3NRM?rH*R##Io*(uq6 z{)~_7#%l%5J_lx#U|ST&^LA9Xtyzip=mWlAe3SZDt!`KKYkL@4ty-D2yhirNZpZv@ zNpniW9X~oUYufbf+RnT?n|v(qj|g(OCO2+6y)Loe7Z-AP1U5Ray&dld?{C4_8~!fT zuajdPD<0Hm)xVT}8Qln_*=X7YbnI9ZyUNuSA2yGl#yw)ZW5 zJ?=Tdt6dxF8zSx+^<-l)ywQ94P1zWT!IaIV0;32ser{{k9hA{v7uZ3*S(Z+SL8OQLqEoWy$Ml}m~Pw~Bjd z4>~!z{<9yfWApXV>DsAzUMM0Ey(*~CBF1O;uWxl$oNWE$%sq&R7rsJ%F&Qg9)lIog zmUoUqb^=r6nD8G-VT1m?MvQwPLHWuNVZx|TBESn8F_D4+J6f{PB%k4bpTtAN=TIe$ zn$~I4V>hgPsnZ4pj+z(XO8oCzU&LZ=+OqBqqDWKBf9+ABE+Hsn@V`sS24>1rF-?iG zME$R)8#LRi&K5|_L7M;ig`Jg>PApm|9W^JHsTV5F|NrmztM3aZ!FNep#!W4Y|JTX7 zXkyW57zzhXl*)gDL><7O;(O6T_`iSSO-5X&1P;y$-(L=da;6qeZY2w6(gcEqmtVq++$^Nhf;o`|S(W|GVo)%Lbj&xpH;7CS7}GTyHEQ zr4AvRBboQDVk^wjBeehe4x?r)-8n5iFK_cKoRPMlT;W=*F<9HcIxa^4coReZ->Sw4 zk6SkAR%=!+cz1RQ$<7zw+!#iR3VTb+kjjA2dmxKKp%Oumuv8!jVUP$2l{@f%V|)4} z_Fd8}u#sAMfW~GqaY)W`k2d-Y(#D!{BD+Wjr3kGDvIhz}%h(A{6So(Q#j`M@S2gGp zIW9k;(tq6|@;qT2Y(fY@ze9X2T`487X~v($|ULdtxi9bm-}@~fT4^b zNWV3LTJ?G4K)ryqY9l%je%|(09M6{T)h12nuKM3(*GH9udxfi!ZlxX-(d2HPW&)9o z$rWT9j+NA&9fwkhuupkFvmK)z=+bN;kWI7o3F3Dvr0*mwL5a)T-9P&*nS+a=*kEA@ z|F`9Xz%E(UY0Fcoy=#Sfmy7qZtT4(kiIz!ERcGU1g{VTPty~)a*}kZe z-O?JX3kqT50?>auCP_XY^UJUn<)sAq2QEhi{^}Iw=aZIe?g_~J1XAg44GHugG>YQ} z>#fhQikQe!|7(M6*^qG+_T*^j4;Y>5hMVx4R9AjOj`su(u4um-RPc>iHdJu05_0<$ zmgONPVcP#Tz=s|ifrIIMk2B|!M2xOH{z7QygvFHhIPdfQBczyh`XU(P2ug3D`%2>L zE@{>Ab1BA@5ts)eG^3s96Oje-LS~*h>z?*z^M# zD!t*XZcr}@;mOJE(}aYCWfC0d;UzE2!4Q*xAliu8d_<`H-~D#slWYtzk8e?Naj3U>B&LL7=RW?6A0F``QrPKQ*P{hk@OVYQ@5K|d z5SU5Vj1EQl+)%9 z)k;AY+sx+@x_Mbfp^=fTC%gKbKM{}ROh3sFwgc~A!r!@Hzz4b1u%r;ItgOWE&tNZp z;>?f#w)H7))O_~jR*WV5P&is;Av8DTz~g?UTLLvt)un$$7koZ0?zF7JcAC7pNRN3E=62K=uSgN&aCiDjwi{O%-d6(fM;r( zqqLLdJTp8*dB&;mH><>6f_^2xZ&>~esPu%>{>3OtWI7&T_t(EC<=tilX47H+tVUGK zqJhM+PWBDGkiy16=#>_PDz7LMJ!^}%PHHa%Z;UEW+#74|Q~+xT$!Ckkx6Mn+&7Jol zX?$Fs9rE9y;byh0H1Vt*gAVG2yM8DvdCH>+`U|dCfo5)l`W)1YOs$>Y34bj0-}wm# z6(d=wSW2*|b}fiqD9r4Irb@{BZxBJGuLH)+hi<$~g-pu-cSMtReWQ@EO*mv;-op65 zkTjxfx*PI2ANLkdxB5=s?=?RdCXP_h_htDq8iy%!!t$GNe0nmw%~g?IZ8i@l0|OOj z*o5W%JC97hGH`?yi#i|87C*H=g$;#;T^~&4c2AS1d3^nvsX!If8xa+SFJ$tn^ z^z_u$*8ZYTXtJhw&=-n89Y?~I#Q<{Jcf&HUbalPH2m1|gIb}0JjlgLcSadV&_FlPo zHT-zuvsG6giowg#TU%N2dV6^^lZqu4a49M%*ftdlR;;yZSecobk<g1i2o>sMB+z;jyfY0sXPq`Xa+8SdLww$!Kwyo`P(P$EzrFjeypC%#E z>Eo=rtc8_Tsc62nqhoDlWw(>z*SdU_uW!DtuC7u(9RM(ZC8y`7XHdvw`D8ZKv_wT+ z3M&>u4FAQ^@qK5SEL#%>1Qp!6HAKMkqefaVxYw!fayw(L^!{wa=kQPOtWkio!N}g2 zySqE!HNYm;sg){naB*bFQ!%CyU=U>e_`y%*JZ>FH!NS5qK|ukW4t+a%`CT3^U?j3oi1;Z#nJsXW`23-M=|niyIr0sv~5}~b}M8|rI3jp zBu9lED*65jPG!)q(XjA;UC{DUs+~J=0|BVd0Mh-}Siu{U(EFLsSqa$d;_dnVtj+<( z`?M+}w+)P(OfhP7clvy@qx8n@CS9zGh6ue?QjWRcZgtv(BcZ0IHsh0Z_x^OU($3pw z_O45h4IPX|i@wq7wB`DK{|hGL_N2>5aQRnTRn@pPvs(+9Orks$n*yCdXHQ}@O8W~UF6;TQFewWVYbV+4F9&`_;fG!od^<6dEC+@3y+c{nq8HM@RFQ$L%Q$Y7YGG9a|=K z+R}*>xG`)^9CuR&vc*@C!Ih6&01285qIMj3{FoR)ybfa77*jx^sL+ z!%pc;O0n9URLkHXxBECDfzGehqWK`u5|<4mz$5)#Z$pXVbbPmbZYEmW+1`G-ZghLP z$!Kq|Do4KdWe)CDcKhmoeY+HnLiBX^Y|!gK%qE@ayVDe;aNa4EyZ1H|m={uFSnFtQ z{nKa=fVD}zhno~Zr25TC6f)FseWdM*)a%rAlrk=3I7a+Q_e`=o?RE?d&W*bE9+bLj zYS$NHpDhk1MU-e&j_!;c?CidLxtXqL+4Q~sz68!weeL*yC4!0_y{9fApi7(d!aNycHN`~=C?4Rj@M+;C~aKaqy~r$QYpT5QoZC3I}Fpfaas-fCB(|fuBIKY_aN0 z`NA=P2%`rrmt~-^ldv!`#j#8|vyLnn<*AbT_u6mt>vcMb9kHV=4(^D&?A~RTBXOyc zXk%kzqoTI2ALXI@MH3~1dyUy`Iq-{B%QY*r;ei2ZckGwD-rgn;0IoE3FicGm5*q5v zO&A198vVi!WM09##hNsjvMImK*>&kH8Pj%Du`(w12viDafW&XlB{(=ZRMaG1xW}?#`3rLN zge5DUJoKEXuDYy-y@G<36}9{CHrE5q1O6w&nP&mP0uY3Th6dR8^)}7uOPx@;` z0PFhTOPn!3HZjH}ga?e&U+vl5m(p8sFc7I8|y1H7Xcb+~l@MD-bo7KHS zngUqvWJ#5Bb=Hi(CrmBa0e5)ri(5i(%k6t(z;90ViXp%KsTGN$z!$79c$ za{{3*TeDQNvQhWI4+9?T{XB5Fqkat=l+Ne4nXRJ}8EF?12% zX4O0;_$W@N@H}?f;=y#m)WVd@a;^J~R2x{OppSr8>@5!#4HhgMRZRWxy1wN?e!qmd zg{`|k$bIK_v-XyhR5H@IB2f}tajT0wbF-~^KaSM0n#@Q_%(M--$fZ_t@IA5DjbzOj zCvf<6nOZDIX*LuR7V5H07$g#p9X$$!XL%JX>|` zs7;E;W_hU)J#L8snqOLI8rf4c1>$Wz7)9T~uF$AvYHI2SUnvR_Mx;1-Ld4S=a06mu zL5pK`BihKF;n-KNE2Y)TX5+2zOx;gW9it9uZePDTyzdjVhv+OB++W$%sv^)l^P^wA zJ>FQXvB>~P@Eoi2e4YY38Vd~%4-GHlM`G2a3dzH($MNH7@C3=a=bS=sZ?B?7tb!cD zi9ZC5M7(f3iV~Tj)SzggoFXl*Q(Jk-nAD* z$5L&Dg*!dx-BVdYBmsEX61N8BBA&u;81 zGqbYB#*nqEhs^h1pN)=o1SE7e+^06QVmlL`^omtaANI2#4kwf6lkKbw<&YerSA1_q zX3j(_+R7t|Ux8I0&%v1XaXDM=3f5HL8IB9M+)MK=7KaetT$g7eGiJtZ*=(*7lEnnY z0x7`F2%78=0JVhP?y33NY3b?z)(w+bEfa?05D=iGrrnp`IqwX8C*X?p_uA_F$dp_0 z>C-2zb+2Ipg3a~IjTWA`{TTG<4moWJtAn~iDeGXR9M-*rA3xq`R4RTX&tp!ZP}5_d zFaqV2@?YSzRpQ0h#*m5qYX(9F~fula%J-Bp|7g6qxD$3k?MkT8cHHYs5&# z9dM>^ufX8$n$7LJjl{KixI%bOGImoaiohW}Y3b9hy_++`pxoWLe zqL|I@RezvHrI3@H%9&ct%( zDWUE^VFdik7cM?NgSl8Yd49I1LQg+z3tK2+0C0U{-44#mWZa&|0uFJML<)D_Y|Tmu z%3}f+TV~uQ{Awf2ltHM}x02}gFZG1>3y(iFq+$t+3BN#zyl{N394I)0Vc{8avW%4*6dJp=fRM=Fpf4>U@Vm#f^j_|BO2`W@? zQ)oK|RGsT{>{;ExxBknWVV&mqau&0ph=PJAW$6gzWvoc0L<<=I^QmiVJ-sy`cHDe8 zyInFmn9vy=9Zj~Rgx&6Yyne+jBt#BRZKi+|FpRy|((|~Z&n~ms9*F$#mm`<=6u-S3 z@Y*4;9h68I9&a{sz=|)u zNfYAUora8##5Zr2tQi0bZXDqbczbCNcvH()hT1`LCNLMMNf1!HI?3WPWq>_{)E> zcFtxsk;(7=`!FPgrjDD6R~MCV+_VnbkH@Az6#D7h*u-fHj>7=HcVq;Q&C0*UmD-&L zSiCOk&!7EX9!_ey9lm6;Q&CCM6LZMYTQ;n0Cf90K9$EcV*;X;kq@kgS7&TrsPn~VR zEx!6ypwQ}5^L zX^7A?Y!D)f?bM{ zc|Bavgh^HmmFy%w(vW}iR*TX7`_mZH?X7z@5fKnEYv%|Mq3Hymnov^1?I~l6 zC!&R(Z`5R`mHEJb*XwrHviSh$Cv9!5t(`7S(N508Vl|pu=_UmCLcn zD%IPuui6{=?M-}&(=E(ryHefhrKCza8rkaa2zV)Pf0CDyDwQm3{#s{Px}z zi%{GAtmwOdalRZoA!n9srd&qQ0Eh}O-)QuOBe#dZQyiAxrgh6_=PR!miHM24XI6N= zRAfk(I-Ab`mEif zF04yR8>b=xYlB*K+T((snd`J)?)XB1RGZI0M@H$56&Eni`Nh@6#Z_pKR2<<-o8Pb; z(=UKky14+-b|Gpt3Uv3D<{9In37_WXuM^gM_tTzr(z4<4g_Jms z03Dw$DkcUq62OYNQz+Qbxi!36kxH;!D#uJ3A}*w!4#Q=xZ%l;asG6rb z`cq)mz>{y{$FkHlI}0T5bM-O9I~(^sA6yS6kdTn}t~>#A?B8?YCbW&D4(_FSyJ;XE zq@iS|R?Ku;(9o)!uNbqpcWHYWh{Sjfc)Zp3^K*4=08}*n>iNOr#?{)cbs!_TJ1e<= z;@8@QY5l4=@Ww=r85Tm{aoU`P!*urd@0U(D87jVjKP|7rvBY-P)~&vetU&rvXoZU? zX<)GNE^tp6KGt^16yWTOY;xyH;bwH_bTg8Wc4d^8hEb&4%WM->oCO%BW2$dUyFfECj^<@dXT;S3X)q*25W?T_FmH8)7vJi-pfwvPrR z1nT@)b#Ph!%gS)BwL33lAXG$b?54Up1rhb_!ct1FdB}fPjEwvbPQRz$-LDhY>=nts zgJDq4Hm=_4qA z>9?=02A@B5RaI4$w6{wzmiqG{LiZ!Q8wfeR`96m;B8N{HqpFL|7TLT%ZzCL97%^wa zKU)z5_g?fp-8MpDMvB*BVve^vq#o4mQj3a-VKsLOf8d8<2qOJJFAM|{V9&NOd})?` zG~xq!d7XALGm$hnT!_A8!B!>m$sp#cb^S2iy4%fUnI^ISWUY;&Cr0{=Kuw~{uN58{ zx#Tu?)A)}Q_Xmv4ne}zwmj~wxQ_iHvzphiUawG>!_<$>ViVu88z}a-#`3^)E#zz~E z-pX3TE?+zlEVS897*dwdlXXvy{@{%1lVi1{U9+}tS4;vatY9ulZv4-yt!{zzS7U(e zSR>&4`*uWqydqYtnnJ|w1OT#|OCS*=#RXY{lq=^q4%U6-%*=KMqj8SXTo7=OW&Ho`G8i0nfKvC3QO3VpJko)7tXak)o0-$)4J(zgArUeeuDtCl*Y~e z69G&^n7I#(x1oScZu zp+^#01T`TcVa(CUf9+~-@zFc5KRoV+o5+ka%dbh$GG+r^BSm)aJkpFRI)zGM%x53$ zyJl(~J*aAAbbf(2v@>VcHOP4PYid4Mpi)Jz1)mlFJAy;&w?R{+#i?E_+R>uZO1Gm~ z$`?QJcr;j!Hu=GZW4Cq@!07%7)88fMW@hp=E5oNPfr0~KHcFIuTuNASzZWP{*x43P zx|l?dFVhfMAV}7y3`4)OUO=0vxG#Ogdgd z!jJ+Q74aet2rDTmsrOrJy)}y`ul_Hrl+;En5kfFBTKW*pf8nj{rnb*N1;r2oG2gIO zt6Uu*n@i_rRt_Pd3TFUtg9RTdS;&Hm_^nEorP!L}Fb&1B@8Kpu;F-QNqsfsJcFww- zXL9X3i4LOZxY@vk4jiN=X53Nt)Rx2sVf;aYHiHw1)6rdEcn&NLq^tnZUl!Nt!OHd zSTti^#PmI}0)T`-W&{z~zxDF-g7Krn?7 zT%!S;D`w@(;y>A*<>ECK*d+kH_4XJua|7tLAFeUpSG^#ZRPxdHZNtOjJF1+;c>3k2 z6v+pW2x_k1(+ZGMj@@_XvZ`XrM1KI#3T_P9=E{nU(p)Qodkm(;Y)3J`a$YY8)~OU0 zP?!y|b8&qLl&xlD`kck;ek|My^M$V&4XStN@|1}O04uz_nNh1|%(&n$YhQAH zyaiHh255ICj7`|_Z@mSe8EzruDGwRYBST{I9KTq5zHi(^8%sz7kfAyR;PbzFvMDJn zzpof6RT$O^Ilpkfs-+|*ZeOvmb8;^FtASEf_dX-6w>pvC4qBoPgl7p|{8}kj;?__B zMg+y#MvEPNDrC{HlHs~P{TBSx-fjT)TO#og_0=b=wg3*5$8YtUo65ePr}_E$Zp~0Z zr0|A5)$Q&0-OsSBFXm-{9?Y4{iM&VQKp}H{>-}940~SYg&r*ak%5;9p)NMTd=H~Lo z@99+`OYrppBcQedFDV@t)BLuZ4l@c6c!`lYaS?&2^t`D^05BLE?uqyL2>_>glFH=q z1+0h#@QT<>orA?z>;Minxv@TWOp$H!IG^gO@A>g~9TeI-56;fcR^o4dx(EB8R6oqw zHW}v30{qtM=UTwRb(fVohgNWWb`%gkHq#s6f8XE3F?@M#0YCeGD;$nNgMyjcIetF( zXliQmI!pi{=nL2?cP0)BAeFPb^khGy!UDOgtduq2v18mLXb&kM1j&|ij*H8b~DWUU!;T2#GVo15u zN6ix{WB_)xVa1#?OMwb-#Tn}qSqXq^MsRG8j2*tuiI0zOyT=V4qrP3ixK)93TwV zJo-39D0BPwh3qEpUR*>(j6?v!2d`__=$A!X_9l)dqkz|c^yA^|fT{DH7)qPnaZJXN z!{ZCW@gJO}0|KNWyAlNqAbF516shIH|4x@eM@2=YKzEw*u}Jl3zI^GW-jU?CeycAO z=DZ`|E$HLYRF|Nji5mc`f<=_sKFNlv=Bt+_(}k*DE*~%4h4KS${Q)n|d&_l4M@KAm zEL&&^XlPrI-vTEr)jBQN@%8ldln901R;Y97Qe=K#~LJU1Ym2K#eY)SWV?H#Gd_* zqR+%P+E?e=rMT)qWC<~RMhb2RPK4{FOszHGYqbb!b&^7tkjTbSO#GJ*p zQTvV~$8q+-(lWK*QtDQahu`lrl4V{0@;bMFeBjHRSf}H*e9>#sG3=urZ~a7 z^iEpFA3|V2bq2)P;Jv+OIV#woKQ3)wII}1ibgL)LS(A!Fn*jymXlGdH9sJ%tukE@& zY2Y%(u-5$;mXq+f^?V-i@aDr9M?j8fQlE~EU<25MWD>A8he+77?GlFK;^L~NiJd^~ z-Y##l8}k=3c8^H>>9@uubpEKrYKa)$&vJN6l$~9j#;8K@d&b9f<^8pLO7QbW$Ng%? z(*Em-5!ve|CIchm*dZ!lmszD7Iv65271}7K9xuj-(a>{i7cKWMViNHuWEA-0 z9h$~Bxl9=s)AYl)fjW$SaYXQU394xK_YWwzH%~!YMh)+>1V~}ci=0z*I@PN8)w3~p zZ1rbdC$~%RhX=nF|GUmKK!GB)xh@mxO}YvPA>=YwKmyTVk46D1OO~aBfP7^r77q=l zwoDpLexA?lwzTkiFrCUt%Aiu<^WLR-MQ(~6KI4M44$!MjXZN~7=?Ktm_mt7I`&`{q zCfC3OwuJ#f2j{EPiHWv1{{fWeJ55MET~M{AUH4S{Ybih{)pkEx%FD-$Q8~A*zw^JZ z7t1kp|9u`b3^{Utl53aCafI^@?&^5+d2G_sdObBcnZgsHN%;+&#BM8tKtlifbFI9* z>?$A@-h7>!P*5m+tGZYyT8pu(^{YL64JXkpuNMF*Yv@TQ8ul22=^933_?SLPlRhJS zVAe=7%Ks_^z`NxBKCt^o&x-N{a=;tv;F7w__|!_zYF*jW`{DMkpI3^ZQ0apn8m_N) zhW(Y0kPQgA_S?(SrIDwowPaDnfxQFpRGs@+nubOS4gpglg$H`G2NG*}(n>*=a_;mpzW%YM19wy`->}B7lMz z-am6f=msz_QE@FpA>Yw`TnL7Th8#BTZmCK9UWW6<0Oy-ou{nn>!BG78@ndLc=w)z_ zn=TQ9Bm=hG{;#%!&(^*64yIJhE%`ntiMSf>vMy=T%e+zs%5e1<9b&_DxGA%?*c3 zKK<0rR`fVqZ^QN)YkPVsiN@j3MGEfyCH7pU zXgN3!xH|zp{^UY#(&*$z7#N|%B(wLuBfwmMV4Wgct=ZDjGM1IK#?)fRj^D5{zUTaj zoLnkS5fv~(35mghA@5v{eslK4`T6;Uh3twl>x34d%m4r&hv|nig;LQ41%3VfwS;`B zf~|FRD%F~5kkkMYo1~H#c=^f6!7-YZrKii7&8G!kT&-9-;?Y;MwocckkHyErA|#{- zB)lM~%gal7zDEGytCs&gJXEe!pQmgY(PK2QRdM@zj06oL*CvgK{VpRdjT$Ll=j^=o zb+uE9mzNixQQz9uR#3}miQJJTGX^o}&*ej}sEN3^r+5(!w|39L^`nT0FR>CXIyyf7 z@tI8L>($=V)6;5Z=2N?Owc@Zz!zc1j7?RY0Tof8E6@wlJ@BSk+ZEOz-2Y+&Ga$#vH zTD)NGyfrx0k2m{ldobE^O=%Ypbp?wAfnw$F-@jk&|GTaa%nO+?Mm)Kq2>=b9lP+ z*lHbyV+`>`Z;Cxog*I{#s!PTj!Z}5V+t$D30$e zR~tIl%YILg;ul^jWN#aul4hKKRF$0D)BkD%!#>T^$>{eTB9lNb0k}#Z5-ra(8D#qP z?_m&LMxSAc#&(^yS>fQDVo#41>wAtjmWQ!x9NE7CgYuS6H0ZFNj`{!eTNGL!^k5ZY z-W&0p)iqvjeipQ94R=uaH$PmUNXNGe-=#H5g(Nl1 zlzn=T2IQ1Ju5_^Ra*=W*B5>OB?O<6kcf*w>d0Ytg!P1|Fs1w0oTzt ze0&e_fbuXJNxx{4dv3U$t3{RiEQ%A|p`v62Via=qj@%+6KLf7iI(YYmPVUy_qO@qht zei&Z)A!TANcXiAuM}pUn4a+E$#S=Kg2->Y_q(F%y>w5B`^rmx{-Bd&Qrul(jYYx6se#7r61p#BcM zL>JepkSTMMxPLYC{a8Q>rCB+H>?cfv`D8l*;CjVUr95=gsGCe}XP!pOk-K-pC?rr9 z>xl>uC22hGZO3^|*-RME{;;K=ffwPz&feEbhT6gkWh>06F1~Yx|=Lqu#;) zt2JFNTslz!-AbK2{A3?L*+p>*BKYTnkh+oEznlUiR2o)dK2!d$3Uvgw&zv|((7r4eudKmFe`9Q`EGgF%rJejl?Y?(CQH zWK?4AvJt46sO(cHUuSE2kk6wZzw)OK>`_&(MDKq91vdAS2(of1N&|9c_Pxlf6KnkC z^2rh`+}1A?+c3=zITv8xU1Tp#n4!4eA63aj^l$rEh*tYJfS%X-pf7 z)Fpjs$GFRgRhO)JS52#^^9^hGxXo9q1UgEcCK737RP=*_2vU-HxlTGid{gn1e>|;ePmld77Xn5^OEDL zK8Z@h5ig?%Dx4S0ueHIK=?3RX&VBKijMj@fkNZX185nk4;K`Zw;LY$qK{H#lk{<{q z#MVAjBDD~Ho&8-UQ@9)nMG@sS7dBwgNaXLnS+@JgQ|sqb)V0tYwlS@1)Nmg_LX?K$ z=^8}6aVJb3p7gM5KL@=+ANsil^Luvg^=gZhEn|hXvs2yvNK5S=|NC{I_Wl$~F3lGZ zAg4?kRaMs1)T}kJQW_)96X5%Q)^FIYaQDgHRLn{f;J^(!5M&pkB+at7t`@pFwL=+~ z#lN5Cs_$(xL9ujfm?NegPlLo2GKI7nU#A%F9HL%E#Hg7;e%nHT0 z-Dh=g_&h&Fr?`Dea6g6#JsVYWI)6?g=*8iPs+;UIw$Izyb+*70Y$a&6U^P~glX-2? zN#=RnPavn*WHZ9p|3jdDPbj(;7j=w1cUcP#bie6dnNFk(eaxVdd+_V-bX4)67T#wY zj6y53mjeSS6|D%?R#7ACEz^qe6kIpi;%l~==*37 zuJUtAgV@&cyEr#&wikUH*BMf-Y5pb*;@%j(1F8C!`?Js z<-W|PhiQxSBAFJUj_U@9x){|N=szbMmk~=BpNg&*zpT9OyMfB+ACBYDtRe4Wnn;9* zQIy*p7M}0yXji)ey!Q^EQ3Y-`udhEnt3hzl@3l)sbusCpBPzEXUAu4~cYOPx75$_Q zX7(+2`e~;e6HB2>U<)#S`VpC6jQ}q($`-%?SV`6QaDVNTNvYr%w`H4_1A557#m-?n z$?eNpOj$2-w^P2iiD zhvS#z+)TmB((Y7j3p1ZBrtfOaxd;BVpSqHrTMW`Wmb%#8w>((4<#Z~>5kY(PniZ)EDUI2I#g>5J1`qT>|8(PC;N;l*rkkGU%-&-c8JlPREcm%7{c>!`+}Fty40 zx)l8V4|l}#GTL!cSn|T3&@uJrOXD%y=ul+(*Ms2*uqSI9Ja?jWfZAbqPXNI`K&O^6 zXQJ8HN-MYM`&E8s?9OYyZ>biu8B#je?>fJ6^WjsI)jg&lYg()u`jCQ7{eHho+qI)# znzOLa-5Q5$^%8kZ*7Hbs_~}z%y@;(M4@h_m((Y&Y0@WF!cvG(~1=n!}<&-}YTRZov zA2auDq!_h3(fZfciyC$^qInbp2*YOLElJ7=zk#iaY84R#c%qu%nzcznKh{!%lbEZk zzK#;@2IS?M4KDF$N&%wT&q;R`4gB4E6B-12dn7eFrUDUvBq?t>Sy`@1a5y(cA^K~& z-@*6xAY=ZE*-l4J(82%5)>{Tt^}JofhmcY!X$b?Q8>AaSx;v%2J4G7lZjkQoR8qP@ zy1S+O9n|0b{6D<=<2gH~_MSD@thKH|iI#{A2qZ%$lYsE{bb!>+LfKi@g&*fD_#UDP zYE@h9$K_&_c#KMzD=fCS848)$p|(40=^n;u{k9@y(U`l_V~=~qiiqN2`zb4Ds$(L9 z8$L5ms1Ao&3EvE-S}S7Aq=a4oDK4T-|44kpay>DaD@013oi>2Jri$xS<3xe^+Oir; zn^CqdSP;%RYpkri(_yW1@<}VD%~}o*wU1BV$7Kfo{@CW?#9()u**6~iKR;zz;RaFx z-;b|+3W;s6l?fA#rLpdZ~9jCAeYw%YFbv-g^m_d zAC}wJ3Kcmo_KgF?M8L6Dec;Yw*kb3h;DN1LQ@IwH@Ky7sc>)5VFkdqdn?r=Gr!^oX za~~=2J^yUCuTeSI$CJ62V0g_%`fgarib75h!F9it3JWQ z?7W%`t3T7l;X$#CPnKQvzE|*w61U7s#m-=4^7tBwWr~lLh-gMYW<%Y@JbaGk9c)Ug zKc(nnsW5;=#TQ*UGC}o=5F<)won$+Y!A93z`;6%5Xbi;MgV{~2>_O_H-+}KK6)_iF z%q5SFF(EkP>@Og^12<2*28W#YXL(}yMMS}&6OiF?*odMhzTIH7!!E1iO;FOl>U-Lm z+*Qz4pUG(Bz=WtH3x0lHOxo&hA+s|l(PFJXIgbAX67US|dWWTtB?XMX+89pj^{rrd z&71RQ9mDS9yFWXAs%gzM1;7*^J`^u~KL1sSvd#j=AMHZJgyRYM<`AJ0gjd;!?geCN z_-RyW!}{@sX8}u4q%1gAri)2E-JCl+*l39qBP1(q_Q&a0V~g%GZj(QaW0I4Ti|}`* z@wD7I7<;S9IS^hRwsk(qeU^D}zw+I+_FdU9biPamJ8>muo#Z?zLCn~lxnup5o@)s38zy+YJ#tlf-RWW_Lj`eBKM;wT{ zWU<=v4k-MNk=ZQ?< zfn{0ResgjrF&pRLpvrNt+KU29kvvUyRc4+&%-5gja4Z%UNB3KdotLwA{EwFW)P$zy zBc+?|?7Hplsi40bIh7amO*n4(>`1_L2qPxNjZP6dvZruNiiuSkayGiIQX%7FY3yZdMkEzeMwDfFS)_m69)69 z(%R$a9LK>jB-(QBICso)<(?o(6>6hYg?_=D5z{p-vA!s80o8N2tHL0r*g?T|y6fee z>tEbcw<(pKiLFwd`0Bw7!rMVF{% zZX;2$jW)edG;jHy?CmP`N0dQ9a3UDfW^Afdvdv100a7bz8K!txUwpyZ^;U}KO2k|# ztpK^D&gi(WTS11UNuFZYj6+FIL&LpB#d>?IySBR_;z^2Ui|f$JrWBsgd~>!NEal@8 zLyhK2J@}f=1<}Zp^Ub9zz0R>`_z^;=pn5%zu`H z#tMCZz?PZgKNj*SAWE2vLq^*j^X1uKa$Z8FvPLR>=uw0qGJLwir>&&IPN$sJLnO%i z&Bkf^ueuE$B-6{^OgH?CbiMhC?+!(BZ{AI}Z9Xt?Fbw8IUS$enl2$;{1xt!*19#g~ zh#^#H!U5Y_3Rt_M#^d;-?3Oq`OE;fMmxzp-q;$fp4DJEhv_wc)S=y@_n>#v)x z6-pz4lhV4o;*blg^w$sjhxjRt>uLBcrkk+}jH_F^0$2t-uc7m9k4WK}P3A5-Ulv2Y z9;n1GX}+;=*&Bdc>Yu4|?JL;U@LD%JK2;c6h#1X1cRu&>g>#*+zFEA#p@Hcry&n9g zqy06v^lCZR{#g6h!!J)q_KEo!n+cBArt1*voJ2wn1=h*A}EWEc3 zX&%bDhD09McavL59w%4WT~g;Q<(H!0np;`dy&M|s&vmxm53?*2is3{-%DHX^1l)7w5Z2<&V5h4Z7V5F zP2@_H8t#^CB0XF{q(Rq=-+8!L+Ed`56{fzqjB|`5{f&r|A3tDl+I)pY)bL@1k^QtI zuUz%U50UrhLvHj~a|v!w(>BDD(Y?G{J zEk_AO!-D2!>621O~3ED9mAgE)r(?S zg>siYYGe55**iM6*s`Y~;plDlchQ7>iLj8YD<)<+&ylhz*zY<+;isH9(F6rBo|-QD zT4PZp&+|EG1n)|y5BjS5;jD)HIT2JC5j7xg}g9UWA2Sl zX-oUS+fK5+mGV3c-;5`7`vk$@YwqF$GPQ_;CnpQ3^YS|v#M%19OgMvlCIkLULj<^_GrAA02{&oa(SQv&}fxs9|Aclr=SA_kMWW8*T~<{&Ppi zG52N8z!1)%>Q~$t>KJ$j_tV}eDeeTI2W2Lw;L0UhjBZJIu_2YPxjbi6cqx%UfKr{Z3o$UcPJ2%*eSG(+dT2d&EDwhsrh7D~nts#P*W*g& zc7@m<3?Nd7V>D^2GXW7E^NapVxxeW6rp&?~^zPN~lM2HiDt1RXZ7Ug=m3j?Gn8<`N^(-HQ)qhW;;exlJl&#Ow?`6sJHkBgPA zeMVi&?VEiXo8Ga`!`M)d^sHNNq5{S2tP$1Ui;c*olDlDOU8Ty55Bcz-NGIBa`_yCq2G9nUL=W&v}!5Ucy|N zbC={CZS1B}ZW1rt_vSp@+RN$`*&BXNYzrPwzIT^{ZcA>@srWJ=6=d*YbPrCLda;<6gX+^tinwwOyQhxM+1}eVB?lZ{u>2JTSGr zUer;T_Ow*JpEMr4^yhJ-y%5S5#P59#7Ghj4S&EZzm=pUKeil@D-dzSA2X;Qxt{>n+ z4}J{&1`qkJ5$E0N?s~S#)t-~>iR84 zcu>~+UIY3Xwp@Ari&Z-L+Op*1&K_bGVex=b0gljKU{Spw% z`|hdhpdkDpo`jkP4LLF-sP>b=k3xV5I}jNk7{-xMH5sDu?&fH~LuhV~dT2ghIS!t< zsZ~whXCv^`NYT=SDQzaCrATS7MKYXI{xk zOGvazArOd*;r!L?e3aYZ^RAaKY!^OKD*mz`GJ80xL6B>Qr`%Y-A7<4t%(v#s(25R` zy3lMx#&gkOB2I8cfl425QJ%iL_-T`_sDzA4yCRXjfa)`c1_jA6q@Zb>z!j>%gnUuf z#sA9wZj8vm_R?fbM11R?-!(Mb~)pXIGfW?H5sc>WtAKdoIiE_XooS&3fxx_(ts0O9t$ z%Cy+M1W~cv^dw%<789(xneC;vsYAB|`|O=iMF-##>9yHvifFJ9YrT{MGqL#}@&-QQimLB?_5x(qm6&l7Tk0x{pXV~iA^KD$`oneS@{q+>I)zsIK5AORJ)8sDB@^gvY{MRS(QPGuK}U;l=|wlsR@HE^}GLpR&-?!2vh(khMZW@gZ# zN6A*#zOS--90G9@LV{EoFR)2T-6ahXnrhmNjB=@`{qC!A*a|yivf+Q{(?SBF!?ReJ zQXieE&inpr;mRcN%_73F%#V69bC}!GodlEK zvc@#%HPzd$i*@kz88N(#LTayb9(=Jlqn|SR{$BBN51X0WLTgQe_BLE9TQh}yBM}7B zl}3#m)0=K5d|B22qAozOF}!rPyo$zrsEdM; zzQ6pkN87lUX-?IXWV?&f5}MbhRYM!S#ad{0Mdsnb>%~heKGQe2gFWENb2I62`9n^<2`eiMQI!uI{K70%xf8Ze)Fj8{& ze*d9zzd6BXAdhS08M1LtX*j&QJ-2GT=MvL*F5nbW0BB|V<{YvukRbh8VFAlHI zQPc%%`L2H=UmL3l+*BPyS6v z)bG9>yD>-2$4j=mu`Ysy%WM?4*a>m$uAjKR^jZ9KFZ-LTrO43SXgxRO;R6*kccdxi zGz-~~n;(ixp--)6lj5o(@u3bY3!7%Bg~%ehym!LDlpFTKUm0eE?RBIIo7UvZFS+-I zVe^EPd$>af9+nn#Om0z8Xax8%(TR@-M(6XOw5FA6xg1TZA38%VtB7dh7??3m*bT7X zCDqObQte-``bAmNh#;ePsq7+uO^Z1h(@(2Kd6Ua|Hx_|nA+@F~@d8)SAI_G)W3{CD zq26-HVSZayQFS)9^;Juu`HvA?sk@?}*0ZsB%l@#_n+uBHleI0boDne3(~3SKXO?i0 zQ&4&QDEwA>=wT^R+umL3wtMVU#=}nY#$}LHsp-L`W=cV_Y4=La`$yTs@o~)?AHE*P zDW1Cl$|!Yww-ZtF&&wzJx+}84=3;d+x(<61N_&tSgqzMQDzCkNpLUU~zA-ltmk9Y9 zp!wT_(r}V8t@!kv+2Xqo*;l?{c70|sHV>|);_!5CEx)%9-6@2IJ{?EV&u6bomG&buD`3wj5K;t7N=isbG{0fzJWsr7Y1*O*e`VKI z*&2Iq_&B(>aVxd(oLw-yb#IrkI=8aX4soAAmiilL4sMx#c=9I%KC2M2%pZxuM?U!^ z@^L?-u5l&BG`NZ3`lu;@njT1R>+)*Hx2Q$oY^}wQgje$wEN?kB1Y^1?Nh<>ty|cS` zgM8*v7%#t$teP%*oRg&-3D*bCXt z0KqF`cXi33gd}aH^i)`y-JYzn6PAE&jQi9;&8qs6tB%+>B2`YdNxTz0{3HhFmryRUSzp;FJO9T_4;lWH~}9rb=tfYy@j zcT96yUZHtl)8wC#DTd#Z?9UQAu4vgvX|6rz6mhc1s$lH6FL8~C;wJ5IZ#cD0FR;;M zeH(^Cm>Na)W%4>P@!i?4Ms6~WruEPTp9s>q^6ZS$7%EfW3I~}DPY1>ODvHi^X~Kug zi^kOC7cYw%l;z%oHU06r} ze-)IJ-+DdMavWL>x(z<_E;Tth8|oZ~y)W;}*T-U3=W>S-QjSaa)8=pom z;PSZu>3Zhmu|Lk)r&Dx?;qPF-9oVk{|G#jrZ#i^tnRC^v4lcxY4bz#+D#Ig!d-P=U z<&EGM+klAd%NI9=MA>{nt{uyG@pmxApI^ZQWUSQMK;mO#eQLfB@(Z!n9Y^wLU(rnA zi$vBL*lF{EonsiML$;b@tu&bLx3eW-LZ;e=uixVL+aF@9tTswN?r6LFfQ@WFD%Yt2 z^^^6Hlk-L{lOd0rj_B=O3aG3r+WDBz@7ut=*|+RZ*kJjz$?l|meBbe+F6tiK-c@5p zX~7_bN(7=o;%D^Fis2wj#0H+n&3%_IU^$-RiY#H2@c?!hPBRlR$H8`P+J;TN5d!Qn zNx2ej0%~!CTS>9ZJJS}oD;s!$WCq(~9Wu83Jystuz0K0N zcmO@#O&bRSHrIv59;q8YdA+}#-hUq2yyNWaxAFmU#q> zn62;j_=%85A^;o?2K)VYf9DjdFF+#p$QXEL2$mEAfgoB3tPH+^ZS_Y&#Lu!PT$wG{ zOwY7L@!V7&+Y$UoCKRE$)c!1!SY#zb^Y32$7aa^-e0r1jN#)-ESz9?hV*#GJt-N@3D8A(70{y(OIBPfZ#&bWW}^5b=nPsYV{ z)H)4@OC|Z=0;7S`l(bLL|9LvX$?=C+NrXN9cV~oAY)oHZco2Xy=L)Ly#~aMuyHn>I z+I8u+yE2~3e&V4&>*+5{seViCa82DT%Bq>73#41~ZysH71LD4)@M?l6yB4*P@7oSdBV z^YgMYI&c!!S18`h5u@sv>uZ;9(l|s!VMCse7wMqzp#m~#r_rJvrv)s#TPo$XmXBg# zaX|sNm$9)iVEF-67LbVzZ3+ts0dgwPUt@g*UM_$I54}-fio^)+5|A9Ji7$?d>&xt3 zR7RZ3^Mp0o++1>Tc1745PB9Y0UOEpO~lsi)rmkq z=ld9DcU@+YNc}=tw_NMy^M3Y7hKB=S!aCL+a=dIW+t4~xvB(nw9F%YoYd^ukfOgs2 z5FiOk9pUpJ-W}>uEv{*9=Axz!ak&!MPT16>9We^2qMN_DnHD}sZDBCQgLHIs$SWw= zJ2(K(4`en*2~m+_ee3rjv!u(l1;Og;t9_Kyn!j41Z%4=g3keN6tQ)ZRA0;CU5yXLT z$8baxP8VY}KY6)SPdD{^`O&bjgq{badE|Ro>yj4>lJ^_*39E(O)*U{Bpuz%Y7eLXz zDO%d9P72lJ^uL(D!7Glshvn15NIg+!Rro0J3GP!;2CcewVMQTMt^q4Gg;gWfgnc>T z%&PnsU9QtYba<^o*CbX|`dnC18w4Gw><@jmvirz~Jqv6k8u#M|loWgWdKWc~kCZ8) zqm=j`fCvU?l1xnUWEgNyo>0o3J|MN{fpj`qcOLrP7)Q*nIxos4(PoTYoGPmUvHYKNv z=ZVdCSxk}A$YCMM)k%QNT(rPS070sCbQF^HXCvIa&9{9`Uf8?!R%yRRF`=R$IBU8Y z36g$07P)YFp^mki7qa-%y6)Hfg7{}OwN~pE*QDq*Y0>ESI%i$hkQ0_sW@#d`WU1Qk zs(!`IzC)}|48`kprmX@eeLU3p)?FAvA3ULdS?^RTj_i>XTLTdz>2niCwasg_Ey zyvI&*lB6JbQ(aor7;z-qqPthir}rYr#od!WVf=Ytf%u~;mr(?L`+)BHZHQ3&=7|PD zWF!M(GD*6`xbzQ!6P6h^CNO#egWK>n*=%QXB@zAR@HejHrc{DNL8;yR8t>36b>*U( zzflIy->o}ol{gAFJl(OeJs(NLi%SVu1j35&1@H||8pw%F0Nz{RIJt5|_Gh$-O<4H? zriGHuty>lH*Zi6nx|f{iO$j~Yi@?xsellIROQxhYnZ?X@?^K?C|D;kfLTz3AD&O`! zqL>t^nzbr}2}f|MR4wNIkui$zFUn?x@fnf0h9L@_8#G!=<|oZ_pEdQCFb`~If15E-i-@fz0serF$5e5X-)d@lEUo*k{#8+6%S-B(5&Dy*-4>)<5_LHz_w z_N?AYV`OR|__hHThl01*2Aa-8=1~nqjE)AsC2YH7Jss`r>Rx1$7pB%Cr9VNoU8>2q zZ(Q0Nr-@*HK-|^odP4ZN%G{q#5}(6!M&Pvr>)>Y&)1Ob~d2DlWG=#=Dk}~4xc|iH5 zp9z`_lIIFTnKCuo;=YgKIUK-~QoGqU*$VKON_q6@yo5Eel@W~xicktV&r3u(ptWdk zj-x9b3&+kJhlL_|)yiNxv!N6xB_8&4rLrD>vQGZ^fvftJ=YGg|F0d|=cOZ3qW1?H@ ze5lYk?^~zr9u;0u(4W-vJ6b{CflsW^fkTMSSp~tK*nR0VWCEFV+H-X+7)ywqE!#YIxih<*qWj9PxdZ*8nf)1+y_~yL+Qsr0kNDsr;61Xx0OA;s46xt# z_={Rt`8@R-MQPn6^X2EuHQa##f~0~1(KiuJG=jNf3Q9_01RSyL%jqMJ7weU!kA9Gu zMV6V)^i#YccPuRx2OmLt!^w0W=5jE13BJiMEyWHL1Qw1oX|<}Vs=yTJ1v)yQp@NS4 zxzq-K7qI97TQ$Z_{!vN013L4|E8vji zE&9jE8X60TL=;5pC1K=5p8E1@6*CP3eryVIqsMR>joVuxa&``m7}0Fi;vD|HE4P+M zM=;ofsXtFfKu>QQ_jt&K7~_2H1hVgQ1!KIBE^Rt(N0bJfvvoo^Aj1TQk>Vc|Gb-(M z1y~g(GmT>KjwZ9U2~cz{844*hgygB55V!EVoNvL$+kr! z_vgMlbtk!TODZS&51GK?rb-`6jW1+rV?T@!eRo?tXa?Z?A5eM=at@H*5dA^arkf>IMwOui#OnDqjT3)+lsB&!e@ssd)CBvICWhx*5 z(WV>T@lQ<3>A*r7Bm4$);yz3M|9~tm!0JqR`GFe;L|~He2L9GSPdu3;{%*{FY&gJ> zK4?VjFL>4a)eAh7rHmykjboC1u_iPV#UmnaN?fL6YnoyeE7fdzTy>lTwt^# zdjCit+lKRKo8l%zf;qW4NAMaE^MV5~&xijLj~Ea-fFJ>I-T;!NXt*a3;88GP0*qzB zpKflPgH(^p27#3O(&uL{s!F3HbGPX2{ed}=Nwtyxcf5u>+7E>jiV6w|;yG1SRhO5S zz(V5@{+$4fwYs{REld8jv!I~haS3RhCKo9HCXKb|%hrO#wr7nE_*zdilZs>XFeaas zB>)iBg%cvwJA`f|ieY972VNCWC5jl}Z<9MFlW<Ck6hL33pm{Tm&EmNY zQWeEVM~)hPi(NJBJTyZjs&Yo{XF~83KqZ!@!+8cuS~`W&*(K+7<&vwwdv}=ddBsi}2fVd& z_~?N5Ibi!3Whn+Aza-zlc+ewC;z5s10Di(4nc%LRnEm+mwhiL_iiU)F{+QjwLHJJ|fOFJjCr z%h}bj5yh9Za{m-+ZmkeJpY2fCOzi&(OFFWEleF43rCFI|`Iq=vnEWz4mKCdZ0TNJwZ3(f4T+Hu-sH^yZwL@>r6e zi)rC~+FG+lDEC+<2|oPC7u%fE*K$_F~8F zCS5wNvXM4TqgYPfeXa@bPN7_j&*I1vS?&>>t znFNQN%!j=NRgPIUqb?&bGB;ahL~fXwX{k^krvsP*ZG0jf>qGeG&%JX@O#~U5NOHN~ zZe=YJrV%|+{4px-Da+NTBa~k=;vZgnU=g( ztf4(|QYgDe)7CEV<75)KvelthHG0zhPbYn`@G6#8)9sT=f!QQ<*ooRFc~5PkgaYj1 zp_$D?pr+1QT-bHxfrTi+1CyXHIUg#>{?KTz95|Amp04T44xD?FjNOx#D&k(%%yM8t zb9uhq$H`EZNxQED!071SYL}D62LKiqYKI!JSp80Ni(EWn?8nur^O_Yupz3LV%h|AJCrE6o zjL8d@LGu05i=$76;}`1ye6s7F4h3L6nkxUrpcYvr+bS+IQRiAJn_r3(t8T~HW(diUgchJdaGja!C@vR9#Y$N zkx|@>9JZUl@%scXMcN7-(=b$&SMV~ehHIHhpaTaTI+5pf~l(tcA z>BDL5Dt?ip`Qz+N6c&7yJfg zOfLkbf76O}3!i{nNB$KmP|E_Ig|n@dnAp>p`JQ=mXWd%F1%<{&;ilms!1F$U2Z(OaKBFPdQ zh5I~6AqD#6%*CB||G{T)gx4DwxbuH23Wn z;fucRFhLX$I%Uf)3w(U_U5`QKj+XyT#P}awVthPK-0^RKvVrq3A|hg*AX-PnfmkMR zw?xR}#h1^3K@u z=tZ-%+ba9J7gB$&&Pnm8Pp8hzkybN!GgC6Xe)F!p7$SQFrm)<=1h8~!6>eSNNzPmD z#r`oPrgQr^n~u2V;3S<#`QTec7z3C4or_nP0&(7_J>#+IRl#d|dw|Pqzq|9AUeH z{t}kgO_VbKiJ5pzuu~-c%TLvc)q^u^|H0);q0Dr20z}ZYo~**|EvsZ8Qu`RRoBwe6_S-+^E6-H%WOT+i=R^L3-KS`eXcuP~`R)F3`=$=Y*?;H%vAi?# zj{ugl|92Byy?%qrAKI@(&?V?cUx|zoeG<*#Dxh zqfbeK#6Ac!c+3z)gHWjXzw?LqVM({pGDiXi|HIx0OR!wfz5QTH*ZyWszp{lrbTWRA&YE)SL z%fkeZgAv54nl%=_?|AZjJNtXC0Z>aK`|C2{k#8H9D+A~ToZ;g7K7<)N<~=V5Yd6% zw5Ip2m@Q&p%XvpZlkKSI;X^C-(>70Jz z@IKA}6Z3hw&=VxWF4o6I*C{Xk4$m!}gqBLO5>*ei4eHxbDq-_Cu?EC3geROz)C0W( zKB>rf^iI0;W*B(zpsS9fwAjs|C~>}Q{3 zrQRAwl#58ei}RKjzv{C2mY!bKw`;&tc(@i+nM5Jk+dDu;N!FLn9T&H;8Kb{u5RHj3 zrP5q>-f4HGPk>zIK5B17tAr|@U4M!-qhRTYHm|xK(>cdUP}lJN;Lv;Dp3fnF{)u;z zTx7*$Y2UNYLTpq|%dqqxQd1P>f6cUy40;bL=u3VWBdOb>XM;%2KCZU{v^w*eDvD^x zSAV1))~)nsCv?tIt|#bRbk}GU!Su+^Ze~B}w)q-Vv3CliS4^8>aSEEuc^bQT=KDM6 zDG_suZ^)4h4MD|}3#ojxufyXAzg9QrJ+^kO+NLlO8STIj*&KK@wpT8Fr z2ANS0^!cCdGIahPy@gKLU!nN=wYerev zN!bY+?i%8ErG*d0`*xP5pL@Bm<5U-8K|`%$er+tU!Lg&E%!Ct7-7o81+50k(J!+qz zqtpc~oxsad$4|{sR+=$?{f5*1{8`}lW$JKowP+e>h*9w`duante0v&cPq{Q%_(Fa7 za|IvpAF@X7#~feTPw1@?+(sAqg8+N_|zI63Jly4u!tXyKxOO?-D$sEbbSEGy`@X z+a7Q=C9Jfwe%-95&fDj*H?f0#q8U`94&^0%L8DY*SiAY}S+KWqkWdYqjGyd^NTF*WrM>KF|s4)0lC(iyHV>1WT_irjyCU>*FYYg0TDYm7!R zrni^(G0%!6Exn3VbKs==kny8^bB8pm{`@6fe)w7ZIW!*vZ=>u}h)FegZzt!FAgcNt zK2NWTF{3#(z z@66)*_>e$s?+X@%M#U7(Ek`BFo7sFf%S^`^n#pM`T6-Hmg^@K(y*T!`wO$>~@r@nm zd4=VgXSD_d9YWO_cI$rFpn0fhe)iGS5b;`=OOTu2a3Y^;+BI@JY9#cbO|;|u$$sN# z6tj;?*nN2J5x@&6#>CuT-mWBvA`j0Jr9ggPQ$P}Fg!mJ~g!xGAH2{Uiy zv$nAh(9*KMnKX!z#!b(waWKDYj6e0bIrIelvfK4aO*>kRuE zg*+HjPnL?WK`o_JgM*+{JY6xJ*G^ss1zQ4X@amaaDjz(UW))>M#O)L7 zPhchWt(=R~!olQci??Vo9tJ8QDf;JE?umj9;S>R>d$ki2XI-OO_F14G2Nn#bIPG<)fz>%g+ZY)U+(N4zN6zNty!ZhB`U~QarZ69FQ^vf zKDCqaoqFfbXBjOF69mRI=13vS905fsRbgeC+o7waGh^4=<6JE8RkMa?w&#|3!%

me214o^75 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 diff --git a/docs/reference/images/msi_installer/msi_installer_no_service.png b/docs/reference/images/msi_installer/msi_installer_no_service.png index d26bb75f265f6c7106e4ff57d9a9992bdf21c2fe..fbe9a0510c79556aee3a809f5860b1ee1e96bc3e 100644 GIT binary patch literal 37896 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 literal 36374 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 NT3kV_RK(!j{|9Qm72W^< diff --git a/docs/reference/images/msi_installer/msi_installer_plugins.png b/docs/reference/images/msi_installer/msi_installer_plugins.png index 3a6901a18e220748db93f5a59b2be81b6697ece5..e58f426a47dc7426847b1e30306064b8c61b243b 100644 GIT binary patch literal 243912 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%0D%) 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

-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%0D%) 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 literal 52124 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 Mn5<}#h~9_)1)PURga7~l diff --git a/docs/reference/images/msi_installer/msi_installer_success.png b/docs/reference/images/msi_installer/msi_installer_success.png index 337cbc9eb509be7fb7bb7229a256eeb044302bad..3a58467ae18fc810220569e42787a1c184e6575a 100644 GIT binary patch literal 53152 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 literal 53234 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 diff --git a/docs/reference/images/msi_installer/msi_installer_uninstall.png b/docs/reference/images/msi_installer/msi_installer_uninstall.png index 26c9bcb7c9a090ff3d706795b66cb60fb3a90f92..6b5b69a576841931948ce335dad3ec77474f603e 100644 GIT binary patch literal 32250 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 LMKMkO!j1m}gh2)* literal 27431 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 diff --git a/docs/reference/images/msi_installer/msi_installer_upgrade_configuration.png b/docs/reference/images/msi_installer/msi_installer_upgrade_configuration.png index a72fdf2ff912f043a1f990318be6e6c28492c857..7ca413bb299e4001c19eca43a973f2be8311c8c5 100644 GIT binary patch literal 60423 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 b#|bgfd(vj&z7C31{4ee(X(_%{Kt2CIk$VwT literal 58311 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¨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 z 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 diff --git a/docs/reference/images/msi_installer/msi_installer_upgrade_notice.png b/docs/reference/images/msi_installer/msi_installer_upgrade_notice.png index a4842b8d6d01c92717939c4b2bad97f01982b34a..e5ee18b520807a73f3b4d4eae82bdf3adbe5999b 100644 GIT binary patch literal 61376 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 diff --git a/docs/reference/images/msi_installer/msi_installer_upgrade_plugins.png b/docs/reference/images/msi_installer/msi_installer_upgrade_plugins.png index 6399c99baacff450c07edcb30e968e185b44e576..3e7496505f742c42711479c0bd22eba171c6f4eb 100644 GIT binary patch literal 243447 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%0Dx!fEajQ^l4JEAHL8ib4FK?* z+x6}0q=`$?nf*`r;=um>>$=zgknW9IG;*@9(!5_IH!%QFp+Wlw4(Ty5asaq<`e2Hk zucP{INXV&EA*a&>GBP6UR=f&;)N74F-wfzi-;D!63Vb^Es%DcOolcp}frU%|S|!kW z1FuGdLA~p`i&+2!FYuc&ooX4*U3peKH%tK4!(E}V-KPE4!Pf==2!Ug}u@gE7_@+19 ziW!<>JGu89Gc3SS1ON^-I<~6MO^Ho?Tkl{2D4v`>`IzJ9OijHwtw(dInBEukb@^G~mz4OiqERyG2$v-D5Waq)#Nh+{&baRyo0OF5rzR9UlReSeb z$f;8yr*DJQF+2SFqcjEtcHKu!sc)x`eG+!<>QlC(lOiWFhXNodgVCmHoeq`FOQTfW z{@rJMCeHc2M`bY^00@!HPRtubN-s zEc+&12KQ@WpAj2z`|9N^uEa^3nW6+x+~0FF^4pn<2i11s003|uq_%uce$OoEoy6O@B?pMnr>hkGRr%r{Oj}SRIMcg_bL%r_2)^5b_t^HVu&mP>kcGDzr z)2Sm;mF5ePgvB=x3KE*XyctQ`5#P0`?vNbywoZ0D0E7&PQ{%@`z1<`%0G4~F-d!B{u}Rqg00;?fWCQy3aFMWp6g6(%-rnA= zZMI;cCM(x_#rX(H{MS=l5}zZ~=fVkT^&Mka2bWm7H(>H>;Vu9SH!Wz^~1y z@4u0T91BqZuuKM%qj+$IoqK$cc_v-Z*Tt`Q_JSvASc)eIg51xI+%8rjOAKZKSv4Y{c zySYk*ui5ntxmjwBox6++1WB;$J*!u9d3Ni>;gEZoj9pMrkh7S}FhyJTSRA%G_WHpK zSz~7Y-M796i(wE;=gM@Z$cp z%Qqf_$b-sB%U07sGCHlOTB|-ing(zv6X{s7LyNkCjO08TG|~V-aQJ?8 zTluq%0D`wVdoWH?XGf_eEJY@?&5>sVKzdrbb3hX}N#VCi!K16q%|^n^&~^X2%`!iL zEUutS8(*Uqs*hN{R6cj@?15Y6SN>+ugx@A~a%2NA3<5xJ=>bXCH!TW28K+R0I6^A( zMFxv!&JSqbmaNdX@{<=cOn&Br%-j1HuipJI-N56JdQEbwmSJ<)09gqtZQUA`-YhCY zem<@*D}u+S4bM~a00aPB4lBUBV7Z!N3+%Zd;1#?ifTRp20|h{9bOuxEgVhU?3B8d4 z02Vi>bscvu3&2D(E*{QK7XKKb9iNq!p2OtdHvmFRQO=SKP1#5tEBY0ZWg=Ix?d@Fo zo6g%L`EqrEWDx`bBngn0s!)2i4RR{HCc`P(O9x2=0-u*@FS%lSjv^;Z4SZm0*F+Y9V!I4mF<+S$U;d%ca zH&O@ySR8)U4z27F0#n9`%in~j9o=hyu2hGjGmb!p{`v;eR8NakHx|KFOBJGpA$2&wev z&$!|yL_vuVyeyHWVGAA@&CR`Q*6cZxZWM7zqbl*SfvLh_1LP#C4V7vHy|M2xMI%3N zXMRB~lUx=JF=^QVzJP$wspwhotk4|31D7k{<@X*$kVXSd(EzkcZ(x$c_bf=@=#4Z0 zuz13@wQF!#049oYb9Q#J42wK_zA!aiZtjDY$|3=P2$-^YIX!pv&wbiC*6;n@&r?P< z_ju>9j#zzVf zQU*E8Y+dXM0{vKQflTz$TZSM=tEO*8J!c2W^hc;U99pJlC?&6jd`k^*l@21tgYbp`}L=#mdFTeR&)jL|^z_@)>4 z(uEDV#G7vuj^_`v)>8|Y%NEVv}(wxqeBng59l3=tsso7%B06!Vqpw}=A05AX?#0HvS zT}%rEJ-I~gTW+^Qc+GUT1VN}oV9q3 z-G6I;?6%Lnb+?};O&U)})=YXM_oaqIkZY5K!*7JN>M7U_ynb)QB)YBy5rtIQLz6 zWL9Iov0ytiAlTe@r-5Qnp^U7ELCBovg3D&7X6I@Q00lycHWVbj;P7||+E3rLvWGim zo(&=dB%93!V0jLl5!`#^n6t+>Px)m;1ncn0i484cs)(!S+?Y1%*QgeM|2?Kzl}Z(a zSqEFyTV#A$O5&AonScI2dsi74)zP)jy}Nfc?nd0*U4Y>3?(Wj!R%mf6)F>_P?(Xgc z34{;`A;fK6ug&*klMn)R+K=|#=T|m+ckaxYGc(VeIdhI-m?|I59zXV`=g9ff+J^gi z+M}zj5=JvL1HcpEoCmLq0epW`gms-q7>2DQ(Ru+3FvHw{NU%>Bw0Hjm8v_|NREaMD zR0+V=-5Dbm)oCySsLLeZ@rA+F8g+?;sU=kUikDJ_lS{pI3&)^RqLfnrhAC2BuGVWT zF#y1LI9^a%p{=_ZYXQu#4_aT)+g1WX=$EQhQ)!IBErO?PIp5t?PrYq0K~!sCo!;R& zclm5p`@SR2AKg5CX8)(uo}<&73O?XS&;CNZ9*hT2dikeO?nzHCOTARkZfqRoBV ztX7Z=0>J&!`>jjw@bKcHbqnWp8YDY=;_Db291y~=52zJaB;pkpl}c3g7IMtOzxS70 z+Yf$lWarOcjOtTKU;2KC8736?_g{Vf^GIFwER0yd1$<7VMJTnIiNpyjOHtJ*l!nGHDHcj<<&zcZ)&pW>(PlJ0iI69JNqYmudOq% zv0obKii~yR#(wQTZqba^@qV5Ss0*JZO?W{w06Y^6fBw3d0*?>Kx}I=2wXu7EVe1cc zRA)FEe##*+)8t`4?j94QBbm1`*Bn9!gZD?LfX`FCc_{&SXRjwVw_wX(6v^OiP+PK0QYEr?vM`(pv2}MZ zbt1plG#jgHo_I%!^-fqY000YfLaEO#l!1Tk_|g{3B+hQm0suW!CpfQu7$FwWZz`C% znWct&P*72)1$wEZO6cZfTit~S>T!r)ov{Hh3CIdja4tl>X=)hakRz+HS|X5%r> z^>6jQj^c~G8%|lcBi1-y{0<-KV2fB>^}_HuRR0{~DYssGK3L>P`^@O}t)f`e!5n7iN(_ltca zx+N*@>ala9~-)>QpLWgOr(z$DHW3HRDxfqJuTg z&09bhC&FzC+{-)Z#ky_B~A(87?avSa(vSv30|Eufs5fdhx>=UbrH(|_{K&N z*Y|Bdlgj`AFv>UQ*Y3|%kvtp#Y{J@lichRRSPJ}_+zB|sxm*sblvJtoi0=^6cId*1 zO*nV1JknMZS@L;)k--%Bjv^P_J73K2I&^IFe}Ix50$)X0drWK^P3@?aKO~MoWddo0I>{P@1R`>(z{o-UM|!aPHi9W zSaT}Vxn*O&cNbHUleIAW_RZ%q1OR%)(_P0Nm8kg`0&ob7_M$ibxbu~U1puT|T;KfT z4V{kjmj@gRU>lqqf*u{%f0eFjFotk0!8PoKSvLQ5feZjxR;QB66l#{R^=~og%g-9x z-@ABU&Hln<_6rW`9Dw`>w~I`pM~v^37~*G36kfXVv|PpKae*aOQmG2SEWA;_d6T*d z@~%B9(F0&y914m{BuW7G?Nv_-K;WO+$iHmas`YtFL&wu9jgDm)rLfr(p0uW9po z#&WJ-d!z@~^cH4WyEfd_a%#p5hl6o&46?4B(YtjR)3aqvpsP?{aP!h5g80y`PUPJ> zJ@oqO)rYfXh8B}5jh^}4TW^7H>oAw=8~0`FY6dNVF~A`>0Br8^adRtM{BUs7aMi(i zOAfrOWe(cx++xaL5~y!&ugbcUrT6MLwr^Tgu!Bf>_vXDfQXUtvU?1Hq0blq1HxDWe zl$up*v@~NFW@QCMC3*mc{!`<`Hmr@i=;*owz~kZwuYYMZ|L*BBamw(K9g@Pl ztO(haGiPy}Q!~)<%5q6j6#!yr1OQx|eesTm0~L9yfH*_`YcoJ`O_lmE(6* zDjG-t_q4V?==<3}yiixSNUPNR^?{BM0-jfLocWc*JD#YyH3J?|NnkTg*~Ajg|t3_-_8(J@Z#S zRT)IVl;7XG?O;WtR!uDdK5htc1OT?lZK4zhmMnj&HpF~pDsOMwaz)azZ7T22h8?s{ zXcJF9S-SL?!m!O}wD)&zIA7}5reOg5g;0i1ub)G{UB+~65gp*dSLNNhn$6?YEH50a zptw|`0$>@M(n+OKJp!lDhM&!w5^Yv+J)`*DcM`7tPQ0=wZ&cO(3{sgyi@+fyZNj{% zu@;3_?-l|G9344Nvo61svm72l$zPtic1@0(i3!5WH@Q);YR!)u9u$%U!Nq}Asx`ka z+j? z{H3_W3`L?EBgStY&tpJqWfCRzu5T1Z2x^;SmIAU+&#}NG{iX#O$g6 zOO{c@sAST2$rRElq*C=;BMqZhRY~MJ76A8}xO(BWUXv1254C9+DxfQ~Ub5}S&KlV8 zT{9a?D`gTeg!5wba;a4FHjV_VRY;{gtq#DY>Cma8Z_MnImf5p!h=n98&&<4mnVDg` zf;*1-`pa`&CMU<9Y||pif+Ks8BlYY3<(D1YE-mQ#)kB-qq#&aF$(cOsaZ6hX0f6h= zFxqG2SHp%rYZ}q2TmMecZ;dnljr#hpUe+V|b=Q^_s;v7Jr97ovPO$*MF$-)Mj?MdW z*r><85v>*vO}F$(OK|#j<$yuz)UcGn!y5K(?D%EBriy{BP35m%Yf1?@tsyA@Zp!2z zrr+s2rb*ncc5Q;K^hGaA8hy39w;d&y%GBiBiCM3bNMu^pKvpCYZEburL@6az+FF^j zq@qeKui~O>vLLJ6x9_ScUF`@~qmaqfWUU+-okCKD401L1 zP7bvDeoFrV=aa+x4;Yl{YgxB{!N%62c60U5NH)rxk^U?_%`m8w0wB4R=v9?x=JL{!t5%<)37{TnYm}! z@D)s4hY)jJ=9R;NJts8v;{$+ud_&v0bH@%DmF^NVbY*`~%Vi2J!vH1>nzdp@&Vb2D zNte2`ixkRpGEJK9nA-99vTh4*Id@3*ph_Mdl0}Ugn9c{h)wDHTuJvgZo7T2fnu~zW zf1KyuYV6FBDTy8Xi+6wDrcHVCcw5zzds%9c9LX62jZ;)9YSjZ1w4Azp()0d*!;m$a=&du>Pyo)}WF}$vW9eD7CywrmjEVW2Qa7TyUc6h$fNO+qG+EFVYmgC=|As zxu&<%zL`DOl%<{+mu6}Xi$`ovA3Y*2>|i$^Q^$}d9=uVbdk$_M7ut8=_)ySQNtLxD z*deBEYpbo}+O@seBAk(z=Vp}PB5Spl1l)1-qVJ#f8`dN~qf3`WEA8tC5VCUCq(ED{ zgf#yZ-%KA&UTYBBVNt)Ncjq5kDSg{mL}?@vh2dR{PAQR!7y!VXMz8wvZ09M7u_xO$ zPcY}ma&lw=edf$-?^U;?z$X2@znk7IwSVVk9_p+I#pUJ-d38}1fr(-0hcAW=$nXws zy>wzDGy8_|5o^AmG)Q_mv|*2-je4Zn^zGH8=b)Az(w8ObGNpnNku(6;bNbq4_c~8( z9DlZL>r_i^X>Nt@q8*=EkZP$+QRkqKR!SstLlbDNL?W-vK7lBWq)MuLJJ8itRdPMe z0GRfizUp?DA&uhicWRwzi|gKGy$TpGYjURuE=$T~Qq9|G1nK0`Dn@7c;FA(>`Qyrd z1IiO3n~rGjH^2Lc!hpDFck!Fs=josk{Zs1)Qb3eiB2m`JQz4P5m^#P8a=AqLUZxD| zt-%v`iaUZv2Ww;C&zNZwXSuaZjSY6bu>+mYXF%xpKZ zS=#LmO%hCGFU}mh=`noetgb!)z>sQ*Oi@Qdv{G6n|ByX^(JQMYa>J9DRpV8gCbStk zB=vr3^W;Ef?$x7LmHn5l>l-Wv07j>hNmX^scC1z=k!xzxpRgKPl}tsxo!O)+xtc-% z37z{9eOI(@|0X@!O7ryILoHt+moW$caJSfo4zp*C8#1zqXHwtkA)7}HxvY(D8fI1c z^h%ab*B@F2*4xdyhQ{)^U*CSBiiM#ISBbUn6PRIa4hnwtSU@v9x9+kJ0!@bZ!Xq&NH! z!+9L-vjwAv-mztC|K_XL{J%-B>2S%p`I0^v}%d?4$PxP!wp~x~Em$0OyAZHG4 z9^JA-qBl|as!)Nu^_w~~6^`w@Zq;LaYdZkKHl$mpMrLGLVQH0$5l1xcKDb?&34vL; z*fP?R;xY+k8aVu`)k6{-4d$cJ#C(P+BPn z?)v5DeO=5gLlRR0?F7KFOzhe@#tw=Kill0;Z(7ezX`Ut+VleZ_!~|~}ey#o2);~Ep z)`Jg-WrV)TsX-3aet8UL5|q?1%n;xkA(pTRO>GqDBnHAVwq@5?SGMp?iA<$6^=~q` zX9r(v9spulo?B>aw0CuMJA_!ieMoBK2umEm+^1P&1664$ZRORdQKD^K6=T%rcWod# ztY6*T{@(RR3cg$Kc`Lu_oxra-doXDbk`U`F#zEv9p58cwCoL+L;z@%hebzn1)Wjnp zF3=PQv3o+7Rq*`}l z77-p1sj>d{ocbGSj#EH%T!147aL14)9aDX^#c#@_Naqy^ALg{5*%QlE+AyM$YqdnP1?I@s8m^7X}qC2|cG+IjlYDcuaNb*8T2F~MeK zMP(e<#MVh(T$VBSj!X!5=VQQk4C~N7&6=usQ&genyLIT?A=<~u*4m1yC@8IzgH810 zMT>exS^)rV>ei}fTQ^KvP+YFi;yw-A_Uw^vE5OB`Nv$HRD~pS&l;Wln=MQP^~rnHFSRu+|3>HJ%am_DqbmB1k`EyhvI z;aK{#>d?%cD}7y9rorr*x9^l1Y|q7beo+ZNd`WSo&O5DDtlPVZa4^ImaZp0T2v@V} zLuoVj)Rb^50uW;Oc7Z8LeqsQgU1*O^4Nb|?!qO@=BaUd?eNekFlXt@y5e`vp65KSE zMP-#JX7CpiI|Oj7{nHa8EeRmZeG*d|C<=>|7Qw9=hguPWfVdbBk)%{Yd&H+FMZ`9Z z4*}_$;z}~C=j6|O#h6*SCZ$B%@_}m|*sXID8@8&TxLm`tZ`89}s)sco7Ps(Dj1RZ2 z@rz_>Q9w#+jGGw%Vnoi#DN)ria1o1}xhAD0G%&%c2fC$KTBB$O5k^?~wCvi zy0oycLZ-D(?7QUa34x~A`z9;Q+$$+9#0m#M4BsI%CC0zTF%q$?Rb=CoVEcOh{v7wX zF0Ep1Bt>t^Wm;SRl+K+SI+_r;rB9c>Z5&ZWUO~Br>)3PJ^0^}#*Lb!fu4#jql<3-4 z6IPx{X)%t5VDpF}tO8Qfqiwi=5M$yN(J;)@5L+D~#@r(=CD~tGQz2`gxYQVLApp?4s>Z=VTG3K+ZPhJJ~esKxG7Bwa#mfuKB>(^%`1wFC2F(QljiqJvKP8UHHz{TVPM)IEH=cvw5SYoiEq<5 z%FRq5Eh?yxt1N;$E?zP{(Yd}8ILE?HapB;ZO1mzN{5S%02XnE!xS(97G7V_IWa-Ql z7Xko8jv-B(hGFF;i`7_z5lwP!eUqcZ1nQ#FN{wgJA>;a`S()2L#Yee1 zJ35&W<%NY+3VnmrA*&V+_c!~1juF;g$!ReT?*|}749_|!B|XZ9i?J-L)9DN@f7M1% z3|NZhT6xAt1zYkl1_4(AZ>E0vUmSMsPi3rBT`5VE?$ zd&l;lLVc$Vbm!`8w=^sQGuMbF(f0C^(kcx%q0f{NO}uzEzDY3wW*mlOISqoN!@R99 znn4&a6fJNFNQw^>0oN_MVbfSYeQAEF425WCxHNR)rt>}Ztrp0`;GN1qCpSrl~?uTDBG&2g=2qTOz z;-f(I;D5St1K003I2B5@ND zPp6V`1m*@$<})2tq(E~ zLJJcCkZP%#s&xtF@Wf_f9-~z%bgaO{T=*`x*Q*s;3KN)!1vTD0v`($okpvg6t{FG; zQR9=_`Uqv}cTR)>yE4C;9G{X5~<}l?5_#6NbAjC2ZiMV_L#xM-SFg5HDF?zMEj@ze< z)ZjR7f}@X>aF{g&uX&emu{AHnFaR!ry?U1EmOdlUjIQhLieW(OWHMd#W3|7fG%}gS z@XvSa{uVlwLif%;aE8(rmggn5PqNnQ6z{E)q()9^YX3o!)y1lZ|ejIuSt2qTR6n5;2*()M!{!{z)frDGVz z;cz%L*^*e6rD@v0N)2SuaN3Ju7(&Q!73gEV+CY4NEut``(zvwUwUNWc5QF}v*hq-M z1RbXz?!w_J|6H)s|BRk}nm(idZxC9T61cSFRaupi7Q1(!v}DDk*0vm@Ts|@w7TiJ` zMtPe5=^4z#-h@#~iwn!-8rD6r_rhX zjMV=)@c-?pLi_q@#`ALRKlMIVR`BfZlUM4$%`=2nmORSLs??%?f$;T}PcxnttEm6* zX1*ft;l1aDu8UT7jzK?0e4(ClqH0$1}GJh)8Un=wByzg7W_E{Z|W06eq zA3AHE)FICOLw8%4{AB~1zZ`t!^zeXxX3U-ZvO}*6VYf5(#G3ytFHk<%(s{(;IeV}4 z3;L@)t9p5P_wi?u-3CWE5`Rlwi%$-2wz|X7(L%@H1sJ8$=n!C*4HEhcXmWEw`&K){ZzxO^ZMrA%O%O3J9(M>FO$)N<%61M^*wif zRFF!g{%wE6+J%T=7>31oobv16wP>}n!>&iGx_Sfqk?-q#x_EuCUGw#GuiY5r_sjiIeZe7?jr*FHaora%$O#uMXWiA^%Zo!^g#RaeLT{^UR z_3XZF(i^oNd-SQS=EoI#r;VGq^2qB~FLQEoo@Zz0yeiOQTf8eL7zT%H({wcb%bN#xd^>!?$XUlS{x;R) znV6aIIe%eO$V$%d|9RK#(m%cRd3I{YmJ@f?e^(1Jp3u}>Eclz$>)GY48}>f>!-i#y zYhr35`YoL=yuD||zT1DZwNHQh*!o?kb3aybbj8EX%XdDjNw?+Fc>2LZYnwQKwCZyB zVso(oLjYjXcJ8J_s|MI%004ZknOH!;KMY1!S@7~ncFvm@j}NSxHgVCmhp!4=WM{w3 zFI6Lm903o3DG)HJE1U<0&KYj#^Xgw+N?fC7MExp4{)pelL=_rc&qt`)xFRHS)t@-(( z0;qus=AHO?Y{0TIV+aSD2$Mh(RkGK7!3od`NVtX0qBX=wP>eBvK?6f(9 z%eL=%^7kGY{RJ|L%Z`4xDR))t7(7l1%=E5^VeJ|`oq)V?>iED zb>zEMC!YMxvjcN^>)h=R6dz68MQ4|-I9OccSZ3y)(4kFw1M$bMH39&>PeSwdsR6=| zqsM=4F0t=Ce)+ELTi0#bFu7~P&{m^=-uUyD?b}y<(Z>k`+pzR@Eu$^}p8gYgbXz>D zS?=1o#|o?eqq@Ce(HS&);WW?xwb*KS*^U*uL`JuvfDL ziF%dCaIQcg;NlpEClHCnVv&$fe8_f!argoOug*6do>0JhyT;=jLLd@}#bP0!;BX0{ zP{753VUU$y!=ByRc$;A)jc_hcAmHH`&Jzg5VzEfT|3!9;ad-yh5{vkCvqo7*7J-SQm^N9FT7m##dOD5ZB!oo3B#BO6EXFhIZ&ig5X> z`*&VQ2y>_Km>`QfnkmhD^dP5{=9&9O#<`pSqCP&kd`_VY8a8RL>~O1nH|GSWReKdM z>awg?64&4WUP;!i%mPMW;T<01ZvIy0y0RDdp1x8ugsn$-r1v|y0%Wf1Y#Ti=t!qJLB_Nt&+a{Wqrt@P!O?yW^`nn5^1Pe3&c3Qp zD<7UaW$EA?ln~-#1^}R|xPSLyr3NhB!eagH0Dx8&KFBC@j*4_L!2kgCm5=V`Sp>PN z9$vfux>729aO||1g+oYUm{a}1Kvg}tmszMFg_f?-u{G%d)g_rZYM%&i{j2+TUR3cd zoWo*+Eb0hSSN8ng<9s!U{GvldLgIrbIJCUzX`#+9$fM%%jc4VQt#^2YyCs13*{$nu zlpF`2s4(}s(fjHA$9JEXD7fZM;n5*hTmZ7_{=G-{a^-T)-BYJICa%G80nP*fDb34x z_(D$MR_?)3z7BOp0$udvURIGDvv5!L;R$%0Iq zSpX}4cK^|vDjmPB z&dAmH1pAoQ=)bxo`@z$EHG`Wvc!!61R1+sk^)lzB$TQqQ`||4jT$*d?7aiqd`s*7B zBzcupjBEqE>O;p+4+gXHbbhxW20BDMz-+z{;VR;T-kzt+| zzkh~K8ag}WhvsvaA856z6AxrREm_Vi(farB7|ar;1#q$E45#LUab$<**Ww)|ygDc>!$fuYr^?0Xsc zRa(NtE+8b>(X6@vbuk2)A_c3k`4;U;r3LDB`HzJiM7z z&b4p~jtI8p8_dPhoXld|kRZGuP86hZUcs-(jpcDb30%7C3mgSP2k|FdQmh z+$+VM{9LVYgg87QrY_33^`un6Sh$6Txnh~w<@SF5_99GEmi^?7#>wBuiiZ@XFY={= zzyRCadpBNG=*^sb!UH{o96eRf(t>mO9IEhH#z~&C(RhFDkUi~sl}kty&8xuh8-xX z*R1QShbQ+)>|f}5D*vm_Q5{!4LI@q25aru@gRTs^&C`_$y5 z02f=&ls+f&Rfca1&VSP-);}yRB`L(&(mi3;v6t1&L9$b;CnkA&1jfcE$A(A7Ou3H` z%HP$@C;juUzZ^HBXS3882cbBu)7)%%^$%X3UfDjvJ2)mODZ$M%JA8ziF z9%3WrTYHA3CdSU#m%}2YesyI^>nOhf;y@k0;PB+QFmIo*K|ftqut-~Yrmd|>%Ip(5 zgb-qGebdp-zV)G;`^%dry4#qD?Y$xsk~>a4sH`X4vRkwJCi{oPq$NjKTRFEHx2u#y z2qFF5h2G|kzgRzaNbgRKk|Qj*W-TYKFQKY`Rd9Am>u68!z}Te3v`#(xbWU;c?moYO zsW0G))API{6K2ewKfZU1NMBnMr^I!)v(7Ca*Su+?q(B=p-zLkh71!2d*X-UQ-hr{n z$zeY3$pgN8AVElWYhv$ocWb_hop)?fT&G2cRS1=4?Cu#K77`hg5bbMY88mIreYK$k zvi!)x?g8$eq47yEDXqtj=pE>4GI(1qLI~*#Hq7Ya7ZjF|lHg@)7v65_;|d08UVZa< zx|f{^-`YJUCZ^+O-{jLs`|^0-<`IDr3CRh;Ru*2PR-cuBNO7vN_V;TP9uORz7#HU0 z8#{N`JuO12r<;PU#68z!8ftTVQi5st_y>>93~ra;Y))9Z1jHxD4_cprP|3p1NdYZ} zELb$PSKkreoXAD;tVKhc_=Q9#r^Gm0Iy4@<`n9@7pC0ZW-ZIqPCp;k~IV>#ZixW>D z?wA|r>nP+|_(es>v>tKfCCgl#Vd~uWVs%By$J?eij_~pej!RAq^{{nr{`tn23WN|s zWw&}I+jShj_^V01o2SNkSc<)yP0J|Pz2CaKOWQejoLjC(2N@Sm6`zl6-8?lZ+S|$|aMU-~4ZqLb*4Nsi!{T+b#t-h>ciAx&(my+~Bq=g5JSs6J zsDZ6pM~A(VG=Y5QpZ@c7ik zAO{PthV#$n|Dmb+{_=F5MbfqsR()xfi&^l>$4Z2dKL6^MeG*(fgX2<@B7L3g!@I1# zQay6_jCOJFwq8^7^VcV*g*c=iEc(zQkb8Y*=SUCVz=Y(4K+njDYp>A=$+C|PPKyc- zk4lUWwlQ@dwf?f9V>34n3HMK%weqK-9h)XZ`kI)#^#1vV`n?f{-t0-X4WE0lu;#OB zO-C1xiH8wFNb&rK$!&up<5H93+-=Q=uFIeiLWp^GVr7$H|FEc}*kGT4_zpkctyfpJ z=+>6LG2WI!!qzJ!GCY0e`kQj<*`%i4DdU!YH@b79q&Po&fo0@?0}slyI;BdZsmeY( zsa3SMUr=0hsHca2w15X5y$T2^AHnlx*1|E2-HA&JRx z{$8G)zS#Okj}SuA=ZA-OhzpHLN{I<__m0?Ez&zbPw@H8tUts1P5fhg>Y+t6N-Yod) z!nciMfP$gRnY!C+$0vt+`-jFS#snp_=y9q7p;sHjd>YPP zyLx1g_Kh0G+7kSPkt=i62qC1eIMl(qL7#;`&F<4CF3MZL^PYe3*5w_`x;0IWkMyy& z5BPll!(zd{V4dE9KRFp;qOdSeO_QGi}K?6T3A@jEnJdu<=P5 zbnHo)TCGRAoW*V8+f3P6p+{8C-XUfd>eMo+TuoK2@97>m;OA1cN}<$Ny*M?o zbDX=oZ%lk_N|ScIx+a-gM|^)*idc2#o+&{tP50$$5UITQP5(gOmb1TJ*sEp3s8A0x zbGM;u&sV5)%D37fmCBV`WzmtZM@0t)M#aPiINQ2K_1JsAP^HzXt8#ZO=p7vq78M)g zW#iIz{JK0ftCrXPQl?f)9~@cMEHXGGJUTL{fwg1&;)4(58jZYe`M(93Ojc1*ar(D# z;>3v?H*P4EO6ecOr$|P7Z8b1X`o)RGL5}9#R^2BNVwE2zqm{=eIs0_HBu5Cn{wm7D z)F+`sH9C&n`M8E%Jsub1G2}q;dn`3~c7my0OuHZUU&xc{)D?N#$2YdIZgDtYhjfnz z2RXK1bd@wbS$3*@fXC?FIrR$2+%P;QddQvHmAUwEieKE)n`(s07PfVEYc>CcRHs)I z-MDx?yG(-+b75h-xK`6&vIrsNp_xs@!l2>boPI6UC`ulGk?Japo0+Ra2x%@Z>L7AX z{pQSb1*NZiadT0R0N|UBKVDjIHl#@1>yxu0{n8G+CiNN(g^={#&necz(2mp2KP}R0 zB@Z?ax4|vvU8q0^m0epN?i4a%!wrd!QI|Yj)yvDZ@uD{$>IC(pb;$woTVBx!p^D32 z1-UdjP*T%BkGG~fT1R*L{P4{zxlUi6bK~=PYxlH;`3NDTJ^l6U`QPo&m(is3^-qJn zMJ|IL=n+E9{WU{uF`r2rFBix)dUa*i^~?%{(CdS({H<&o4c>7lPe-X}g zMhNL&>~8Mr*n9cO0u@EcUTvS;*e0xBhOB<$HC1 z?7KpcUR^h7-qH&%Dp^YLY|kW1fz`ZAQiPB`C&0h&()Y<|<@MFER>H=ES7jDT6(vs>whFXx^=Ld~ z<-<~~zC8QO7B-d%^9>?@@NF-9*VY?vywuWk#k11`qAb%UU(g|n)T)0PM@9teRe%^tcN{wD!wtI9E$ED+$QWhaLW5WO|$DmmUA4@cPS^mSH$EKM94BeHF z5JLIqr_7qY<@zfvtuJ}BCCP@@a@B2w5Jjo?kBN8f^Q~N^Qfu@qLRH5Wcb~sCvsgvz zCFd8l7gu-51@+F4L0w^1S;GUrP~7mz|mE@7(cFu}r1b&7as^5n#-MqVbZ=AYIegru3{l0~KwoqxLYM5$93++7!K$7!65{UM=Po){s%J(Yp{*ADiDo*r3VUyM_nVM;Fgn^z+dI1w&Tl ze$%sosrT4t%paMnCAT|y3Y!d@JEW;=aK9~Dgpe+Gen(fE)Db7{y-`zoY2LkWN5+YL zJDn{;2%&?M0z(FDt=-GNIXgGex81Rl_on2k2UDW0yc&%;o>{0RwG}U)X1yq;5%v7U zicvFmJSkR^ddcZIEqPXrcIE02LN5+Xb`V-NAG7|!t4gh+{O|W(pKee z9TVjcIU!S1oe)T`*Q?a3DydSQcdToG!|?Tw%1cU0DkO60<7u%XQ{VLEJFXN|NM7IC z(!>QKN31U*SyJ+3VaEXfMib9JFO^9u?`!RE(sQeNm~rgCJd>}ZnbO-oq}iA>|7^qKH)Ul-FHWo)=>Xh- z^cAlVOX*6s42yM$9Dep$8O7*dU0j{w?mTjHE<(C>-Q2Ak&&{sX(dx>_SC3?tu!to! z*OqyDrtip6snr^iepdr1<+Y!t4qJ1!K(5!x@>X~AGm9SfK#mZjFS)cbJRdZ|c7=_M}~cWhu4_4x}DA%wDb4|6lM9`@74 z*Ak6RRgs%{J70~^i_PA4HeM}1KX@l!$Ecs~{>%Yv#~ds~2r=^0okXJG)^jdB%vUOk zc7EQ%!on`D`;2qhB^r6j_TkYyuf8`b5klzovAO&oV|PhAVCO~ zp6wiCWtA{s4DjC0qqaH(jdL^)`lS# zgk$pwKi+s=Bq_~aH#A=8)O_dTDxLN%rIRVO>Vn%}fAQI(tyc@HWM$8e_X#rh@Bd>N z$-X-BMUa!%#GMbSq!rI^UpRCAR-vTMHbd+=NKy|_edCZtAhh*sn3kNF zm>8Fk5aC$k!GyWFgmNpT?|Y{qQp0T!-fKvwM(!4Tk&Sz=FXyIMX6`?i3;d9-9RnVp zIg+OX0C;}p=nGy_&n7;<+pUbn34u&Uk}U8{oe~-*`q>Eq-pOuDX{&hA&8LoT=4K%@ zbqE+8{5>^l*_aBj-R$@R*ba?!?5*TKnT8>SWIxZCT0Xr zDC7bF((4%K4r4!W9O%RoScdl=)jEuMk*5Su9bLCk8Qpzsmrz_QCq-`k2KOZ&9=%cU zf!g2Pze(!Vr;`T<0BllwG*t^-#foTrUa|!|A=BBl`>^n~zlxj{Xm0;UyXy3rs7Y}Et>os0zZYtt&2op0d zUnJyV8ZJ8MRgX7%!)*DP2hiJ3XCy&C{v zLUSRH$K{DE1cX4qujjI09zSr|oXL&6t@RYgtzqlNZtUI1ud6*faI@~iXSN7+APBRt z0mIt5aUW)t06@mxm6^f;YnS%$wH5KicJaMukLl^9P=6@45KHP=_g2FO1v^`qIs^_F z)o;&f8*4+=Wu(SLcwd0G_-4K=E*&;SS}$D znTR+XzKID>BouG~<3)CvwrWmCA8Ud};>3=vy)}7H^VI+)yVf5J>izXs9YQRHJagBu zA+u-1cnh>T3IM<gB;;WLRxPLQ zn%O7Vkxx?ms7@XHNXhNzRrT6bbYcC@B9AFkJ2?yVN-YuHYe+0!aOUDm00Iex|1<~O%(f1&^&kgk$V>^Na)T964}tWp245gyizy4H4Qb;dq9}o5%XZDpWf@szf23T?*qM_%-~D;sx@+E(XLJ+* zNN#W6e%WNsH!E94H4t%m7Vc4FmVezEU0ru1>-W2{zdW;UcV+0xt>3l^b`}r<8?V3s zZ$}Prd|H0?{eqrBjzXH^MYih@f)zc^kpcilYH91RA)gNl_plb2I{$20x#?$vl7*B~$F*xWXrS5abLX?wfEbNNqtR$c zJ&AMqW@hGmoFf#OSy-Bjc?5&|gD7muG(B7(s5 z0*y?^1&+u%B)M&{0|po&v@qr1JQFjqNF*fcIVy6*NkbQ}9nr+a1Zb{Zv(7EKuXCT5 z(}45LO@&;7BQ!H7cme?*<8mxbc^sZlY$_HC`2+y8n7nCbpEM@{L-UQ<+AmFj>M$YaK=?<@-zR+@cVpCHS6CTbNnOazyn+iFY!6UqO zmZmhUb!_*+T^oj32*u7}?PkyFu6uO$-Wv`4j;m=nkEeTnV*71u{DQe1qFu#86W^5H z(O5?L@SW6nPJrJpc3V(PBgk z9D`dn3X#8jR-&ab4hPrhX|0|mOub?oHAxLM0Xj&izl>l76RfsG;Gz# zRFa!hs)SD|^TH?QGl~#{aYH*z`l`K~j^sIqgateQh9^_O^Jhp9>}Uc2KqJgHx##!^ z&E9*8#}i=q`&T_#hPJYFON@9+c0~?OuDZ-Or3eC(I(D?!zVT$H$~T5ScH+8i<1Zqu zKBRMX!xbAoFeHG(@Y(a8Ojy_~cu!Q<;SZ_l%yHAi!O{K(kRu+=X3JO5} zx|lU<8efO_iY(2!A134!%^-y6+B?-0MSHvZJJkq{BeWJ;+$}Bw5ESdMlAAxZjM<7< z1|h&POyppyk^nTQS7PRly;*oFe)HT}ga9y71u3>y$0SlhpRI}3u& zD+QoRUhA}fD!u= zdKLh{YS7ZD--mS`OE)~5*WdhY1z5x~6u|o)kVsQ}@$l{g7ax|C78O;Ny~tOmR`XRF z$D<;HYwxa@yW64s!YTmB&z4#?Obe`|USdlzC_m(tLsGP#x36tAyWollQxg~G8qZTq zAVl~Zr51oG%F}b7tQ!`+1!Iw+No)fj3#kmi3TOr)pxNqW9@msTIJ{-=m8aRS@@4X( zXKxs9Ak*Tv=e6_tgL3YZ>(>P9jMu?$ml z2}>`@I!??#^%h+o+0L2M_E$d+l|pq^P_oH&5cs;{lM_ z8D$=^y+Ys1VN6jBLM%&2EY(57KFRo)y@y7D*Muo9Y-GJ z*2EO+T8=7vd1L?XJ=dQWm6sHi6ugiyL7YE$izTA{kd}*g-fPjngP#ciNM65D`NSo< z))BKXAgqDzJa2( z$!KIGV}dg#=JtxJN`;yhDdY-O@#fFk?M8YQFu*Wic&;fFOHsdgZ-=57ACCZgQ%0*L zX`WDM?j$mQRZ#}C*^B&A|JeR^JXWJ883bx1u(INS1^@t)mc7=I#dABH`+{Z>U>F80 z*w6uq59>+fS^udeOccKGhIXD`ZC@)u7P zrilhGc*DBGP_=U>MYD)yYNsP4$-cIG|AD&?UYE#A3Ugkv0$&yY)Op!TqE&kG`@3@< zFF4ZQw07dO5o(lqr6d54VJ$2zg92QtS53sXakjRzs2**IO9=HU4aERb$#lx1J1f#- zzGGoIPD6d9TYb9Iqt-m_f}j4h~gsa#Yq4u|6gmm6m1^Xn|{DEqs^p zR`X1T>tyfk;ULhfWE6pM#THg3y2^4T%{tb-Ov5Fp+*|wi9lVfJ@TRa_S^hFh8WpzjelQkK+VGqJMQ~*p5j8}bFr3R}gk-_V<2OGWlWc)ufN3d` zVgRbiF|Dcq(%#91iz?2KYu_)^q2-{y{bIud?Ov^EHu?(90N`A}B_E2Th5$Hq>RRnN z4uIpV-Xd}Akso{UX@i3`Vlb|ymEDIMU$dKMDGkptcHXX?jjff{!b2=!W^bry$>S%C zZGgW<_vE>(Nt{KU_31;`3R&s)8QmwJ<@O#rV8nn3U(c#p&HE|z)Z3kA?QK@{wg+(z zAf=9~6%F8k(SUyCLMJlZu|o(UKunF@92pom0B~?%d9Avf$}!(#M_me)kzFS0_ia`s9Ls19vLA4;(nCccfns ze4pIy5@~1>XyjTF;6q^$I3EhjNbm(nUipr-j8y4J!o*AjfDm9%=Xx^wqoJ@d02Nu+ z{;iZsxO{h80RU@QoNA}5mq!M-A5a`VdT{$uqvL}#rx&(aSfQ^T4EVeGsa*#SOW(BY zaKX%|!+SIRo6Qb2`?E7D%UXnY85(+V?GGz%wO$i%ivs{zcyxH%;jew#59m2KHq77b z>GBqnGMPVmrP|cW%Goj4tvZG?Psrm+ij>qlTROFlFSIr@EbWX$LjqKXjn?TEA4tfJ zb1fXFWSw2N>HDv`w?4LT@4j(Cv|8)fWYoT|2b)t`hD9_@>q$~*!O_#quj?Gb z0Nxtd-s%!d>j~SCu|I8X=crMT$WUuSY$HJAuWB2ry@gU|fk1#jsiUyk(s96UV_mE+ohmA-GVefq3X2N4V{NmhMTMAilctDF`Rf+w3cOI4)?#IhmlJwQeo1{pS9`uwv~`wV;P+I?u}0mC8#^*4VUxB9vM6U&YB zDaM6ke=(&ghI8uZf%@4u!+LCnwu{E5{zfyHS4EF7p9c2tPknrOY4+KZwZ~JXmkyuL zc4!#yjR64e)U$Kco3n?uY~NgD->X-Q>H7tX%uN)zFJ4wuS2*|br8fm-Twbje(*(y}?okbQSIhdLSfsl*oe(vedi>P63l}e5ym;}##S3SSt?%KSv+c-53IG>~ z_&J%EZ#}7<(@$+WP+``vp*vJuKXSFqYw@0SH+H4XIia38gMjB5c;6{-(6U;lFF@mnEd zeqKFmclBkHp(!*NWQvSC|W&Z&;gKYJhr04Th4@RlO< z>wRm+_G%Fq*?YLk*fM6!$K$`8jL*M6^hxcA5#%aj&T7=TbD ztbBg^O|?YW%nJvf61?{s#qo-X6y4gp=W^vcCc@X3_S_`KuH8Cyc(2q@Z)>LLc^St0 zLxvQAIiLUHZgx#f;>zp$9u?~l7i#**DzNF4rH9syZE$Ggo=Omg1bL|LWGYM@+}u4p zJv}|Wyo3CG9nA3h6agWIWy+LFK99%!#b7*$#V|{cKnHzp_8Zb8AT&5IFfcGEBqYS! z*#gJ@;MNDRz%lpsc6xQ}$fF9qfX^cYrg+7bgBKNs6FST@FqEmh_f+K*9ATCV%P7Y!3;#Rx9xw)9(@V+6k?tn)OP06ct44?mAtSy|!)8st6 zeJ4xHGZFAbrre5C`%cRoqr;qqwM8HRjBjGXmORNVVsRc%WQv#GKmMRZ!*UP{macxL z(whgaz9vKhg3BXt&4V*XOYlE+G>+k1MsfGt*$R)oKX3YSQ1`Y`-ewgQ#pOCK2M|Im zNivo`4aY3nxnXv*ytBLS6zI5Iyk^$`1sP|rOT*`H_-@>w?kVBkm?FPW$ssTieS#HMy?@qfv>(b9OTEZ= zr16{;YVm>1h!_eU?4CYC+OS0;o_l)!;w`>|zUv-Y9b(C|L;sNFV`lzT)qVA9#}D;f zgZS1hLVg!ZF?~9GHl&&6)tFX3r8kaWd!1{>v*2JKn6CZ^YI z7MXS6r&D?9KXtMZ*V(xWVtaK++OTrdjT!9|Fs{G@yZz17&m`R&hgv>5x$*1G`9n8v z@WMdv73FQZY5M4|R6T>sZeP23;jRR;VJHUpJ~LK)akkxzv{v`V3~1xX6y3Z=_WXWq z2%>cw9rbo%K(t<~Wl23i;E*{3cC_u5&|>h|0SS(xlAJp^Mc!k-9?+n^^My-51Nq){ zi`H5u+BWFaDY56&k%tELPbwKRptUCw{{j(bb8Ffk>@JN74Wg)Ms+LZ(0v*)pf%7-XVD5#ly#+afFoi14-@>Lus|T+G8O@ z*VmSbXuU?GV;BIg`^5R<_IDnYq`W@7Te>w@^5Et}Hg5R5PJRI3;ph4M>#rAVV7wiI zTl7t?zmx%gRC@f?q4rA?>qXlD6^cU$&?0u; z=FmQ4SETIzESb+0dHYx|IJk6WXn~!5e4lg?rPu0e$1mX+AK*LZ>oJS1CIzwChchm{ zD$1G|8bQ(FVcch*2@`hF0-ZWUcb(|9ifMWDI4Zlv0GJ2gxN4?)`ETz-v zs_j^`UZUrL$&@ zSv0I!XnFSj?fYLMWNCO)z1aW&z%*$<7h&7!i<PCwjnM$2~hKkM7ZSNr(X>YoqU4fyG^L{k8W={VGO^^^fUEXVgv zRXsYe;Wp_->EF*=?9ybyg2A^(cT0IWenQ(&+rs-95ZbX{v*;xIvJF#5+YD=MQgUnO zj*~huO;N~Tanx${Y@MN{(`d-|R}ibH?&*;uXLlV%FKlUFdi?YqDNbQr0Dy<;t3HlKG5ItEa1Tjbi`|rPu06ib59tEk7H1aL8vZE3(G4k91)audm84mJ=mJo({*eMv`(j^S%d+G7)qXej48EhE0p+5l)(y zr>gJi081L`QVGxJQB?!XQKQ>slZT{GK()RK4=li!F ze99sZgVj-N@9T{o{Je6xXECASz`Fh3Wh9euc&L7*il`|AR%PNZ>`(+~zt<}_Bwh*P$=nUs8h|z1b zZx5VVTBp&HOzj~OrB!PvngTHGK4J2%uEXO}3T6y%-N1w@dU)}rN8cs=6S;`dX?0Yc z`HIwQwR#!>LM-3eqk;6tZOc~Kg}b^nNlkRJ;M5-1BgD0^(O>y#!L*9z34UhJ&TjZ_ zuV&=W{rxOBfLL0u(^CwhX|N0KkRCN^)zDG++;*`oh45h1{pn#=GovzN_714U_+h$g?eU8v8Bn;oUedcuM4ngAV zj0?wal&7!eU_O)h6ZSL(yWN?$NarH^dg_UEDnDqUke`Ni3BGuE!uR}*H>1p|3#ps*SD&h!CM^4YRGJe2BguPQ1fI>B$CzPEY4(#!^RRYle$`GlRWBaA zz*2g&@irkGBU?5P#N*tDgWg<)5u$+?@!32xpt${9EFZrdh-$-Qv!@R=(&@0B3m9zEIwIKFc! zqTtqvOS#@7CwFrfzq8ACu;t`s+!V)j3Wmo&os*1RvdHGi=2cry<*FM@`hL^Y)?NU> zwGT`25#2kn_tb@(Y-sPv10u@H$i#N-eatbwb9CqC0cH139XWpH>Vr4diLKiwcnh^< z_a8|^S~d@{Gc2@B;p00}`{YiI{RqM#y<>CpvU>*)p15@NPQJ!8y-$xgcdPmvN=z6S z8CHH}?~zN{4U*d@_}KcVw~4iop4ork?B%O>a7@;R3A$-(5av-?k+d#H3DGikVkvQ!+= zvRRNVt9)}ei%sd0?jhn-Yu(+45ZIz^tTQHbYty+2_VV)n!zb?+VC~1xZt7f?&kO6^ zBvSnDPKJ`^WfVbLvu18)7ywX`oQxNo)Shj<#2f(7S3Juo6gKPJ+EoZ5_oR-E8oa!6 zb>Lh1HEtbm`{dN#qi3&_+osN#)Q7EjX&c?Kk*}q1e1e_&$^HY!uRJ5$jGNL- zsFv77_H68j0oaEmSXX51J9<@N8rZ6B)7A|=UtKqb-O#f#&sOb|T;H{; zLS_xrhkg zhzRE4iG!zah@#s!4YGWl`Gj&!Xc_Cq0RT)KnHtS`bA8{@Q+JCj2F&;@SX#;kHSQGS z3>>q#_MLpF?30I&p1pXdh~wR%T{BN>9%dDo;HWyk|H$ofPHNjWUToggS468$jU5QU z1uiYRx3es|y?6i7OV=Jz!Ck*uy}Y#{6B?;_`ZU`U-Zcy~>Y5^~szXBib%k*(AGc|g6sf5)l&7o(#k8^8YXR7R;!=Xu*!4|C-M0O(4y z9+x?^=^AgC&P2|U34U;S|G~3Y?wKSEo!H*@O##uYOS1-IrsR31#3HP1&1_&0sDqhm zVxoJrIMgg^kMn%E>=cIp!mXTtBisyF=EMt2l`{F{^)=k~- zoY;52*s*P&zD+&3H;(N+eeo_4JLvOXe#PZjYR5JnBE`LvXG?A3yEgRwZ9@g4%6{GR*p$C%q0Tn*ru^&_fH%;bMXd54w=_K zsw$6-ZQrb(5&g zWe!Q5q8o77irZ%nA3AyAYDQ_~&}FMWYh{W7CU8z|l1P-^IC|vFjobHLYn@tmXcg&h z`L358!+=dlltccFqi60EJH<9n3J~Vrd&~=I(ag`1q*)HmR^&d-V;Zz<8gIqt`@}bi zw$WTYa`^nUTlbzTd{bMrP6-rq-r6ED4wq3BJboz+N^KGBDAbg{d|V7kjT*a~<1~wN zk>bhyXTs2CO@pj3%YYWmqv=;*3!Fz8jTMJqIR;HLP<~<3>K%ol{57 zUb%7iS%p(bdZ%V#mVBGz(aW31ib1N@%95?q2apT`UaPY!2Rcfcs zK9;ywbU3`eeBksg);_6Iw{A_n^_LFsKXW02NE$k=yYI6yPOEn9T*bh%4Q<;tO8(&N zzN4qF-+OHy*s@*oATv$z%}iBNyH-BcA*<=Uj2pCPtJW#*9IEp1og(ix9mDOo0KlkA z9zHJjZ{9wpffxW7#hVA2iil2~LTve5o1pg1qg0t^4jems^~T*Qv+(Yn+WOcEKwtUb zNx5H2({PvSP|+n%Zk3oswQb_hBN;32Fc*R;)<_Fw(tdt;$phoN6#4dn6z*g#0Y%j5?t6fr;nY!_DtJsBOk0 zKame!`gP~neLmk_S={3P^VV7J#_+UR?YCvmYiRoaD@;|!+VtT&hHtt!DCmDJ@t+Y! z{L{$UGqU46`P%C{k}ZtF_~$@TluoB(SP%$>JT4AkFofyI51EoM4o@T!a4?qCYIGzB zOvE^)R_j>|1B8GOh=g3E(P(J|7+_q!Kw!x9gIJ2x>-6s$;1L22Pb}h7TD6{LaY7*G zb97n_#R3Kh0Y_va#wm?j&lrwv1wtXg!4S)0P`RXe)HbUx&hHsuBG=G>ad~_JpNC@r zh@~k~rzaT}2!Tk*<6;;fmLYYzTK`uLPbd-)7)xt4T9QGvr_~66O9+HQ9tXpKAt^-g z2pkZ@A`Hj>!C=zgSmeSQd#NhIXifd~I&N$L{{i(rWLY75<7`;xb zr&xonkuMf;8NJ3})y8n1Kqw$MfEc}2$8rTcAk~J5B^XZd1p*!yLo9}&$JuhCKUL+B6!p@7H5fF*T02IKL$ ztVXS4ajsA(BsdrVVko^%N50Q6IfOtcP4jW{)Ba!MAJ8Y)ZplS) z&7(~}nUPF?=mNSCMi}uCQMT)c6V{!#B^sINKW4c=h$Km!PRFt=hG9C5>Ng{NA%;{d zYc2?4pi?WM=5hdr0j<|4^@eZ2aQLdyYky_62bNN+D8s^!XuXQA{Vj$8t5qqhSt3sG z2rfhEC_QlbV)@-o+cGo}E8}fRQfYV*sZ)`ajhd|U5UEv@ zwH-^VYaGZiEy&F#P1BOSs@b#36W%X)RwaghxTixvj!LyV`^}zrK zh|+1Ow;fAU`sx)~t5*K{&5_!!G;FvG#eY&>qyKd>%C+nK<+eet-hVLvIFda-zxVW` zb6b|!_FdB>K={vAT6cTP_q(nu!&^jJBFXiG8#W!z8@lFnoXP*cI1x*~>yv7P5k`FM z-Dl~m^S4f4IkJ0qdE@n;B^%Em{-G^4%WAb+imK*3Cwbma7!#U~LUE1%Tw&h73R>-Z^%lVjo-6&3Cv#*8D! z2qXTkNm;1LIJxcg3!fGHzDu((jf7>_F%TlX1Qqd1}%75Nx)CePtFv18U zeu;J%nCKt9Z}Ib3nqh%xh1uBn_R;$(P_3&vsiT+DbD^iAGYKf`g?7o(j8cy^7 zziLPjgkG;l2pK!;lh_{o7aLXjb@%9jE3Z@kofhfuZl0d%?Gx5{%JqVe)+}xDp`Jl$ zYo8kZS3W-Oe$z6h_jgtQq8iJtc290R|6K83AmA0tJ32=8T2pE4ej`447_KYT(j%il z{#I66D`bQa!!RUCYPDLGN~KgP)oQg)r=uu}eK-HpKL8Gg!{Zsl!WhZu-?`&uD3bbD zd1jDRIoEGKEdPZQ1O4;u!^dw8n!fG8_Y*@c{(_IXy8P~yD{pG^FCv!uWIT=j-yzSt zdo$y?sy4;5ru5F4J4Jftuc+Yviz?+my>Tu3PY^;xkpUDlgZqejfTRrT+c)87Z?{`R@kmmQGQ3KYij z>2D>bM&qcFjQ#=;t<&iNK(DB(tWwmb5uh~+sYD{H8-bXmby^(@0E|v9l}P2PUzu7E zrIAY{Qkg>ko=P)1l}sX$NEO<;b}U*WlSrg;)!PGXLuLg4Ak|W-OsTDYn9(XE5}8W( z-bpvBS4brise-JNZkE((DF6UkDU(R$s@mimq()X+CebL=DuqNUQ@xv%gVn0!1;wQp z1qzu|uGZF;r&lYal6T4h0G870C>9VUud0;FwRLZY(mJ)Qs&8nKm#k zj|-GWCXvWh?`2U(ltLzvNaea;pk#=lbUG3MD2+lYlWS`FK(CTYq;hS2sW65r*A*9H zNUe?n09LP(N+dF6-E7q?sWarnU{rFcL@L+5*T$Q4?>UR_mQA|bUZsZ=Uc)TW}RG)k#NB2#MKzjmur$*a4g z=8YK6B>?nFnM5kr)X@W$)@w-y0CY;3RIaQ^u|cckQmI_|z8dS5GKoa0)KKpWjnOJ3 z5~)&CW8h&(rM$ALTuSOy5{XpxUV?R&(rfjML2eR>RH4@Ya<|r%97}4oWZmTFh*HU< z5{X>L0sy7a8JdS^Qdg5)9+5gNg=)23Nf{0BMi`l>5x>Ps4aAgT7(*T`jYgwVsZ=VJ z!II4|AOEaeTv;<8MgnR=@D)S-pBM zy4ESaz4pg*N~C#B;-HAUm@lveD?{x6jTP&?YG%As{fg_nadY zhAz@Ro7J}U*l!N*+ccwFqonxofY2s`c0H-BhVH@sMeWjJW0MmjV^T-2IwmvJyx?GZ zOq)g9x6B{gwOx<-H%s2-Ta4!6{&}4m#m6Khg!l)wpS-I`g8<4m4NIT2`QDSuYudyn zb?v+A;GxCM`+uJ=xjdzLVob{+yED}Q>bo1~v`tNlPfQH)4eIjMp0@?-i}o%Vo)$!X zWmFtZv~2?+kl-P~B?J%dE`bCI7D8}$XYj!xIKd&f26uNG2=4Cg!F`Yc=FNBSUF+4) zo~~Zi-8EgO&faHla^kcYT#9*<%f1kxv&(_(N3^1kP@YPB4}<>rdBrj)ft13xE)MtfFoPQ`O@QTh8HIzW@H;85iKv9p_w`3T-9NO>^G{~f zOD;o`U9We-#$mh)@}aNM<%wV?3AdFVLlrDCRx4NX$|spro@O>XO(^%XA2mIUCu7WF z4LQOtA|Dhqsw_-W^+Ooh7=Ukf#H(#LfyOkzKc*zC*Jt;;KjV)IT`Yd%o*u={fMpw2 z27rh=_>18hlE}SisdSDlczA*lkr}$_5IaKbeBOflYyM?!h$#CDDIPsz&bY}3DdPN{ z#W~{?7InQg2M^_6_!ox@gx*qiP?F|{ofIx1f`+zs$A$g0Q!ao%sh??hvDHo`7QC;} zKTfvPG;w;k)gkH2isnZD7UgNLGgHO+Uy-f(7{y}q!o^VBnm*YS67bLNiT{Ib&&!Z8 z{Zk>A)0sa!-rG7H1V7lYG5>KL)ZaF(nvan!3y<%utYmV58SwyIey9;_Z`I^*zd!mU zGev{DTt8ZLn{NrK?Y%kjY{}7^h4pZp*+r-VwRgOwkUpOiBNOmGJzug|?D+Jw2bn=s zH+)Od({8-Z6Z9JFa6;qrsSp$JUa8^I^83xvI;UuTw!LW5yT&CA_9a&4b6~Vv_uUJg z?e0FYN%ivF+EG%0@ox=LPkXv(UuKU+@X(KC3f0^*Wo?yq*}$$>%%3N zzb*79dzAcO6O|h0>CjiC`7f6|Y+)@r{l#Kr{NTOwhs<;*F^>U%* z?3;9BbEPw#m5a?=$>1k@Rh-}-R{2TbN->pFF{h(J6|}kAh8E$fv(U5z00;_!9CI5M z>O(+^di=HCdi~NEo>8lrSb(26%kZM(o#Avopii-#Lg=n{CbK`8e#Iq{tlR>BZCNYe2k{3WJSR-|27r**u{VrN%^XvY7M4 zv&3sB%NnEYC{g7o!|o0d@Iz0Mh|&E`fi8ve`M;hAT^yqjFVDS^YRZDoCvdOMeVEgl zcAaG$%~4bIhhH=5LWHIIK9i8A`Q~Q~Fol?>hm$(2EX%ho%z?eT7V}HoOF6c_O|Ez5 zjcj6nFtt1ON4iF*>&^EgbR51~r=LgO!E3`xW)C6Vb!I(c{W!RsIxY`QJxTrBPo(jw z&;93NL9P-+axytfO0snv_902u1h)0hkLP>=xj<+CO}L8yIQUrP8#i|e5>BDM?8(dM zOhJAmptF5Fky8JKvxe_SkegU^3mrHuQ~z1GO9CC(;5ArlLa|(CB^2kY+^W@Q6 zPKl(JK=d-zKnECHeOo{DTdxG1y-Y>+YdtE6R1#^WXsXRq5*+*DoFca^tD@Blbmp;{ z*@U0z6N5pn`!YbXFnDC{I!HYWAu#cIZ;TCEEU3H8JBtByJ{^%`P!u2pX#_mAm_xp^ zL%Ve8l*lcR06i7Rzj_Cwz8lVl{MP)m%JBNIm~Fg&r&?!Fv@yQM`dcsuEjkm3l2RSP z$P6Y8HLx3RjeNTG_7u53<P>=@!px6_Pe))KKs)eu15+ zs2(-|-MCpL?`4<%XiWEAik#9 za|rjiEm(6}hE(QQk;VRwAC>pJ;;+Z|z5n`*fb-+_$3lPUQ9jTpapD^ZT%xP5yX=k! zzkd<{fSprgSu^FreIdr~EAssCq+@iVR^QoV2H^aZ*IQw7R?Qmc_f3`|4$*KiCP+-6 z_f+yKs(IernoUzH833AmcuocqgXSw_rN1l*-nT9%r>HH#?)eu{0KcHDc)tOs8;na1 zK6(GH+GxtEMpWSZu-)+zN^Tv!I(Kt3sr%&GrKjP2v%t{ZPR=)v-c1);;7|_R=*3u0 zM)8B?By^8MMW2e^O0)q)DMKR#XH}DKmJA#8M_FuuDM?}l-&c^tuE>^3Sa8CXwSCi z2E?aT)}o%J#D|23jH5{j^|k#@@#S#RV$;V4nxgbdhcPLep`C4G(>7~d@?Pgo7mN8v zl&J7?3k5&^tan~sjiHxgF9jMx!%E_cte*smyDmd(wS-s&y6{CjNg*HpS% z7lxD);(P|CMp~P<22^b&*4+j2M=IS^JixZy$>gD*06*oa62~L|?|__$Y!G*pL%Gk? zh?^zjU;P&@You(80!~V7s1&z3hysfLWj@;`P$|ks%eK zV65Y{Y%W-UZ8XH`d3Zmh?^!VmEv!;{j{YIV>wWv=}vp=R{U}OfbY**n$HHS z8~=2hx^ShL!iiQ+-=RYJ6y{c)`25myHx7Y1{It(h|Z18&Bm7}gw>Zwd_}SNg|U-widM)C z;U2d43$z~kx;y#hBal8x4G%Qq>3&hB|=>)xaktsGsI7quGa!F^IFENc9q zN{-RV$sOR(e1mxvI#+VnfraM9!-=hapb22^U%JtCC;}e|{5sDYr%5FimgPH{yMg!O zfcUfl@uX$fh*=rcN8Dfsbf;)>Qb6D@usy?pY)q6xTwq2HU%j7V?_2;sY2!-pqxB;J zR2xpO#wyAA?X9AzagqH|gvXZ0LuXaEv4Ff_PAr)$LEj>*@*GVdLK3$r7c!03BG=h# zWfh_8?1Q7Q}%}fy?gBhJvhhW&$rJ+ zUlGYv7cRmH!Ov+ap+AeWuc47>3KY1ulChY80Y4RQ<1H0{i;O#8@=fHezTRv-j8m{# zwKQ)I-y=CoM+6#^S)3pkwY|VLQ1Wjkoi3n(pN7|Rrpa=+VoW^|EQCh%QhfUD7|h-g zReq#*4T>wt$*d}}xT&)->lLKY^M+ZHi|6BbO>mlN=#&++NNZ%){7A-xGA6L`V!dZc z1wC65KxQq8t=&YW`2tam(VqfdytvJfni5Rp;lZ2reNDwOI_Dn2{ToC4kXXKxe$74k zUi{aa7vlDZW2{8jG`;M7#vatqo9|Gmx+7_+@t?+tq_JYZid8tw#FVTJNJpB~tsN^% zV!N@rV*esrqRN*wY7cQHV@2@2E~bS;vA7@UZ&!-FT)st!T_nl^E31tMuNN)(uJ_0*UcBj2RF9iUtjq7to9S55CGttIOX6- zXa-A6HQuJ;`GZSM_{~j`jx)-tjDnmbtWxj5&V(h>l4}XCk-oXRF0K)JHxjT|e+Gb^ zN>Ut5Y;=HMr=aGstK5c{ZlyCTnbdsjU%JqpHVV6r=-KW`h^9wqNv4bAm>$DSeZ)0@ zKP6k{F6qHz`v#6g@&ZDG{4|)Q{xyEj^px~367bEWY=)z=*zVz{*ye6@Zyyamim(aa=kq_GoayUUw4jA0X z&WDKB0JuE%TKG?McDiPiEh$eAH^a;HuI1W|m45CJ+cPiT#@gJ>tYzOj54FA%vc*c- zprdKi=T1LRNJA>*;D_s48rnV|^pIVXgvUl6Bid?ur;^RQQ`DeU$9X|Epb3lJD2GQ750bANq;7OkIPouM0O zdJ8@7h}BJZ+Fc&3#ojIA=kEjf>z|Fh;%gkrJVQwrVUmw&uZsI7e~spL&nAJAcT<5)VnDLrc${rU8 zgc2vnXOXRW_r=24UT5m*3b~B@VU8Pr5?2vr0MdbPkUPu0B2cs#pB@dz)>*J(A(sh^ zdrR!4B3($Qx9xE(~%gIWk`+pqjy$Rxptiu-$9|Z zjDZ3G*x&sXv~1;w5@+zZ&7C-pJ({q2`nekU1Fhc$G~MRjI;nrc@ZM&`9^v%v4_*q! z_xj@p;^(SYBIfsW>)adngr+y;Znck6C?c>{pHa3^A(r*_tE-sYtB?lU$+Dw6%71jz zP@B{p<6lVc(>E|)wCS&~@LIHbnYE$~Ke&)w-B7<$@7#YC=z>3OEZp&JV4EG#Im(+w z<8`TO6Dd}QC9-HivpR~?36jy*Dv6aCaNFLYjN!jVU1|Kj85W3)jbV^lV>L?XAFl6e zyJ~V*g0`s(E7VNcBm;m{30~2ItQ!nSTOH_X@1*cJ4hME|mqae?0Ep*l?XI|Q46rXw z`mm&eNgjLCl8HkaN3U@K08h~QCYIKglTZF&p~W<<;z>P=wT7Fo@*up$r~<<-+|s)L z3nX=>(R4)|)|8aZVu)Ox$o;6+Uxn2%`=|bAL(=k)> zbjd@VP8oeN*&w@qZ%T3E+2}%p8FKaER+ALRH zuRvk4(u3PZ?;q-j6ht?E=nLTp)XtHXk>y&mY*?4T(^8$yWOe@?ihu`~)6Y*;F22Zq zSL@iMhK-uT_)LvAhZDx#vtGx#0x?zz(pH63EM*Of$1^#$x3ubINoR{$vC8n8GbhMrjb{3Z3*chg@=-3P6$o4Ajlcnz5LsW$xjl>$)1@H%`?3D**t_IV zt1E{0sX4fPv?aQDh&nTc>g0);z)p?tL$x0U z;HU8`hXY~M{SD}edmH5B4ibywR-Ey|0GNx_j)-26`RjL6<0&Yk(}$+cS7CnP5I?tK z>bCk}^>RBU^j^ce_!|vYTg%Q%znK!}x8V+MbX$!yv8D{Ez3Be1VSM(f(peh&zz^u@PK|eWR0$=iYkr*`-x8HBUoP;b&;RY$`1} z3L1)3<>Wuc2y>$k8xtOH@Lv2hp4#S;C?urpfH3}6vt&pd=EIwn863L!UkE>c+t^e(hAaeGM6-z*qkqTZ2?G0wgZG)Uly%tVj zRm=3x*BAA`uNm6nrf`bkTFsc#pGg`y9I|cFg^Y zyECtytHTt`-hwv$@^>3#{%}QYgm2~nn@s*%JOBv)d3DxaddnZO@zf`P(npwfzMfj@ zb~aa)DaTL6`7|pd%zvBl!;1e_x7hxOV0<%N-nye4hKlghLarF-IewBsM%a3x5{>s9 zcM~r+xePtJyeqi^sCcPF%dl@eu?RyIUQj+ZJj>btS@?0MDBT7F`Zi~s4cphs3SZuN zdUxWd%*^L`7@k9(Mg@v;y>DC&Y`HknH!ivmh(0enX72Psg#0xGOY-3xcE}Z8V_|?X`^yEwNQj$EnAJ){TOc5=u{I;82O4N!b7ovhM;MK*}{F3D2>_93V z7u^jq5lIxywm}TL+eSi2w}_sxDOA5;YHj!JJq+UhOZ3ty^vLz@Icjiv&jl4mJqAxONz1NJ232{b^)#-QbhEgAB<3OImoIW#4(^Zh3gBMCxXb zqY-;37L4xa8k&=liu{k$fG}qJ&#`Kn+twrpo(7AFw(H$-Y^I!lHygU1c%KC$RzUDB zpwW|hWQ1X#`v(7~$KTLs^j|pBwhgm-ar0H<2LZ1tqOXt3Zvl1Hj#Lz?n{4b)z zeRJY5j4a|R?1X}ynsvtXOBju=X|VhpFc&}L$;JNAmEBC&|79C!5odIG?uog7j-Hzq z{vSgLf{eJGSN|dr_EZIysRgh6@0v`0BOWB%rC5N{F|x{Y*IQpExueckAsEk;Yl=)) z7_UobAWis2Ca8p9){}&P^hG~2)UQX%i60=BQ~hH10vcK^g%wS7sDQ#nENdd75#*$* zyV1I6Rqg)T@eX0Q_Hr#nmy?s@ia(^c_1=0pwjQTj!dp`R-)OQzsCwEjI!Dn1IK7-f zSY-WTE%mdZ`hV%oWF+g@IyCFt176Uo~p%VSiwo2 z>nrWx+l$9o{e_H^%!-N%pO%)ErTPVuJsR3l*-hU?6Ec9!w)$QyC()R%;@<{#Dj)t* zoxVHQLEj!ZsBbRpt@o820E}1RNkgGMoz9k8@q6K*LLD*BgWdNKLG0Zj-6+T3+v~4` z4TqE_lD0{cuq#?e4rd^Z(^kX%89 z5dM|I$z5@s_fHoR64KAfj6%`TF+rYo?HlEN2jE*r=IWGK)#znYrE|2qr%!)k+C4Cq zt6s)xnget?onPL%u+ZjO;@n2t=!&qvf6q`eyApuYvD1;Gsuay~sw2ky9&s>F>a00r zOBHhR@Wn%w3Ii~LR2fhrin3DNQ_sY{rp?>~z{=7MHNpqxV?tlHlGrj8Gc$A9)oGd% zRNh*-@x)Ks5ip3OBqyJ5R?PSF^XsfKLm^yI_?GCx?bXs!t{;B*=_g|BpKR$sa&sua zd-vl^&GsCBWMpK?dcrp8x3@Rb(PXhS_fGLr3EQoA+F4*jRm*7oc4RjZ_XQMl z;zD*ITBg0#qY5ope}5~g)?4A#wdUa}ZN|FJvC%<9(R0(=pjUv&yGsO&<<(ODWjGe+ z(vlR6gYnU<|Jp8^!kgdSqXN7VwO1b^re^5MYWK$f;qLNsyT|90*)%74%hqwof}2DC zOXmCDVfEW^ptDs0W+4O=Fi`WzDLbNYQaRi>#$Qe>dm9U4X;7lQ0jp~pLnE6-Duly? zTpaB%{&0A2cUElQig91s3TH4UKk~u$oW*=EkJqNrv^?NTB-q>G`TT&mp_Up2V8AJ+ zUaGIz68nJ|0BHJXQ2_fgGIACN1!%Z><@Z1K^XU{g6g2*2zX*)iY);l}MyhR44vu&K z=3-QwB)dPYgb}xnjaZ%ju>m(}K$_=jT0u8phCr_{D`mH?W` z`r2Td8pAAdTJ5c7Tr~X3u5bX3uC`10okdAP9GsDT# zZW^T-7^^|1g}v2%oi^(=ku{$Z;~yUfN0r9#{YqxBHrvlwtOau3SC6yO?Dm8i?)Tgw zA6C%*4TqQt3@Jyg``y18a1{8xEhNeuf3Uw-N{{Z}z4^O!6K?a^elMzc`3i`cBWi(~ z9`8r1tE+9I6oFRU6EMmS7ohJNzgzeioRxAEzjEO4q~~pXk0gVXgivj*w^@lmyVsZ* z@D^YU3rk@Od8neaBNKAfzB%2S>vzQ02x8{PPl`!ScLtl=q7^oTM?+Q?bzcB;s)RaU z<(s&BmA>uqSbWWAegTY(ug1Rc^DhrLe3_KvxPB%3QtH{_VC2T3?x^#L?fV=2+N{%v zhCdZ$D;M&1@=aBi7tbM5QS-m!+2Y>dtE^}K`_U!w{Cu~PPtOgUKl#0wPP0b4JzZ_J zIxed3Ujqh~6i798g?iqh`2C$p)Bf)lyTk0_exS1w9BuY9vFX(X67Zd}EsxaYSj6Us z*irKIHX+40uK`wJW|)$^yu5c~NiJW;t?Sj$?3qh;3&iB^FNWYBx%K)aQF?khx_(`K zo{@k;2(S4}!@N~=fz#S94Q+u5=y)&B!v}iaoOC~4Y2#grVoy=EspchNG*>umOv>Mv zwdTmAjE&s*Njs~bp3baSkM!Fj31iksO8tcj()O~?V(CmzPhMUv*Wm}rhpyRhHkwqw zomALecl`Vu&k~e^zok05>Hq$?JilAN3}29DGaMF5ze%Dre!w_y9DmURY^>E=eU1V0 z)az59&-buOa~Vn)PItTcfg&z@3l?oe$_u2w?sCS5(g4I!KfXhW zBC{-&Jt|w?*%K3DH{SQg#c+EO2kCHR#RB@L$0~>%@$HT6=w4>GUtGIOoyL=<0JyVY z6MQfL6iVq@J&;FxWmjE#bOd7g~ z18fk9+Rtp@NqV-Pi>szne4Vp8oKxW<#RovVPAzjch~Y?iMB#Nx0l0j#g5;N^a^5nGFsbgU`#{w*Jzq`M>6maMu|4%JWI%?a*8 zce3A$6JbYAgyqmmG|=>^QqWX12q`;np0J^f)yMs(xcIb( z@~&tSn=J*8cUQG}bd;Bu^UnFC=k~l^7T>{N;>EgRr}2)~M)-95Q;6b9mal;pDoQ!( zdvsC*SgE*@Kj)K(ID} zu=kK~{^ap}rD1S1CaPl2_%wqilIlW31^lP@{3*YCpA)Ic&jQFMint1Y_l~Z3HOE_( ztkW4hSo4%Z>n@B%%NyHkUkr|61MDRTbreiSc&xa#->4Mu+}eXn(Sh@w3zg>ki|eGy zD?GnQV`tP?owoty0#WZIx=Yo{Nz@MJQ!F?>b7onoma1W4yt+#aOXOK;GS4o>1nNK( z9Un#OuAw&wrh-=nQ;TYmPA>uQW)0$X_lMuQWH6fpy}kQ{0}w8O?O_Hqs|L@^c=L#wLkLs0Hxnlz?Yo`*V$8$o15+SZ_TYq)iUw4 zi)&?0UgufCwbN&&IAoH*d;65PL!HQewy3_Zfbm}G*D>_bVi5OY(<3^T)ZoFPfxZfl z{ey5ytaPt`Cv!eGTx(IMZ$Bano?aRQK=W5=s#xFnHJ20f6`y*KF?l^L6*SSe-fi~r zHl7xW`$euOOEyjJbVvUNHrJ-pV8v?``mXeN_V5bh7C~dP{XMW0zAa~!Sc94YaXj`0 z7=>jux7F3Q+5F;urJfq5tpqa7cWZR)!UAf%8`lh7`75_aJkpj|e?bIIdpaNWu*xt8 z=ie+CZtpo~P1(6Lwks)?ryhop+{=n5xLsRL`n{4%8|LF)DEPO?F7ub;Ejt}xe-kyc zhLB+`XRWg;3XHFussY|RyWkYfWUCh*^390|@>vf!pUQ~oonAB_-2e%KU~9ca&WTcvPPL+gFqYa6YhFQdTZ{fiaHNT~T{8SG{siNy95%FrDOqxhe z#3vNQXPp%HTpL>-!eKr#V>^ENnw2+9yif`tW#@!Bs)2+79l?e3Fhz*A+T8s`l{+vmN-4VFjGUnD(H01edJR9>%@f1m7h z02J#9b|9N0c}Uapf5NoRtk|YPGPtM#k=a-yA|2pkRJ}Ja>4^1M>!HA!q^+g(Jeg%` zW)??dX>NYIEhZ~3U)R`3816eAWX}9>&ZsFJ-=Oz=%E?+f=5r+w-y1k4DjM~jb>iVV za6t!4qZQ3#P`LLQsuIa85oP_Z}#BV zxQtQG$jsC#;$}ye`8m=%znqFu_9-(nRk?__#xK?=r8PQfH)%e{DQ>$qybklj9VeO? zsDEqWhsnSrQChj=8&4&Wy0zkcD6_P{&;`>eYq^@Xk8?4Z7`p%a&mSho74WP$Q=A1? z;o85HlxIrSD}7)b!G_E%ECB%la0YAm_#i`ce{FU3{Y4fHE$!ms;uALU6E*uGcPP^y z;T7TNX4fdpuC?FIwdv~64Qu!^Rs*qe<&wcEzP-(#J{Sss4qa^Hz7Eo1y`?7@s3}t` zeS#C$Kbpr+!dKp@YG~Yrw0zE;e56u3Jo|7mmO1OW4&y5|{UY8MfFjqh%H^*+E<0V4 zz%}{bHFxj8T_&8>%-W{}Ed7*I#BnsfIb$3Aig70Qcr9Wma4O>Zni|d!)5kldHnro1L!p<)q-)ow8x!;Hq%2JN#*lOJ9PSLf#^ z!`0^o@MI9LNam3(=Cy<$1~W4YEi$H09p}|aw%Dx*y8m6XHhlLEEmpa^TLdFf=kD8@ zA^c6Uqhdu$^~K+iGm1tgw`z0f4ojBrh@S<11xs4Tj$#c|nnJO;)$yko-W47W-8ZGz2~gB@tTt{o}<^Jx1cdN9T3B-z+VNQ3Qw-?Lnlh#EWD zV>sGtYE+}J&=3>}bo)3E6-!W%OlL0~L;u~_J1;>I^UDb8X5mXxC6ckt3Din8g;d9X zUy#%Xp0ovf>??qFL3YsLPicO|M_L{u9oD?nH4ekMJteES@%#9Ef2Y9gmIl+yx{OAb zRZMb7IHIcJYmXSD#aEPg)SJ86JhBL+Y<`!sj2UyLXt(0Xv|qC!M)v7Q?-tT57AltfzoxJ98C0w&B^6 z`4uPy|GHyuN?F?nVbj(#1?dYNn^tYmaqHnTZ%sGv=@zcD*KmEyM$Xf|hS3m}jzZ(0 z>iM~H(Q{sSH0ev@&4i~?MF{iV?q62u;WJC29?7$OB&uKA{Qg|!GG*msxJj~)i(2g` z46!4QzZMe*xN3iyWmeowEgPZ6&^-rc8ZY6iwY|R0Ui4CuRo{zAZkF=_c+9U&8n&C~ zOoe3-VQf0+S0}o^S}*x>-EN;b?cnw!%OFGF=AXMG&qlrQ-gAhB8NEOZ>|v`f!+Z)s znz^8tL#1}h_Vn?&YLKhMB02-Bk^c+7eE!#~>%0_SUNNl#EP+1}I6xp*QC3OzBdu zgCMur&r8s!p^ZVDsEE3rjmkB1E;HM79tBmGhnc7jK-{fCAWI|gD$v)Xou9G#cbHE_ zhT&TnLB#zrr=21E>pH&E)@AioQ^tOD#gW;me5^d(CLX-9j&%PQvnj#4s=*nQDNd_O z_|JwIDJM5PDXz!uLvel}d^$}0IRGmlid1ck_>8t2=!EQN&M1bmJz9S*>ZrS6*Y<`6 z1w1u*wD)D3ek+Crj>@^+e;Az)#3LiC)1=Jw{c)Qv)*2Omd9u2^-wbL1^iJF%_ za?7tOG_u^R`r?El`;I!-WP9&sFi;42`PYWS#6J_D|WGu&?tF6A1g?bBiO%nu2%K`(Bc&>kAS*p1gaHYZtsuic5w97FJl-wDlHtRMk?QLT_Cdp(m{ zs6y#)uR_C32o3Iat?iR1i{~sDo^a3d7%oOCXk|AS(kPGvw<@Q@z2ryeXa1HHR4ZBQ z(3Qgz-={Es$qHI_o4xduJe)~NoE``DU4V#DxB5~1f&U8J@A0xSTeS_^g7Ke+>5Ah`c&oNXg_{{rFv=6F#gm@vn7R zk$;d7E;(1=HWosdbuU4zcDd&dxg5Qkc4)Q3`vCMHi8CAM=M8SYej)sZX^usHXGgiv zGz2@d$R+8Iu}1}H%5CzerwCzVIbiTu!Y9+AF~XN5LCQ@cqH2SRz=Gdm$g~9>`{bAN z?+;fla6W%ar>??v!qO69eDlVy?rTt3FFLFK2hJ-RlZix~-U#;Z@87&zANzYaYlSk3 zEOq{cHsQn9(y3GpM2cWXuBVrlsGg)f3<(opuMlTu)|;%>N|@?-PDGLfh(gS%2sRO0 z@lUyv)68m*OTyJncXpqaiYEVAbOK1YdEy_oQnLOyTSdlpu>%pfq2Yvap;M*x`G{Iw zR3qgJ7*XhYov_YNnI5JB;<`_z90!~7_w-~}yA?71)H;fHGXOx+H6l98ds{P_2qo0ugzPD3xukon7xPHRC zFMBp)Xk?Y?0_}hP7|M0g3PwC0Ap06IrdEbIj&7Q0^E-RTe}0SZv0Rm9 z6#uBr6cFMZ!HVf|7f@Tyc?%PCr+F{skGX2)eAT_X2K@RAdxS?UoQK>V)KAvXruYX zjGGASw0oGB`Kwp!HU3$(mi?N+BT(6Bw{$P8%j8fw3vwI(*s!jpV7yn3I((|8t%WG& z2mJC?#K{Xe3C>p+6@tXSw5eKL4w%ZR^XBb~tV?{!)qntNU{HV?bD_AN^ zpmuStZnM2K`y5OKY7M}IdHQ1e$>9|@3e|b}oSi{daD!>E>ch10GWP1gH)Vo)xw>%(%+u22Flj7e1svyo}^U$#oGo(OQ`rDs=o}`9%)BL+5Ay|Qq?h<0df79GId%ebXlNSxw(m*ce$Fv(^&uH z!KWbHVYZ$t-{CYf;ou!m&i|vY+og2=G<;BzDRq{Qwn%SWS>(tanMf~dx%q5G4i#li z6tQJH#6v!64R;{T`!1+r_7 z^Hr=q6!_t&A9dn(co5ErYLKU2eAY9r_AQ1XDTZf;+jjdperGD|$L z5WlNftiP;tDHSab2tQ+&^R#zfzOW|#sAfR9@MpdA;oZnEA@rm=fzVxnUOl(5Z0+$w z^iA2CtvtMiU|mI6_hEWf8>ekx#{LAk6`Ppigpq+`;YC)+TQ)`JH3yEYH>#Gi<_1S4 z`kxu#O`!QTPmX3xX&uBoHPkIl;h}|f?UjRcq_9AiK?v69(0pr>{tdZkKI`q|jwWek z$G4B1Qc!Oj)j!)i#iJdV45oi?W}Vc;l;oa;A-;WjNkymI2tRT|ar8i)`(>43zRclI zytB(V|CW0oKYY2eD3|MCUlTL0E9Onz8e3}4#tHlQ2pi8tZ@vc|r$&?fBM0BO8TKwp z9;KKnnu{#gxU3KVD9=|P_34J8Y5P`Fb0AJ`Kj&NrI4i4IpU|WPy*V%zD-}VjPNJ#` zYIduTe3ibj@F5@khXJB@A z-`-V1J>itrEisXaErn8F<35zU>6&3Zbnyi*F>h0D(hC%Yp%h*1amd&wVc5&n5ZBV( z=l&7%(_!yrsF=)mBQi^kBRv%Dz{FMMuPwFU`9T=5mDI7a@xb}orv@4#rwXzqhdsTV zuox*9vIU64d!%4#wEglWN6F03m?x~@>vKv`;~L+-lpwS^5p(b@bAj1?I8whh=`j@Ku$A^Eq>)|L?Sg-;2sdldn=9< zYWlW~d<@-iz|tJYITeAvFAl|Ru0Q0fvNV(GNHVw<(l5w zmC`FV2d_2Ig>*saZ;ClLhYlV+?4VjCq>_%2ILs+BQ&RAUBX0b_`SVl(ia7=0w?LJI z0&frQtG9-slHV6Wyv$258dX~7q9s#T|V}blMmP6hU@7_V4aA!?WSTefpM9Z?lfn)AbH?J4tVd z_;0RM;m=hP{;TT?b-qz;Z+2Q9r`6584dMKfkkDVxFm>#o|%|_7cRQBhq9#%tg57*)YCY}QYF)2E{;fc53=vaDV zn`g_&I(8LnhGZ7Q*!O(32Ha(tK9Yq z@MG~3t#ydhE3KHEv#Xf99?ciY6&m$YKDVy;9;cpTpOv{*z(}dI56T+!a5Xq7P#7+V z7P&^cI!~mfOj|6u>SQWr29keMDvc9!*_{4C?)M;H`XY4sQ?^|=+cblUl+uaZJ3a7= z^qy5xV&7jA^`yZ_L%|h}KR*9Sq2>$=U&`^uZ@|h1K z_|l$mC`;jFqUd&sbPxZ{WilzLT0~0SMuVJlh+4fQbI08K+3wk!mT|W~iSDhhDauG! z^Y_!YoMeLURLQMwHJt()T%CgNOeJ9_J~_3x1n|4*Vt1C*TS#iJ1Gw?t-g$CDVBgUa zBMtx#*M_=uLqM@|E4s8L!_s3NgAcrqu|pm$#><)Ddv2g;`_y)|#lrDylOCTPBz-)Y zd7-e(tY838Gu!Ae-;?=MS2;UW;s28HJI?U7sv~L?32;7l#`hKG&vy?Or5F2$1URGe zDUFcuI&YE&fcv54$Yt5=(5)6MxqZcCXs3a7xp$+H#_%U=Fk6c76Y=h8pm*|W<#4hH zM{jsap|((c%5}EP@hPKswV~U-Pp#24f*u%99_iQlg@Csrvx4&{;5^vRchYpR=tcC| z+d(#~<%CW|UCwLe1S9kKWGQ>>m(X6V0>kOh*u9+j7*YKl{=w#mtPXFTCp-x3Lo9DS z(8|$fR~bn)0Kkv$a7W;UGiRdDWg)$9Bykv6NVZL$}6oojPyG6u-i2?$Bs z2-uCp=l2fviH@>52ByYmCTzP-fM5TgbqYOB+Y9%d!*L}c(I(H^Aa%}>a!)5W`F7}) zQN-LSYdRB`U;}A0)ss*M-fqghZraUqdT%wjagtJBI9s<8FMPb6N31A(alSwmmceET zri7Ipl`em|?MShGrt=)%FdGdii$o#SSJ(VIcwe4czXGe!a2L(U{9by$AhhTO2AAT{ zn>W}kdtaO~Es_9!>EQWWMM_?yZSD2GjI_B|Z$FJ?X0Gys*_XNH|#R=xkl>kZLfK8>v^gk zQ114rz51;y{+jhD+f1iJMv)x006B^IKO(qgyG{ioQbn`b1t~EuuGRQm%ewHOZVwy z-=+;F?0PS9^K>M$_f2hItNYe00F02>I$QEN1cz_wU?t`AvESJ<#c-0#!}&ash*YM% zNlrxGZZ-tYmi`9kit()b(?4>>-CiAYtY3%WC*KzmJb_rCzqM*i=e`R+vexcS z#LX3>XYBfD#CaG7Kyc}_D+5|~U3Doz>g-LXU79px&I1)!fCCP=zA*OOqRHb{UNCso zZ`Z1(qlHNO;c(w>UAI5gyLdVjJ=xW}*`NzaMv;V{aAskje$(HYtlgZ*n9~;@eJ&6P zjCZ$=?=<0Hk;utWta)_h_|ptVDBx=2FAirLId4?Pm2w~%n&9-i2C z^oCTz(cOl4cX|86bnG|6UT~Z+W<6ayW%`{$3pYpM^X-#*4xI6{h!pclTJdIC|1O*E z=GeJN|M%(;|t4ul{(df143!;*p!Dz2d`_z4uZDTppJwI5N7!fQ4uEw(ibE z;);>o2Csh%A}&dCVvde$+H=<9Jid#U?A^iHqgI?V34aj@gmc7P{jCk-nsuFUKcDOF z=Aip-0er4=be3c6`O5vAssEyQn}yK2Kt0A?9El zODk(T8!G|9<7wZ_?KgPMrA#X?7dqzH&=$?MzgB(yx-8ey)?UKH2vTBe?Z>bKJZXpt7E6S6>S0X4^HdcwD+P*gn95(i<57 zj6-sHJRX-MFqTCa#^I34m@@<0^x5}H<>2O|c(}1g?-6I>DY1x8(m97`cWOU;Qz9#K zbCzb@THUSXpaV~GN!|}dw;{j?lFI`=Pbeg`InO?S6nnTzc;x>${zNX9qrV$;vEyoo z^;rA7%tx9%9X-V^(T8J|G(rfeclUF*uQ~i9g%Fdmtd&pA;U`VSPo~al;@@=Y1^w4W zK40C*xz6~6l0QwVoR2SGym;~AZS1GiEG2^wn{&OrpU;3pNySfOU20j;f86PGgs5{9 zs#;d*emkqUNPX(n&Owp}^KZ}yp}bq2f}DD7d4&){C~rfT%I?jl-g)uj#fujg)(`h} zuxhmA4)crly@!jHIF*%!iRI!R0 z|1#f*5JHI6#-FO|V%K`zYZjrrE7P1DT*jRIY&1Vj>-8oE8QzSD^lG;3UP*0nBdWT# z*!Bsb^oe1%{zFcfKQ+pgI+|s(uC=h^blMVYE?lir8fe56-0$IOQE$R&xsgQ(A=dbQ z|1dX6*p7E==-g;Hyx zzo}9Cd~IY{?ftQOgiz+y`9U0hyVbWe6ha8)-qAw->@SVvtv8#$6o!tM{tRhHxx0Rh$=k;$8LP+tXpA+fXcJ}*1o%y9+Z0KPxsedwqMRfAK z=B_U7mn0}Dv-T9d*jUBMe)#EhgwXpf&Ab{+O=e3*WahC}&XrfaHX?+ODR)+D=k{yf zeSh$39<&XqI`MpJ$={zRc-Y!j8TdF?hY&)BglSQd%3XIV5TcV$Humx#f1yOopASS= zs66jx-Z$lC-C5!*sWAOql3B;KacA09B#E}0^avp;VNTq=Xm=9}9_>5^Bra`$VoIF&)D;Cf4E+a_~v8O-{o__(NzE#}|vNe)5?#q1ky zg9R*qkXurJ;)VUUk`?Cv(|bC4yVPHiix5H?i>rD_o!ebaDHdPW-IYE(pULOb5JI^( z=2&s8d#<{#H6esh#^o8FnE&#J`3Rx>>+=GlTAfcu2r-Y>_q4F6v-MGSu@V}MCYmuQ z^KYCwlvZrc3KuuA4(zqP2qC128`a3A_qLC}khtpTn8uZRY>^{`v@v7CEyMb5%P_YM zQR!D^`CED~xnF<~`g~-RkJxR&m86nktv6CEQl#FzcJW0~u~pdBJJ7nxf-Dn42qo?v z;M#D+CslDZ`Bx|Sdh~llBZN%n$Jem)>3Ta;S8Niq?)3GssW$@RU zMxgoKVptWcD4zniTRP zc_|bMoick{|3IHcvp;AMYf!$}ImFQ=a?$boFP=QOer$F7Dl(gflb@%nb!uhlk5n3c z_Uqfn&s>Nrq7g!<=watzUd?IeRVVCTU1zf zJx-}qX?5D{w}%>A+q9m0O~q2VDbJ4XJeRC7A%u|P)zFHf8bgm3A#{6QZLWLUTSW*V zlOjJWIXzorq+<>Ya`$R*Jg&Hl-yIz6?bGO78e$TU)^W6LH1~3$R;$%&6zQ+$G!b&D zbiSORqqJXSuF-2#?yL;AwClJ2UV+Y_)o8W4oKxd#+qkyBnxRK1ZBy$Ifgozf!&J3K zr`P3f>+We&aqv@xN&lq`xl*l(`(Wm?P+3%2M*kntU^>#;?48N zEX6i7{xEDz)nK=_#Hr;hUZpAxc*ef3BUU zeX}Y19=&+-;QHzLeXC0>tF5>er#5Q8uU20MpCUg$|LX7I(xpp}9z9a4)$%`xe@fDO zL}6Ul`UBbpSP%dJ;_CIAieF~sYQUd*dj7VO-!Em?maPa7hA}yMbrPHPMMVe zVi-iTY_Ts`@$vH9AuD2O$o>%P(sI}R(|xIDXKxf@Ju_}>-O3;Y7%J3hbKXAsK({em zIF(&@!tQ9t;*EK}TchD^YPLRqIb&LV_pd`VaGoIR;+|oBF5!q3SUEJFbA0jOI=^&2 zGc4G+ckI)`+k6;+unFtfwa&S#chl((zATMdx`ov#zSIugCl$p*H5NHcaT# zc5H`PV~<@`jI4C;zy(pgIZN7ADBgx6_%zR(Ke z26Y}>Z~c+e*FW_St_mh2>sz~4Wry$flPJ7(FEFOXK8{Jq%1=uYQcP-=2EcUn*!zl| zw|DR{%aG$1(X?*0r7=2&1Ca6Z>I+lNV{!q!Sp^KR%23lva@X$%1Dp%2WW@0?Q%<-{wlhZ~*q{<;w-tdJJyk zZ$8Y0b1gfJ9y0s5007S;x=FPIkCW91 zG>^~R*O0;SXSZx+0Rc{B8Wm};pM7NNl?_6nUSGRns@`Qt%i_&pp>N~Pp`o|sdJKSx zzkmB}k##`)_AOfw0EQ8HgyQq1m&R$KH%~m2w3*g7!p7VXj=-bqh~De_UcR3`r9<46 z=YsHqW9#zF`X=>n)4a`+*J`r|pEVeHzG2lX+LgS3SMwfTj6C(({kY83tXLC+QZtJH z>;M>o7m6xc~qbHuc+f)#WBVi-}Fj%T1<5YE8NdK=ykc4;(=E@%pu7 z?_s<9IP;2&bZI=WQ_$WMm)=aS8FJy|EorYs;~IOG3~N3Y0Hkj98oJR%DQ_Nr{+yDR zP)Mqao1ve zi;4TrE4~CXqXJwnP`G*5mc{b z9s7soz(zz-xTQyEWJNKhRkK1W;lxD#yQi~5S zUgh4cP19?uH(E@X8;5S(jmGf%proObAOzDbdi#I3UT(*2&? z*OwmW3frU|-MCh70zQ|@)EYjh3hLIo#qQy^Uu2B* zbyHkDbM9ldd2`qzs9w{0mF&N!DWnJ$-rPHP&;>q&aG8DOMf<1psqM<@4FB8U{(sC3 zOo+g_ZZ@UCPXx&`s5M1e09tP%C|&HMYbiz(0{|G;zIMwRp#r*W<1kA{YkhL;`wZ}P z0|1bPbl!5aB?4+&*Px>Wjsd7OYJ!H3PcEjL7zO|^+`2}~s0dq%Rq6}^H!r)gFC%ld z5UBEVDRBSVBrpc8Ys2vuw-1yeR$y((|BbNj3SPEl#;#CgNg8xo8W4*tB?5a{aWpKg zQyaLJ&Q7wgYIk%t=(AKn7i3fJwLQK$yra{okyxMd^oEv?F)RWAu1Du?RqXU2Xf`9&SiT~WNRg3p64oAkRnzlmD)+m8hFgZ(Yj;@i68|8F8U$>#L}#R zVgLYAA8eey_*5><5n0&_H7Pj^L7EeEpt7}>b~^x2DlIFsceO0L?aSH4laOy?2##}m4CU;8NyoKLw%ddY+a&Qj^%P;klltP=OLO|*@ z9EyJT@JhT{co^Zn&=wgD6}+8@ui9qESLyOqpeV{03M#pkS=X?D@Gly~I@^}X8m^7szHH&qn@NPd zy)9Rqm7qdI@s1>pVZPoDUzvEBM6Xpb04!oP1iDIHd4N=M4ph#+7>qumF>^6(5vN)MUaba+0Ud_j$Bn?p&EdLNklwH zYe^|v6bnuHO1bfCGD_)`8mY5`Q`w`Pq>U4Yd6!)TU}3JDp%ME#^CM%>5>8d;>^sof zjnN^IgOdzt)j9)R)=R(<@U(GPmn~j*Db3^}6_J*ZRH)_hxHv@>7AYJ&BV+u%O7MJcp@u%kv=Yq68aM?3(j>%9a}5eykl#(W1B*o zbeK4OYP6?Ot3*Og>aELKgjjRJ|FG^I0>n&yOfJVY$Vtr7EJEf4N(kY0|z?c|`l@mc^7Zd=<5JDdImY8|%%CcgS$VlZVG&pWoc6n7)RD_EvdD)q0 zH9~+8u&CIdgaMB&y0~%T#(i%oYmrE3gECWfM8&VoEkX$4ToIjdbLP^uw_?;1Cz*sG zlk>FT{hh=~#q$m-G3z{m*v_Fuj}0mf;o|Bd`pO01z(<973IxC<2;APun!}n^1>*?0 z1eXNX+&lo5q9_WXuY6ZLv5g1{KfO;fG_Hm*S_99$|C~L&fk?YNyVKG{49Ad3r=v*q z$NT3~5Q6|Aj4KUl7FmgBR4Vm^)ZJZ*Q51_10Bgd8RyG1-L7|q$7(;G)LR>P9V~7Q7 zhpNVJPc<O8_+Q^9^^dv^w2Waw2w~=&vpgL&pzH8~!)Z{@%PretH( zJZ*6#Rw7ejd_o@7`Z677B`1q)TiXF46k`eR67*2Ntd-{rc#OfJ&#I#X1BG9EV*#WA z-6pJS*K@>^Q)_$9YR_|cwW^JW*ei0xszcpIzCFKf+L*p$W!@J$xY%)up2uZDRj*P% zM*e5H-qX|li=+Ab#}S^-9;8#ER^=9MzY}%BVBZC9?@(SEbL)Z5{f5x^sB(&=Ebv_77(?botHMiTx6RjaU|d z_)f|_v6USpzE5Bvyp*rV(=$~1OQwsOR^y5b3O%@r4%ARHH8(nbGJ0R2oTMb(MA?w zzsw<+zkll3lj<(VR`l@naChYAOo$4OF$b0sI3ZUTWy=AI^#|p~7vxz-yIb*PRu%<` z32AZ&EM-*c)D%h(T)_e$2eEWgwyM-R2NKDf$$HF5#R2poV% z@_4p^bM_stCisnki}QKf+{^^!7wurQMx$9fxB`;a%G%G_GqtwO*J4#ziv;iAC77UY zY1!(u6qB{g%O0>#NUi;lsRo}CgJII^3}sHwnr9F*J{(#weMkD7U8|c^@^EpqeljP# z*I@%&@>oJjnM|?z{GWtDZ1s3ntB2=wbm?0I{+%PpBAElJh>c4J_%agnGE-R_e^(iRl0^tH zrLJ*=ScDPyw@D#+7B*5va$-_GgqE&R4e7}SGPr`%7c0a5ZXO7P5XR+M+Su9HvswdX zP^#E(a%JEg5nXV2{jBYY{wtSHigI`Kuq~L^w%JLof#nbqu_!w|TWw-U35N!ZOPcg* z8nI?+ibW_ZIa|Yvw4jYNhr_{Gy+->*Q4j*QgujqHg4RhIk6t^!ji*U#KmZs{087z2 z9V@NSam31o?FYZPxM#%7!BZuUJBQX5xrGg0vbpuZ*c-=IkDojdcL|tZ-GYmYst;Se zrJbWfO95aw4v1k{WMahuy=Sj!+H26m%UefI9H({l+|$)hOJg{qsb8F9!T_kmoP3hB zMGS*+Sfopg%X74=U~R!(nKO3x>)^G!CWJUSx=OX{`qe*ZVi5usx31n}dX*+U-aNjx zbkx}KD)-CBT1x~BC%D_5Lqi0Vfkl8}7{U;TEBX9fU`kKPGZ214)`)PDK$JnPg>Re_ z2;roKC0~;nm!kqdE0QuYI8Q(nWhI-q?cD7F3;;kgU+e$46$(x>R}k`Ux#6I(j(g9G}o^(uZP_ju1i^K@bGNg-n=N+<>qE9jey~S zZM%6s-ZcHt`#ci>fHC{Yj-?mh*YDKA5&Mqc05SCMo@c`eC`#ME{pfqS2>_^!M?2SS zPp;jfl@kW^*9SN-$Iv$Q-S)3oa_Ms+0|1OR?cVyuXPLTv8VZ1@)4vIOYtEe0ACx8l zKw5cro{EwLHHmP~Si9j!meM?@P(?W@MFs+AHE9Vcc^ZaqUm?18&zd4rTuK3J(xfHD zD=Dz@tlYJGdoSYS$8@bIs!c0?{E9i7l8W>Iz-aTYty=P=Ahc6MpWh2WPI#7S2(8&D zq@srmFM4$H*fWMEfd+7>+uSC0%feF$MGOF=&3|xY_vIL!P>3@CPGPkxDDG|ActLJp z0Zci`cQzh;ma7+VH~^F?IVB}ehr~|)EqZpXV4;XfDf$_DW7DKhZTY#GOK+rTXaHb} z;%_Y9e#*UByAa!7^udphBzelabYrDj^+E!?tU0PD7tY?wC5pYYBnL0ZP5hi}0!F8& zOQ|IQ7SW=u%k#raw%#o?umGURi(fr!1M=QZ} zhiS;KDi(D`{w&wWx+0Du%3kJl|fWLvLGbvv-F z+|aJ*_R&2n!lhN)h7b2F*!CdPzybhOn47QC)2T79 zkYiN+$Vwg#7K+$gSFgwM`Me*rg@Ou=TL-4@oU{B*z7_zGLH=ULqEnep-J1pi*tTll zP`P*6+KZnwGyt&byljO*myr-tDD!VnKg8SFMw@y6PMIHt?_c<#WV z*J_U77jq^}8)cr6H7jur>^|@=T}N<95=W}sM*rdhByMhBp7*aZh?xzNmF{vtAtFI z*eR%4r_RyNh4Bf66c1BpBqU@hf0=;@!!X6eeFrXo(y=(fYF=L0y6pj=D!Q|I_x%(G z<8Vn1r76nIkQ;#UtJ*}Oy0~h`9VG=gm{ML{KXl@Tp3lR-UKkk(VNk=C{z+F>Z9b8# zLj;By<*{3qZBW{^XcB7iV|I;YS<9+*L$a@}-El3A!Enq}_~gowD>s!Q5sv^UN{!E8 zY^v9-6;#24Ul4ow>f$Ymb+}ye9;7jvttIT@2df_2q%`UZ^0RZC?7(wzm91e!T4;=$y853#WdibOU z#>2e_%((C(sZgyq=(Ppe$sazZ$hA6}rZJbQjr`#BhFz}#(#mB-IsP}h}cJYn~|Pd&!9sK2j9gcIbwdHUY5``Q%)!p(st%EAIuF}+}Q z%ECgv{`dP9BcxuTd8a1yJ?-ki&AEB`nQQNv(|d;i0LADP1x2NF1u&c5b60;DHLORy z26gL4h;_-YVlpcX-ng=}4*^ezKib(ZXV<9SrxeogO3td34{46QmURxpA6VUQ!1RWVj@PW>txbRTI#o1g*Y>tvd;nk!sscIseqs^#+M}C>Odk-L zcDk7}R*-AK+G^EuBLjfd9kIUGodH9mYwvGZ-4fGt45YgYKfgdr0q{K<&sf=S;N%|l z9ySVd;}sd5TAVG&ijx?~Khf&xY9J{hf7Y6=Y$17KZ!NC&?$BkDA}(%79e7ASR&{GuWa z0{|d!XxKV@_r^hOlOrNJE}tFEDhrD=Mj8NYstsJdE^XM1jtwu=4tL=v#k|QOYHU6< z*$V>zFsh3Ru`g*;Sd+G(ta=o~ujlCK^9#FI|JW?V9#iI!d<#vH!en;(DWp8EF=gw> zej6+;3qL$~mT$Ls`@~Q&0Jx4?xG8n`=$5sg)@u;JD~NmXNnUg2o@sS#0l=~Dupx~e zENT({pi^^4V?n;1Z;r<04`042y~mU`bxzf(=_XXC=sYJbpIv|O zSerJB8`gYVy^^g!nVTyT6&4jS0Du^^qClnp<(*~7pjH$r^$Y+Ae8z7+kkNZ&lLprt zRQ0E_KEM5}>#|{2pDJPi2P1!>Jab7Eq$w;=Q2+p(1~b-8N*Fw$N$ov#Yq??h zPoKqmwwu3dLJa_*8Ka`0Q1^wBvRY+9A?FuXk#HP5e@n`U;q9ZFL^li;$rE0F$g478 z_ngM&oP8K5Z2v-iRYw}&rjQ~y!D z>b@MkLdSRZ6ud6bF&JQo)+-7XMg}kpfHvraej{ehjvqCnMZKG~Yx#;PW3H;G!MH;c zYHP2ooVe+QHYD7e%1cTz)*jotktLnJac0XC1yKuxzWwGK_u zWtb=!bt=85Q{tuxeU3U>8Gwav8AMV9v3$=QCkzB6D zIgoT^LHmuF)kFP-bk6I~cD*Kdb2I6&wlinmA3V25gEJ8|{H*xO{2WW4PBYf@a1Cm^ za^$tqYr56D-XP3PXwb@Ww_yH@0zF%n7$e$1+g0hbWNqs3nM0c0iHz`&WPN^@N>^O6 zeMTiAuz(Coxk6(sS*&8LNuw%~8^0<9Sc(d0H*M<00V{enx>mEAE0-cz+{&N#K3`#C z!M$d?_HJjUbZmGd%AZRR`V5NXDYO(~Xh!N?qfXToYlioITdPus;j=pQ>wI+3oW`x6 zN7&&Ckdb`7Ua$S-G|d<_g$44GU}I!Z7Zn-4B!PEtHg@IA%&}vd-#=V0*bc?LeW$i< zy>)ee8vp>LQx+(VNW|r{7>6+uI%Q!IX+Ri;0fr$)t0+`xOy96V+F%g)wO+Gl$E@j# z`!(ES7hKuTL8M6k{63kg+P0^U1w|1a{TI!9Ghk}VdY2LBlgJJ&mC^_b}+_m$%S-oBOjLGyJF3g&=MTK$$1At+`np8!FU|=x#d)*^O zOqnu;&*%TW$S6iy+PjBDg!(vqiMkS5I|YYER&p&4KH~^2Jp-#&5A(O;fxteXQ~Sow zLLB1?Egk%tcj?x;me=<(91w0*>i6u>B*?|q!qU>t*{5c!{?lj8?B2lJOv!Ei8+B|N zA|`+oO6)2`cJ9`zj*l1v0Ip4Bht9Pm@Zqef2b;Nja7_*g&Uu3l?Z+j5{ zA+~a<98o>Q$NslDe#XPym$dDU>>4e}&(sTDn+~11a9jsRPVt_Sy=P$6h@da~Y$CUY zZ97D{2`L7XI#up5VbRR)rHKQ%-qD>}M0)Zt;EC-k*X__T+F#7Y#2(c;w2F2T0?rj% zyH{-1ZA6P;DF?Ig^>?@A;RH|O9@%!r(g~r~7>KOBynKWJe2G=%CPU^=?QTv_DRHUN zwS7YeKEravE|nThTe7rmh;51FWgZnnBLcmI-)W>p)#eR?oun8}xJ31sHM(1nt8Zk@ zP%kSUCU$Msy;YE%5Mf-`@K$3d4-EHo4hpXx;A)8j*FB_ZtNH;vhUHoLbQn6ick>Vr z-^vkT{!*@WMQ?W*7dQf`Pwg&q=MM?8`nFb_D|YY-jtC915#WSC>QVV#J*tAe5P)N0=U%l|RAqN_bgH0YqwdW^U4UT(7S6T$OkF;yi<@~I5L}s~cWBjc zPn%+o6d{ng`G-XWdy9!;33vuZRtxi!asU8Gv9)_}wVIXvBpgwtX3Z-(iwTVD9NK=` zv|gdEULn=Py=?e^@%$qjh1&B8o?TRfXm4Awm9tOP$jWXqJ^;r(v{Czpl?5z~3#~(% z^j)}YTor3`GckdseL$7SDxSsL&;;MY)i0z*M6lgg$yPD9YOQMf+j1~M;#q6#tdaF9 zItN76tmrPqaE{n1tk3w#&HeDqLfWNr)0vBxv1;@q2YzHx71_aWK-e?!X1Z+IqQqheT9z6yX3I&#DchTtpZtt6I07zr!~{yrRmr zss}mnNU2MB-Dqzcq13^na#U4s8?$gRzQoqIN{woPUP2sq3~tf3K>!~Ep2)&6pjNkT z-K)9(;%XvN8q%~&{QxV5#jU(+4H-Se@zseFg%!s1sLlo8JB74s)6l~L2T~++_G!?0 zM7wZ%5(fajfBg<(5jX<7Z6|O+jhw0=KNP%uV}gq z>fkO0Qe@>B92y$rAtV3*M6MAHYKK`92($3)FnVm?Mpb-*!ow!sL z(y3msEwGr#%AjAj10b;TYtyN@w*(kmVDDFJ)WT(bYq*uzLQ!yo)=eTjISBEj_TeqM zG!FN+bq;FSFieW!TubMceJ2lYQPs<@QdAWWJ^`e4K+Wo5Vh-u(6H&KX1qom3I{r0wZ zOV36f+lDxB8J3i}hxVVcbaIIR0SKPdu}V}}MMsNoJU0k%zSO>ANMvPS2a;ugCv~qF z5>eULf<(WPArVoC=T<4QR){l>aU?FGy@!lw8s_626dLGeK@+m@=ti|doG}K8oI-k! z9Nav@+p}U=NPsJsGKoEEwrd>WASQq#4rkh4ISKzW-2VZYX665ei&Q)5BpE9wI zrx?=jF5R8nY}CLoN5MBSFr>&PwE5ss-NN;GxnNnL!-%O91~<26GzJDa2Gx%aa^Y|= zk$qtIG2@$ux_VcN2=cVCw)b(9aWR5t=@is!?6iRmJSeS!#x28}wXEUqfCDa+SouXZ z?9{Wd8woVUtx&&p{VJY(4Dl?zS`M1fy^f!gPvwY8USdw!;3j0ENzbslEut%n5seEh z{c3iaK5t4Lcb>^au?YB52mjD8KNm|35CFn?7ETpHt5xxra?5r&!uXE0+O`OBl>!hp zel_}!9$w4e**iF*lAEo_(k-H4b!P#=;agU1(SKNvXdjmfp`rek1OpaM;b9R@JVNZ~ zQ#~rYQbhIe5KjSMeEZ6M#|&v6>gQiMG_0cYH{MY~Aa$=8S}m-ij9Wac0;*IE^0X>( z`tmANZ`rzzFPC9RiCtvd5sMZK@D*dF{G?)NM1YGFCvXnQm)g1og-2F&kpfM#7%sMT zuM{5P?`(m8oyL=)D1mcG%Qh`*R`Ia5u&|IhR0ylvd-TLf13G#M4O$~BcCOUCc^wxC zhLJ+4y>G)d9hz77Bxs7lMHOqdY*^Kupc$TpZPivo=T7b&UuXr z*5%OY(N)X4%Kn{FN>Sj1-E+ zTmk@CgI0&}#XMH8)EW=~;G|Gw{tApb6-`J)JX$T+Fc<~^9G*}t-~s@mjamaE6bl)h zN@HYjQXmm?jVhI)OuYnGAQl#T1cREwB|;tm zAjYKCl=!K?+^B>MIxTR;d@v~0df*ByitqRV+Njm)OE!UUPN`%7F$SHs_)aN|D82WH z(rZn?6A3V_O7+Wop%7p^fk;?<0~2e|YRn}dgz+t;JVvJ~j`KknS1c8BbSj1M7lmL1 zS0om4FaTh5T0PDa^TD9h=vjo3e3ALaBi5wX(}YOK(W%uY8X&Y!z8%+R!nq9~f>i$ok+tJ0djtD_h}3WTE4O9@%Zpwk;myY-h! z#gz0WUnnZRfr+JcTCM5VnI%pNEkvYVtu)XGBe)U^5z?#G1{(V#p`sXuQ55y+)vMoW zHN#k1S_TIP(=`3h-=_b6VsOhvP>yo^u0c1d3-h%?2YWFGqqPN3_Rc%>ls9@__5YlS zB4c4@A#QCe6Oce@5+CeZw&iWZdCNWjgX`k|e_nL@7j!y9>C`%e(08}#jar4Vcya{h z%OAhpQM692(3O5bDfH&g%@cXquRy8Le<_YO>J-K=U#b))bH#`@s1$~8>SauNrAhzw z{0+20t1x`?88YcrUp4&Z^qOD32!7(QC>yoKhG8xLmIAPbQ-Tha=!{cr0r&88jva zFbpB|Oa6x#>ics36_xnX3tE-o2gwGdQz%NL#!_m90)Fhu)4vksWb}UkN+&N=Xv@iH zIm+?7Fy=g8+-JfYOFs{5eqrLDVL^AnywN%9=9 zeE!f{+&@@~wy5yyxb|}VyoGw%j0WZSS~doyDX7n?Di}*qy7EcnKU|O^L{SvOFu!|Y z{eKS}$N79dm&>JT+GsQ~4D($|T8_VE%;>)zT(ON;P-K|DGg&T+a+KpYK}xKhJZ!l% z!4pYb0_%^OyJ$@FVD5iYA})5U;3!5M4o_g?Ub*X-dGm%fv-`tCkr3Ou1V=^sIf~2s z=;yCMj7ElG$|vma0|Ue|3|me_{|gaf7>?s4N#Z#Em(@rR1eePt2*MnE`A-yDdCX`z z%2AGTl;i&eSeDglwKPqalhJbgNgxP9AP{gk|C^m*gb+>BCX3`4;z@Hs6ikItU zIm%Iva+Kq5Lq-vYFS3yE|M29503)~}3rkB&F-Z^tu~fw8z^_mC2mvV&i-kP=m$APH zVFXWNA>k6`g!tbE4u|tUWTIwPN0OvSBoc{4TrStV=qis(`J0nbHvRSW%lAL(e@bDD z;>GP7FOn61^DZ&vf4X+_eum~ds!11pzH#w#T!E?Fp!{f`=f$4AbTv)$UFFio+`oAH zO$PlRm2Aw3y>jbO&iBI3O$8sWUA&T5^g{*fb7HQYy_2N)p@&uR_xJAFdHHG5|2S6B zSor+L*(aY1ewO*96$!U4-$>W}yf@TlT)TAUea;U^l`VXDYR}FCH`CSs!NwVz6MO&G zqYt{jOz^qyuAIJ^rel6yktpx|gPRXO>HjuUA%;<@k1w9Q^hSmLm}?a0a=BFg-Q!!< zZ8(`!SoHkL;hS%=SPuCsg1vQ;Qb+U#ZG%%8yRo zuXnZS(&u{Scl9A>*ThbvH{3P+M<_aS$3e%>qQ-09!7 z?bheN*99p1z>qH8M=acXdbWGw@R@sa{;C;7{$g|Qc0+IG|HS1edbwlD zkQs*xn7_-IB?#@U<-F#NhmYpgD(Kpvw;yjTl{?xGEBQ7TyNxYnl{;QbiUvOyYk5W=TpDz}R z|2{1FeLiv6Zz{_}i2jDc*0`bIrTapk@J*-zm-}$%%w^-TGE?{IiZG;#{$n zSjPXa=n?bz;_kCKjsyBPw*mkFu1G5TJsbO1!eAg9%{_cmZ|Q-vKYmpqL1Kk(&Mx1P z+J5WBWzC!v8ePEg8l4p!4xh8Q zX-NkBBJ&wE>^rjqDRm8ra4k10e}I$xhUFpu826zvA00gOp~i&KUn}2tD&Wf8LnGXO zuvoseXI1MT;)4m9#_-N99i@W*fgDN2js3etUL*Syx6slrH1cQHfqv%gbOiPzmfVmX zPubOKQP=LL$P~|bdidna`h5resrvk1Pf$eaqci(c+l=l1&C={|F<=owlnHmPQqu=# zR078~Iq48d}8R30e8)3zr|{8+e`}(e*-HzRdR7 z@2=l|m90b8-eFDaRw|vJREdwTJ&aXQq=Ro{?VA1;W|qhnKDl}=CPOE3@NdwphHXg= zOws4-*B@u7k;EaOcD-=N-}N}K2oOT-*9I9lLIGsGxODYR4r$|4uSsnuJ^%nxC*8iE z+b-w$$w#8FCM_bpNB~rnc>TtMG!?~>I7dY{@V77P;9(M;UVRdiWx#kY zA+_pP@i2#CrM|p;`%NaxvyEubB*>xk4V160UU`+S<_I0bYS#{SkpKYb(r;hAomfB# zYR3*EL{Vl>qiw|8ptSKJ$S_ph_p(*b?#YuSewD&PT$Oybkd6( zkKSaMNU?vdX4SnczI>xexqancqQYbo)UXjxNPO)^N_%nj?z?m&&$(`+05Ko`qFqek z<15!=vo)mDIkI-NzkOML!lXPnd+f~1yu#e$8@Im^cs1`=pTq!R9H9WEJ-KirrhsSX z+n{Mpv%pwo{H+K1{`DeV1Ox!+Gu~Xe`?AnLSbK#;*QhM}Ixmqa@6+Ab!oY|cP9jjJ zy?&O+saZE5>Gr9INm>h+Doq&1i2>`0(XV>q? z$W2J%7*M-GHAl1fY3h3hXV|*pe^FDmyY_Q30Y}+i59Y9~ z&FQpPPtCEv5QBFsgWjy+a+jP^+^z0<`i9F80q0vM&Xp^mi1@0!nrcw;oVV$%4U1VQ>$*QHbGQnRXUqv`+6R)zb|(S z@`ZO{#zK{x$-%jyjkbG+(mxPX zddilcTI*pU#?_F1?2lWI@Agp8LS~A^GKI*dM1URKq{oXnn(W`*a=Q4-X>QjBC7g93 zNG|&ulP|<$W0ew4lX8|DW_pqs445(s1hp4@E>G(DB{v&S((PZ#nSVNaJ6-2@)TkDw zu_=+m+l0T#%5Hm;l!`5bivLs}51EE-t>~$c8}0_rkFSO>(yG9*7mwh_HVc5@dYHq< z_wKw~0_WM2NlgzkeB%wkw}YR)9Ly00-=*-Ful>gOS5(0%WbpbM8egY|(CI#(GNue$ zSyXbKvMxkrk6d&+TxVz2N+wc|YxpK<5_)^xLn6@b`aOdr_^Z=6&CW?m3QAcY7bD#@ zJ@EK>1?I#-ROR%Hq}g~ZNA4LRY==GGIc-hzSrF&Ys^owO9qVf@FX`{g1Cm;H`@d`w znKD}^tf`^I+S=?cE`D3U2*4Zeb*7DJ8oIEy90|>izZ!;K`Ai}{wEX)_tMl<}t@b>E z)b#(@ye%(0?i%8|fQnc~YB!X#Xrh@ReYTAK0g=q04oU1_4GV-tPRhEEO4- zxy0Ec>XSQb|BDwR?cr&iSIc?6yCrV8Ogf%bwc2%z^#uQXb&jF`sQfn_sU$WReWL^; z@r7R1G{M&csGqam)*VW|>;&z1tSQnQ`Ath{vKAJ>X3GOI?$&8JEM8I|-O7uv%3*J7 zLQBhd^oeeBAR7$OWWRnCt!+m-J2EXhVKlrw-rnF-(D}#Rda_Plb8FSp!#*Mbonf8D zbfo0!M@-&Fllk6`wyl~c#dwseI2t$oYHtG>$K_U-{CqA3Y__$0|9TPkc#ZAF&bPeo zMxVC>XNu^lUpfY7^W&)>C%ZVy^kpV6dSa`bHHVvi;wXHlN+^ehFD43dIy%hyV^gM2 zU^yj4s=4S^+IA;1es-dO8DD+@pM#ZgESFzsMj?i#GVt(-R;S+}C##17MXuav?_eEv z@*PL&yD$rzz+)ilBAzQ@qWQC?xYr^R1;`;EwtTEKsvC@0_ZKH=Vg*pCqg8f0jVC1^jYT52dLrAvNzxC|)s$621Lyc`DezN&Gr zuCW_ND4!agN1+!1F_F*uJ`~bd6T{sfre9xejqC7vj|@k|E?Kz`z$H!FPHb?ypMR;- z@q!6{e#yQ2!^68>+5B9b_nbeA0wMJIi0bQ11WUFi-Y;&1Vlb4#IiYbYw?^tRxgkgK z`IxD}0tUC&xLqz1ApJ9YyD2XfCoa(RC0(yBP|cw7I+;ChM1EDexI|2AWZ&&)TXN;h zKjUXE|GP}R;{LJy@Nwb~@A`Di*p)RsP0kR#5Ezj3pD9^9p0 z;OaQB87wa_rCYb)rmO0j%}{7xS!T}zP&7CzZQIk z9%&JyDHlsMEi3eqS4`}=g<#Y9@pah`|ILGw2#LA6i+KB+MpmxE5{caJ%4NsZ86zdb zGO-H1y>#L%)pW&^io%-zlv%X@n_278PZ;!Y9l~ap3jMI+_#U_sW@bG#d05>hBmXXt zjx6-8G=+}WiAFIJnYn9gBiRzqi4%$lm7}7eX)cV!A_$o-))P0;IPXUS0iN=B_1C}9 zQxK>D_q~jOzzB3@4pGt8I>k!IaN0EJU;ayJDTA7f&rNm#?bE}}522@2B)D9wD*D~v zNA)lA-{F|rAEWj%TB8%^udqZ%11WZ?c92JYI=N@dNy+_kcCdp1~pR9^EL6&G6+f>ubgfhD!J{}kFD4u z)r^-y+4z1}RU-J73&K*#%2r0UFKm19{4x}(s;8tUlQb0yKQ>XispsWGhilF2em#Gb+4hm$f&V-m=doqF%1jLp#VH+X3JNxQHEmxqkX ze-wV&nUx_j5w$bnoO9PU9ReD(+4A0zdg{icZ)qsE5g~(!$nxR zaQh3x4rQlbjqf+7H3<;u#zu{WT53rA8-v+zf+aHYU?H90^7yij$j5?4NLpA z8W?x>ns-wX=+g5W_B3&-)xM>I!$j7r2c4*clFme3li|2dJXZ>qXmLyCM@m`6@vo>=eA1+allnJ#tBxP_6{t*2uqfmesQj0IeBRWO;j|=&)AfpSq(+?-wG^)#ZW(ZIo`(NK^Ui$jk3gD zF!uf)iJ2i1*4~}hNVjb>lToRTow9g@kN;z^)%IBogxn;n>TGv+?(yXaCKN{f!P%Pb zn5-kJw2eAiLT_r+ij-V%OS~lNhzh}04YPABjW8&4M4OSmi zu5+wo6LoPHE>Ms%St4;X=!;GdQ>1p8CKO9?2sb_SjxN%C+>1sG*=MR-GG= zHGYT;IkoEmW(V$ft?2nuX8*Y9{;FQiRi&L4Yv|w=@Oq`SR`h5yTkPs2EJ=?n^zN~5 z;&M=m-}#9J=vtS_T57JrQ;f^WAbY5|7?AIgodbilqN{e)>5i5jW%`lQBkv!>3vSHM z?Z%$(u}{9Vwy?6Y;?(RtX*PR4&(Yl1*zCeMNYND`uEOlKJm#3l4hhw!3Ex`{Ul>KbkmuZMqP!7| zIo#-Q&0`o6`F6C}iV7ecjG$&WSE*h}&v3FnMuXLCvC@NrVS-KzHXNG0r+7uz;=Gse zcgVToM(1#wT_T#@BL2x(GqJoW-zTrdRfpuIv1mWz6%+j#R{&R~J^FGre9s$VyHB}L zw%zTOSzT0A!w=>^1?e|4oKv*j>z1fsL}=Ar0ofh_1~+ZXUv0~6DSk}}^8>~%$I-4* zU9q!Rhwp|H1}t0(_dM@y5`wlD2kceM6#(`MHB+CJ(nXns@wCH zRjFUCMWa-q2Yl@7v%@`3AobtZqSxc}2Ze|jVFNpn&IwK^p|#ZNt*)SVq-MbZab7=1U=t2@ zAY;yJZTQPzb2}{L=qSFy_?;uqxJ?~=y{5|YiW&RELr3RHG@~m=Jdc45NYfr}_Q=98 zNG2HWE_6sjXigMN=JD&KV8Y^p^5#uwy;3M)0CncP4$|9iPjYcN2{I~W2jb>i@0-?9V#VC*+>R}AAwVhz8Aee z3g;*pQ&&}zk8dANJZIi$olf&7EhnK<)oinI67^>yxIfkD*=*>XzvPi9m!v(RRaZqU z{$jsq9#g%R-^V5!6|X)D2jDgt+11pU)OZo}v9hw#!dIylSqy?H z<@e6^33#=0mqlQ*p4^aArGjot7M-j&3|%{S7D0fR(dKFbMkb4?KrYo7dAZ(8gC&1n z9Goufh)b3#JJ$~aP*&ZYwd>m0@7eO?m9(j$tqA0#uGC9Z@J{)}P z<91!BD@BpEYXuD2MIGF7Zp;M+V}5h`s77&Zj+rd@JhZ>o>5*-@sX2-u+j7r|@-y5z zrQjr#?eKi#KjU^cIoH#~9VpIe4vDe4wvm)mdKDH0PVnh|7bQfFJLo-5`Q4bh>~->` zn+|hk+bmrx{a$rfhP$UlP9I0}Gii+H`EnJnbE;QXdU~Ejdn^>(AC!^=U_`KV(Luvu za9&%4=zQLeS*(ORB-h?M2d}I+Qfx+@bPHCdZR`wPb>lQZn!5)ReBB+6;Ty^InOeeZ z5^fhiB`55I++p_GPl%$bx4RM+3YKb2QUw_2mX|SCaob@GyjxgcP+NhTBk+`81+kL-~@`EF*wncN-V=YH{kCh7O z)fT$m&U(kc3Sl0sPDz66_|H?wqE;7LpHx~kykU?CW~xib%I1W=;z7mgY8`viP& zjJl(8>P0Eh-oGWk2o-s_y90XyUFV%FlKazgZbW*%Fco9jQAXT4 z-8Rt@r9$fV|0p^z0&h!lKmc@pHKr6#tMQikAdYZni^O1Uh|YXpf*f@5N=PVz(b3b`mUIB4fx zv_6b^cbxNf=z&;-vaaSt`3{e!-pS~|AsD}knMI9axq`o+K{yUK0Rk5EtXEcs=hYsc zY?**|iL8CiRj}kDSa>_vFSaX9D~4V(n9K8hB06t^9H>YNQ*?kDy2<=UTjM>?)(#(-&6DD9iYT*b!}cGGpc-SV*`_$Ls;_qO)fPv zk?(nrCj*S>e(zn>+rN1MWkRrlB}uJ78Xykgrv-HGcRqf}fw<9p`1*xDM}xW%HFMch zzSUBSi{k-iw}P^a~;I>)d>aHOmyTVm>!`pGaxbgWX#pR zTX))^c*NGItWIyaRT}TNBM1Qq;V;?_y)t7tVhPq-DojHyADZ8@#f=!(9@QNkgtU1w zOeWz`qjtmree@1vN8{ch+I;QK_gVbXitO83Yg5kx*vQ67@I;U|lGgj~3%YiBUwx&= z-)%HG>b=jqNI1wBTwl+Kz(-Imx?7~OdWb19hbq@KeZ$46UPWk+Xb?w*Q8|+x$hx&^ zn;+`X?A^3LHB99*5J~x+n3gk|N;cg$2N5A{IU+|G#^3zWF#NW?21|yuL$X&}EiW6U zI8fNNwPr_TF8K5&Ut>t{ak-kT?Q>&nlb4^6oZ}qa%CUltOKk#$zNZ*yn%{O)^|B9x z3dGJ=T}AD-%^@^`Am5awR_E&9sn`$&Z^RVxm9S0~hD`ja#Mgxqv?!A-$2)j_7y|l` z0d~0SGF#hK$LA<8uC+O9U*dQ5B#|1UJs5-ci{_LF@64w}L4cmA*l2*@$-33*EctKz zY`!|@%YQAZ@Ul91@&miAE--TGAmEzi3j|$CA~Cy?| zKhikq>fjg4h;J%lLs4KXa!EqZ!WgXXP!9EpfkE(q{Opcb+g#5}@s{2BL#s;kOOl?) zmmj$Js_px?X?g+GfJ!)@cJ7~>U6sb(bnA!_C}akT&?Da z)T6J46vb`SP|dUG5^(46rg)h`MSb*_+DWu+WsRWvxXKrY#9)o=5m1=)>(ye=5QO%8 z<3?UBpASJf+1f{}^dZB^eDc{r&&g4Vm0A!9G0hZUD_?cp%O#-y!V#gxZq?ls%Lys- zdsE!A)#a{Oy)oaADT}7zWodb=ZdKwD(!FJ)e@vZNZ-qdjEozp$ut?}3V<5Bg=0F3Q5irP%8;>}g zCZRb=B%Z-e-7M1gO7=6~-#8B!$eNZI{$-px1+1k!?Y*A)+^!gK^Jm*UTyE8+Zr2@7 z^1L35hjc{Xy5AiR)(ZVLZt7x-c6q&*7vq^Djog(Wk|!jbuLT1TJl<@-{Z~JEaQK^h zIpU^q5fm;jwd6Vq^UWGb*5&qOX$*=X&r$bn7h7uR;VIJUO(Dc>uzU1-dW7)SddS81 zVLo9(fYFOWg%?z*+P41kcSbBX9l16&ut|og7M_Le<7-%K%GTuRG|uUII$Hm;5+s->-tBa+KRU(aXt^F^ zRAtf~cm2oFbJ@o1{dOz`<+eDti_89?nW##?jBdKpB1!Wp1W)uXjhQvl@CQ2j%iHN9 zs)&WLHZinjdRQOU*dL;u6-Z~yR*TP;3>ylxgzMKZo<*B*I_j8h;4w+s@c|H9?l6DlJFkh zadCUyv;5#ri&UWdxk^V?9}}<=s)XUi*|4uXYAyA8U90P^MTqH6gs=Y zZVG?P`(WYTujkX-H@{cw-B0hl38j>tNpo*B7uhCXZroVW}IHWS{ zx%zb6!(|suDYMbTYNTY57w^*1;L5}h5(W&QPjyr@$ScL5!&toC?#wSwxZ^XO(Vz|m z^>#g~OWGLH_0F&#vCZ&!w!4nK$1xw=0Q-lZsHS3>LY9|CUF1N!?S^uw?|2WvPjQm) zxhV44AuIvR%+S!94kf;q#u*pFJ%{<&RSG2A=gMO$4~L^4bTn*EElVnMw#;mrFF|Tp zx;6qIEeG9)uG9F`JGXAIU||FO;6CPLZGP>c!k#qr*IXY>PFbZn+s>8$@}0BcV0*BM zIYSd&p86Ac`9L{q?Ow_k>3B#F%Gko(4I*za50PLolF7*D)3&*!7PuST8`_}wd^Rp_ zcGaMf`4xEzRJkFU>(@EFDFON(F_Db-Zv>oG(2EwZ(&-lQEdno*2f(UnR)bdlDt@s zpX2@9KkrsFevJ53l!*hP4}${Oze2gg{TRL)8={#%^0wBMjH)hkTmcH>)*N zGVuRs(2}p zS+;ocz0h_ROdeh4x7~oG-!#wn(+@odgC+#!U*lpjxR0uxgw}{{F zwvc?{L-#53baG}Yxyjx2nK9vb`?nqxB8gd~M8mF-3Z-k101+_bpu1dJ&b7xF_m3~j z<{J6=Eh!47z~im5v~3^M^#$!_S48aFY|K}_U~3551Nz|cnF{~rf6Y#kcs?RDDs;O$ zU7|HGDULaV`?-ozG_NMDlQonFz1cm;`TJ&uCkyaMw^k{PQ<>g(eN)^{X4tk8vE`7d zm8+0+Nfe<{#X^laYs%Dy zdvPxhSX>W#JNlDwIS5udM33fG%naJ&$ES~|e(LkPENj`9*y@eusbH6tX@;lQxu)=% z3>j$p(0%e2?FDi>-{F=}B=U6>7)+vpr32QH& zIT%VcRfv^C-dW7ECCZnuu#I-*2Qo7`Y{!ZgOj?@3$sv`fx6C%#3FrDN;R zKfi@rO-NVMUr5W8DwIh)U=%8jCkthOP4oG9aD^kHS~xcc z=%1vS@3y`{E}5@(s(K(ma}?%W%2X z08=D^R6K!pTme;ho^`~&cW;%C$$oDzmlCTA1Se})mQ8+owHX?2@s2-V654V8xYP`h z=@B_}s0pbwV-#%GEMpLZ|5J)@G7LQ39HtK2DJvvFA}US(?}C1|!!-9MrE zY@j<{)ft_QyA!or(*x$rCss5rrUDb4vA)Rny0Zm;y}*uFQlrVAM??UCr2nGpate>V z8XC5wY0Bg&p(__z)Pz)SObk1%qIkX@9_m$DXUpx@*SB;30T82RkG4=&|JvL7Y{R%_ zMx&SZbb-HNI)*3ZRW`WD4xRp~K$-eWQU$?cHwH{O?yIM-NWv^~4c(a8H~S*6zslVX zrL;h1rwJ;ZTfERjT6+Rv?9XA5#HB^6#jZ6>5-*$X0nWzCfRe%3=r(T`8V-=(E zF=&06fZw-}wU)zpaSV)Z193Y->r5DG*(7oQiZ{$NRg%>lZ?UR^WO|T}l6p|*c5k@5 zP1QD`{*I1kBuKSxCsOKPz~Bl`Y)+5N?Auf3=lcsX3xb;gGuhQ_;L7o+!u*rsvQ+F7 zF~G0RW-i~`S^p?|{W-T=qSbuH^fbeLG7Hf(|1E0*{P@iuaf9ErbBLDab+MY%&Ha5V zaap2Q#}KC3DbLOo=*N8odAdqmU@W|QugO8sJ9>Y`sIL%e(ax<857cvI>7(m5KKX(jtpV_;lmyW#jfLAd6%=* zllw0zHmb@rKiNbG)@xrs^B!{Wet{cY{N zw561G3u{TrVkxhbVq7tY?M792Bb5k^qw&tdN^cKh?M-JBZLn)%n1_u)Aoj}gqwSS3 zJisq~*><}<$NX=1W@|3!d~or)!(la`?=HluNlM2h#ygs@H$qrkknL4HNjecd8t)G# z2&!fSAzL~!E`=Th`KgkeIJE`?)8d56!-`lT-W}Ub@cw5Xm&;LsB2jCYhqA7v5EeG^ ztOv0yhzCiR+5%Eo#j)RE4$bBeFSs0KLHM3e1E?YgOpAsH)Y``rQr^j)~z2oOT>UYRCV_qbmX~}7?>dIM?Hb@qR?l85sG!_sI;y26d}_s+Pe12 zZ)xtV2o`I;6e&VgXJ)=fYcAH4#*+&qw$!Xu#m9bw4r*4lmK|?>z9yrl z4J6UzO12W43h!&=$8D z$W?l%@ahZ=%U4tH%i3;w-*NX^WXchZQrO|8Gy6UVf;YTD|UB&*SV;4HQ$*j z^KI2rl_UZrCuUqL2(;O>Gq?7pb-Qj-my|zW@y)lfSsttEWaj3#)~pr8r~aD}_i#@@ zwM9jiAHo*v#Ab@`k+QhR7M<3ikgV>)0XL~Jzxq*{544^iczTS-SCVPy+=BXiT z_f!?y=ijtk{h5~jZrb=KsFaKV;8=r#eR`bq@iM%*62v>__%C4+WuTIgn=aJ;?82$p zxjC;Rd5HLO-@8-cJXU?Hbt8E|G?(=J+&QhuNkx`JLDxevaF_Gsu>JL4jrq99lz|?m zwM@%KO^0sx7&@(`LZQ1I9mVB550Xxgcj^GQa8tC!$z`8t6fF<@s*1JRbYy5<#N%hC zD!ZrW_0|0HD*Z2S5R8{(`EACercwhr8rC%nRp+ojB3{t&M4EeVGS$@>WQ$EmwXR#)y?IuZ-=YtIZ z27(O~^>@>G3YsmVbB>Vd8I!~8B+3`;{D}G>iBanPAOU~Om04{}h-bl8VZbbQ zvW6&5vDKgSq;pYnltWIL(s?jHLBbcZI}tQpnm0+vO`OpaAT1*c2e3}e7RFy5Rpg_B zkWnV_V=vooVG0idX9e@{R#8Zy+j*dKy+(^0edNcU#N`nK*4pg&-E|dhZgmo_bRvFq zpr(4;?mss{n?F>_Or$7ezDo3J15jenC?vx2ppt{><3!Te`*&zeN^^0V(`onl?pae1 zpL7&se;Cl$4!2Z}Qo`}O6eSaegNFO4*Qm(Hae5h1`Uw(?EHgskqQ&aZRv*G3J6sC! zJJtLmvULMHiqECYt7!g05lsD{A0i(VV#B8A1amaTBKQ`6#BW}`>eW65UI+#lv7?U? zSL{H$Bq8YEhW2o8y2QQZL`bN)(;P}U#=a?KKS{9I`u}^8JyTLpXSZA^XmG&-QQ-=x z#fuibMd{EI7My>V&mTkPZ*&6^enJxc3`h9&JaknA?0wSyfA@kq@Ug)3B7g$_8>{aL zJy0rB`YwSa`P)sgp68U|$F7pse7Aps1MV~MMCOfSv`nLVYI5?kL}H)wYG>zW0DK!H z2hcmbG_Qon!-{x`bAa|;ph04i2tb}YfAr}7(i~kqPI@?AKHKA!Lm2oF5aIlkX4a6C z>uab=PF`FFlJfCw&dtqOp_$f%;6_PC-6Mj)-wQ?maxoGmM$4p1CEyxQecFxM~L zWO#O=GJTk=n(}UN0Qm4FK#k-b-7xN&shGVt;t?18!aqTIV&26j3R`%+jT53;|I_ z|1FW1SnvVHK)z8*I1+##srY{ro7f*zI%viUnhBCmk`QEUZHaeoUApxBqdULet5yE# zrbPV&m$am18yQoQYjbwmPvx?;T%p($C%_%;;CmUD8pFu|NL*2|%y* z_SgIp$VdK0Ch;~9)w3O$fZcEi#Nipv)j-zTicW_p9WiypXJV1HNz+*D?~~Jn5dNs~e5$ zV<9V(k49NNG6wA82d9?0lEl#lzNyg2PN&x65;Zinvh9Nb_MBeLxv*2>I62kAU}tC= z?ha5R0mqWG)pSyBv=lct<rHIP9iuPBs)5l78|GqEx`S8u!6{yYrF8vN$dwFnlb#*jb zbw4PofDVxRY7`#$-G!T_1q!TeYFR=(sd>^jA8bN#>VlUqdo0}Bxw=MUx{mEQ6MJ^e zE^lsjJ9Pg3CUMxAXxIUz;$-jW3Tn<49AB(ZHx9pps*8)0WpDgjVFjHn$D3NBD>^#b zJ3BjhRLwe$>iapFut53}h{tS!+V6`VB8Y__OMxEgi;2Qk)C_!<$pv$jy8*Ti91HxFwZs|%TfunFf$&=?HORJL-!4_!9OoO+fq)%f zzOUU9)Y|@g<}P&9{%I=uDjW`yr#L^LuUR05>*?%CGq_E=Ggd&L!>-A8A^IC)Q|(l) zkH1FjYjr`h9KNr|=GN!$4sn_;P68uUPR|DJ;&xN9X>?`!YtQ752dOUhjucp(ZSE)h zK0apRIN~RHWxtt`k?;x47hQh`2N@wRM}SfJcsbH3~)uF008Q4og5$l0f(4?!O^M7YwcALV)n)x*vv1H?}9gt#ditJ z2Y^3c-5sifMCSvEp0)80v+)$DSMq$MZP?^BOI#HVn8yaLF*l5@)j!ydJkT{0%$d3E zELZ2JwZY$>BYBkT9V*tWpVZ03F)672v(LQ)Vp05sz)2sB$RUA*aBQ1IV0qG{o(fBl zLiwUaw#>6^+a}W1Z@6F~(1gvGGYjK-#|ii}AEksBT(YA0jwgM0ysvHE!$Y^i-`pGB z>0Er#kz4%JDYh?Q{!cUEfHV`RFxvpmay<@Izu~OsGlRu-*zTS*tD;0+(a0-L&He}(}42G>$&2?`+f{lPxNzUIFV z2N}RHpfKj#8nDjJA!dKU;!$?|k3em;PAx-8QSV?rSj@f`e=leM={eP@>=8=@XNXAO zEG-DYveB~3WQ5vydAC$o2m9xug=Mot3F*vQK+~Xwrh;pGxOQkXj{V+Mm?%doBuAI> zkVx5-&+AF@RD;L*w?GoI+zE51sbChEZ5F^&e*PjqwzOr)@Jek)=I7JP>G~)l&b=Cl z$aojdc%&WyFyjGdcZ@A#1Aux)Z?X2-h#B+xEb)WdL6CJoKm@Q3VEd3)t#Fbr93IWy zhf;~0Z1fn?-~jp#WKCVu$ersrP>|Oq-01uYRrd)&u(u1?K2hHUJNZs6j*a@+@JWFsEx+6nF z!d{QND2PLiuCDg>_VuZlysEI_)rp|%DiLKRSH5d>>3hKF;?Tb*_uKz_z^c|%vbT3` z;n)Y;`6;T3fq|haS}BD0_Y%UOLsiclPA>TB69?NztJ)?eG&mq5{4NK5Nt|GA%gSMs~|CQp9w45#7$Noa@DhO`j)1Ctr@ zlaYLkae~3=K%1Pb*c=4ZTDf?ZJGuTStJfw{xgzmqYE4T?Syxy0vV>7oT6*us%fr)I z3molqy}r1-l$MqT!VgGc^fFBJ-XC7vjT7);$&XBMUjLuZL55IT)+@0Q@&gzY{?!Yz zpSeGFYSEl@y9gLJc9l#-DhDF1UfUe=7kM{ae` z2^Yg`!G**Kk;DEJ5G)OWHv2%n^b^<6zlVMWlmn}u4EFPU;t3&Z)X8JlA|l@c#H6Js zr>B8$xn+wMFfgIOTl?WIO(v(i+n3f`q`d_jI@PJuQzNNKBamB!dAn$|Aod&BsxKz_ zI^dyRI{zsYy0$&Ri5X(79>`PvpCd>~^$S@j|K`4^$Tu-+ZFN;$L&HaNgFgoNGo}@2 zHD4<3{`xR`Py0UXokEwfT^tf@f5!CR(tIgt9-|BbE@p?wQGV!;{sHTYlX~#gECTe| zXYGEc2;964ZQuQ}h7A_t&$Z=jV0ha5%=~}u8s_b$e%Ku2*sd=^2x*c4b9&h>>bn4? zMIZx}c!B_s2x7#3C}|;WVG{HlkcXHX)(jfauFzB8Dp_HWPh(hsJMw9oP6;@9^oSNQ z6f1p89I@Kv1hGs@NlEQc^ki=R-+e$ztj|h64+IKWgnHU%20Il5if>tl=?!DUunB;v zo2>+2;Pj3y+gaq#0lR>mADFQWFY%*)oVoF&=X1}{e(eDryOmvRyr`{XZ%vrl%4u1?2z8 z(2R?TdpuuhE6jyHTzK)p1H@>?C*vz5Z7Jhqa#AVgKUV1jm5)nGX`F#-i|K>u*_zMW zFE^U5to50lDmRxNEIm*s-LcF^ zuYX3~@QcfU0XkkZEqlX#Kko0Y>B67NIbLpi1-OCSA>qhk|0a_8G^4iOf=Gh5pGgx? z<04Xdz%50r-D^_9B=0E{Nb|R3PSr}!*F3ZSX%)G#vKDDPFS=qXyZ{|>BX+^_<75rC z$HSsfNELYTQD)p8dN{noZ)irr%!%cPYra>`diWM(9K;uU{G7Egj}byDPN%F)Iy$6z z^p&^VjFk7^qBF_;EOCxAR*x3Br$QNG#Fxp`QfC9W(-kx;^Q zs8%j&R6kKnr#-R6&t`=>hKMwMj|_veRxa!cb?rvqwJzuvT9?vs&xG)&XCP6md55W# z8vFR(Ok9#fTR--X09RvSb-UH(liVMy8hfEyr`x_V&=$;(o4LxoyC(CX7-O2nkmPsB zm7G`lWe;CZT}Y(lwD$AY?Svr4fzcsV&+4Uy*`Z3Q2id5=wlA?tN@h;w2ZbqaR=Q?# zhO&0AdA^{*dIno1SK7y+(l1jAa&qG_UsMRghHWy^L(1Nf^Rg^g*+hN?Op%=I_38DjN91eiA`Cj3r zi1g?DB;K@6nzuI>t(3<=>PV-%tOr`EPt|N`Du;Q;!)-mozkiY|mZ~mpmeo@hSIcd6 zOualOpj^#0%!2ROKu~)F1O5Ls!;V}jNWhFlBXdzjZ{hP;l&P&klkj-Qzu`uaX1Edo zBO(geJ=s^v5QxHmlM|{6fbDc>15CRlY8F0CaiWhzZavHxGjkh{8X+WDOsSKrQ^isF zfP7$Tq8x&uvMtKq_VHOyNQPH4{^>mn%bhZ7h@q}FQr0|8!(5{hD>WITmKpto)>^sw zOF(5YG@OLy5>L}WKh1+~lyHw`vW2oXZEue?tF$F+1~U}~w3uRCa(by=Z;!qJ2_x?5 zoG!mlY9)}|S8kxWfI9iiw4_L2o~gU~I%_huv<^}?m9ct%rt4-~6{&o@9IL0C3yeF4 zaMZU*81q*Gsr;Xw^ohVmo@ce}%^qJM+x>@Sd-{QCNl8&@QY-xip^sO?V9o74lBCss z0+M#jGn_?+!|C9COwZ!v4G3>KfV8K(e#`m@&+5J#J+N2N|0i-QZg<5Y$JcO|%X7D9 zr!Q2Y9T%36Her^t>*Y$pWXQ#kBfck?+$s$5Aq{n zlucYX{5ea@{FV2J`DAV4ZxKWKjX zkBGaFq$Z3cK+o8OIDdFzn7Gb+QPG8`|yN`dJ&9G17)z()leW8Kzv0Su0!uji0 zctq>+)yOHug^qWF^|fNGh^nnVyvT>-j?-L#725dwuX{KJ(?crtkPrOYd1Di5Dal$* z%r627BNET1{|^R1`M$o>l%oyjUf$Zbc2O*YOO+aiKQuo<1^+dE^VQHQ(sT6q-wE_U z2ou{l+u2!vr<&*ra^)%!kpB+?AxV-)l628pixVV)0mpG1LKu!04uy7 zW^lE{5Fp^VGKwb%977z(A;54P$1nhnW!TT_isK}K1Iw`pFdQc^#4;>b%8wJn2!a5P zMHr4~tt=gPqD140~!04H!9ioPnWIUtVZaGbzM0taAA(ncKP zIF17_6j$pITjV_4zPvaVFW>77~)utLkMu309{s&${sKR$5{-%Rk|348LXtml$6Kg z{Unt1r`#w|G|O=uLw~xN0`wNF&=yi<=aFX`XV z$E&BufD4vmM~VFYum8a z%u6{yE6%SR=U>Oi$IG)~t6ux#RREBBY--)mDQ{Q+06OQsCxC5 zs~dSZF@M+S=3ZWY6E?*Hn2;n7r}AFxn;u%<$Fp3;(9v5`iv2&4=H9kx0rk9nyuIoL z_S+qs`PI{o`poQZ{zG@jieG(mUaQu9u4fl6F}keBD+fo_s^R71U8R1jDR-na0C4KW z&Eq=N_44+v>=Qb8QCz;Ehn0Vf5AQZ**Uj^Ldj-_-s_0g~>)glrAIDwdmGzxl)%33F z?b9f7(ebDBUsder(iIxtnX5iB(Z7$J2WvaD=(YFy^@Y9tt5ovvYyaB|6_tH&SFfPD zUX?tW_MHDj_VF}I`ToY79zoTsdHZ&*`zztg_oKCN>tuPQ!1^#;z~_Cn6^ za13w&knJAZK62XeVvd}7c3hLlG0)1lI)r|EetDbbwY+`2YWTHTar$)`$6(CM@j-QG zoXQ3O$hpwAMfBQhj}FZ5+n}mQ`5)W)5EXRocp!&&YT`$ zb*lMzRjk-_(q5HbcV=OD(3Io3_YZ{C^{N-vo;xP z?Zj&q2mruwR(50W!j1ucwX6DskKFi1i~R|6#t2-QcxAzWHjNrKs9&RM>tX9&Dh*7V z6cE}6hktL=vR-{(-{8(uPsZn=Le7cv1w7fS6O%gm*Q)DVw|V!?@sv^{RRKj#ztHg$XbWfZ!AQ)Dz1F`PZ-G>)WK; z-a?vUlf-~nTq)@_6@Gpywk-DfkebJ zAvMQ-i)=P%!^4X!BifC)`8s9S5PvV98cVL`^F=14Htp#A!7Uop^Q}`iwD;^QscJr7 zU?O2}{obeJu=NkF9U0ZWW&K*-_1jFooTlfKI0B5%$J6ia8x+>GPW=WAT6bT6=8YaJ zG+GioA)!s(JHKDE`gQBqZP0qa?0YF10iTEAq=3*q-a9v{S>5`+^@F<2IQv|Rkt7a) z02z{ribNvpYsiQ8#gwS1@mnulJosB+UGIwSo}FeM$SoW{slUhg4cqdpP~$jF>>smR zH?HOF<5R`kZ`^^WhFLQI<(X0Ko7MKIRqob3ShsG2 z`hkO&9!}R38hkN~5b>CoCsuUw_p9$)zfoZK)tBF}e10igEspbW{mT=pdxSQr?_0lG z!|+9CKJX-Fs$0vOhmC)t76=HyNj~JoP3#sh?|hbt#FTn}x^JgWTdzMow{&=`hQ76G z`gWbRJy}Nx_(C%>XT!koaT~7PJF%f>coW|`wVFnaKK(+$;}d{@;PG^6_g9XIX;inq zZ~fYh{JX5U@QUUMcsxEPe>tOH(3Gtg_ss1R+P2Hm&1?Gxbw8Vwxobk}nzdTZ+VdPF zqQ7CxD2{OYsevay|0W@qEu0OlyLNOgeoc8_w zO*3cQ<~3QeW9y7=b~}gn{QW`-0szh1b*t7qvyEP{bJwbw(Jxo`8@%cU4TVln9LMp* zK24irr;i>>EF#zOC$>M3dPn$K++N+U_rhn9)7S0Uw!C*s54BPY0E{X>H6x$G0H~z7 zox(R|)ttR)-_}(V8cIO>8IeIc`TK_PDORl9br4>5Z%qk41q=#-f|h^AwA?OYWji5=Nv z==ltqaPph6dQNa9SHF%^x9r(IyLBZ1-Hl~EJI}gWXT-7tyViI1{4glE^O0md0BGJG zjPdV%$E4MwE&H}k4NKfIZQ?^P-#fHfaKHJ`ReN@??cnx$ zL}=$jX$*j3`@qO)H$SX3=Wk;%%E(^=@4efEFP6 zz0EVXJvAA&YR{^vt)FciIdb_m3IKq*y>dXO$!GiqFWI$YQy;IFLp$`@AODrML!_2w zXXGlmkEY~;^vrA>g8_gp{X(~fVJn_k4qdo;&(3uNn^nMB26P`5b?-3as&M3rZ9CTe zR_W!kHa!=-qW}POYQ^21OXloNi=Me@^McXl*XOhwz1!e{n0koB}+J;zum;E22vYwt?x%{ki_@wnq#+u&S*spuX{`2lM8o7Gg zk_l#4=X9ESNP_?Xw26menzlbA^`5+P$KEZA+m+9l5*f7QS^)q6owdAQ@Q6S3y=JZ7 zwQE5&+37{=FRIN9aVwlMFDo-&{ZaMR`RSQ?pM_nZk<^+nbb_wZx;Ekp%CW`Aa9`8L^h~qcje&%9NU0k{F zRHoC!mHT$CnCuWguU+2_1puIq9n-e!QFWt5J9ckgIIOY-Qs{Y&yU(A~tBGZmmUC8Z zSvPm0mk1OE+38sYhN-CF{`!FWT`z;*thGCKZC=u*ia7?%%blaTOgT|+*z#Sw){G3P zMyON(pijBbt3!wLSn#rKyVlR{n6!3qkC`Vl2!P4hvu4R9Xt8kHu5}A~=j|QYckT%l z0sv00&N#MV%}b9S=dYcd*|`>-b$fcdmQ&AZ+D=%sVZ-WiZM=bH0D$E4AMIJO@eVm) z$>yaKqcaYU8MgQuBjA030}(ud?8LIA51_`h<=fWI>qlRj*n7%xwMf7hpr<<~wi&w5 zDPrQP?s<{tzi`dQwG)FAkFGpuBb{eqtUC@}N8#nwOAiOhs z=AMjPsh%t4l8O-|sYzTpqU+o%;vtLHZ&)*_+_O!+M$CVr;Ma-j*Fy7X>DnW5fk}Ss zt|fbO!n=3!Z5%yg>doa_msogooBAwyk>3#?fyIjv&b#cQ`L>R<2C=EcW8 za3TUQz)31?^SCaPkF$N|tlhY7dX1F5eFsm!FXNkwm^ocmbaP03HuL`(t|7q1&C>P1>+=MbV*tr3zQLzZ;bn=YMe7(Ur7Q=wk=VxaWXc!E~5XUHzo*Y=c z@*Y=f`s$5KCbiAkGp_${f8-)Qfw3xC-kpP+Hk{528@FQpifIktYQL`IFK4nMfsoF) zII2z5rbNdf^H*=&u(VrM)ylyg<{W)Rhy*x*Hsk)DEk{iPW}iECaA>>8q4UPnceZZT zZ}#d{^Eb{i`{5-IiMX=z#(Cn)KQ7X{&TeQ-fTajQB&3P{HHnOZhB1ZCoau z%`Pf{IzGO!g|OXC8X<(POlt1hb%mBg2q|{P)DyaOc~(@$dqG@rq12pE*MOxj5PI9cqHVniSN~MAUkPtLA=cX4GwRd2}N)a>C5Mt6#hLtZrq8qh?*CJ}HlRKOw}f&e$XB zQlBahjR~pNX-#q0=3SiNQ!ny-CPGNDV_-e!x+7lcOa0*W{vn=KB5oFcs5;fmy}_b; zGK4hyhc)55cY0W)YH`cL9IZOtMF?qL3~Nw6VtIUVAvdQrtr)&u`B|G!jPR^bZ&oTo zNP0Qi({=V$%@>5DJ~FzQYv7c3x{@E#*XOmb7&0gQa|M35%ma5Ff3mQ9<9GBIg4LwU zg%8Fpjkd4UJ64Ynl5gu()uq|wj1pscPxq>Bfip4?l3$tVWL|GWLa|(xTf3DvX+AbV zgAmGJ-@Ccr(0!`n-a6d3l3%Zfa)c0iyeP^!YI+Wfkm6DQ+T}Z}Pbe<^+^CurdvDWz zQeEmvznab?52ho8P{x@?75R0>6stA#Kv@bhfyY zPxg1U_CK#eC~0>sCyRbZvpy*?X;(kDz@cx7bSr*C>+=4Sk`O}r_v4y6*XpuaUV7I4 zV~C%m`q-3GA8L<|Y33Qcs8~PsmnM~~*Y0{Yi*)j|wDjVxe)M~UNzEbgYJ^bk@-D7s zq4P3mq*W@loa)@9R+d#-pLwrAdW}k@(&}lgMu(K~QvxbV>W)oNFgmq{LF)be>bO>& z_?*?M3(ACCu1Jr2@JdSZCRz2Hh3 zV&vQVSF!OO9j8J_uhAojo*Cw**qOW**Cj2sMTfV4F!udZ%#)0kX{4!=Tn?U z%Od*uhLs2oN&+PV~@&cO0Cw)-<=4r<2B}F3PQA=(Y!i3 z#mBwT?pMXKztygi$Ef{p5t5x48_Kuv`{R{@(QEYxC2Z~LWZii08y(Uo&JA+)j9i?i zLwdDRM{xylYpX~cIxW9LA@=RD3FTdz9ZV}KF#lq>SM|{cU#asiw{L7yw=^@sl&SBF{|CuqjtmYYd&a^ zN^a;4nO0eFX-*(fG3s(2LI}|mrB%ral)4Xx$GUoVypXEZDhlK(O~LE)og0@Qyz>Q( znCz!}0z{&gQ;*3JqBI&7q1U^IIGH!z9xp@c*K;DP2s{VGN*P+KR%ux({d}0GZOEcK z98w(~UeB_2?|bbxOIc;QK);mCEIEjYh*T%#SC11qB7aN}f@~@Pv;3Q8kP16LqLqhmgO`k^?ZW zFF9zaPH#Jn zxBRWKIRF6gtH!kT?s9NP@}LFoH~^#9xq38e*08YRxOFAh3YRj|bqE9|*6t>nCl~hJ zd>H%SMQZB(S1ON|i2G=*MgZmwLj5Z)Idf0K}tmVI%dFJ^+0ls#6t+sA(KxMPS zM_qk9MZ(ye0suw)v;2&--NS-T5e7l1(-V{^?i~#^zI5p?K_He*&zAx?e_`CA&&C~m zY249zy^XVXo$7_%Yhh*SYFE3KhmGN1YY%gi{EQq9fJ(adLgm}DT?t*|H;imfPrUV9 z4t9^O@oR=fRw=Hr(8JTmB8g)EN}`1g!hy|rlboBGv}JJ6A%h$UWlCZJ_9_ja;+M4> zCn~j#u3hx6YxSyd?OnPOzzl$4IaK7L#1*`Ln8XX|*SbUyzI~{_*ZR%Zlc2p<=|WkC zF|XNc-}$MIg>HJ-XZ;}b@N`UxJAd}_{TB%@6B8cFz(u0~a0wXR#(U%t|J>6ZhmIfI zs#2llnr1k}aWn-WE`5}iaqrd#tB#A?NlFziYSF!StrL@j8@}!^X#C)gjcs@UtS0fz z-Q1U|=%Dikn+sBX$bN2@Bvm)HlQ=tz60RS7isB_9_viYaWnw1dfK|WS7i$l zfDj0EZ*SgyS1n>yQ%78;(Hgu1fe;EYU}>gNSSv@I)=_#K%-pNHu-8-bq
KpFNO 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 %_M zu|>%2uOjPUY9GjG0{QHQ6cj*hAzyGF0&}qdPE(WiEDk~1S>opC7${S+PYxco##Wrl zMvC#J$M=^AG+(Xl1zMkEgL&?6hCe_Vx$`Y~V1&XGTut2Gi$c4q#Es}{_GUwAbB*oM z>MNrGFB|6+Oylzzth?6pg4Ihdf+piR@nH`C@9~)qW57X0Mi-3k5&1c9n#axY;Z~Dg zonn_YXF?b9WhR!{-m?93H^}@kHi`ub=?ieNE~R2Svt`YX=Vm4{whTX1as56O1`KvP zDxI3I!Bbpy;$*p5p{_Zn@rBJEeJ*J6P|fp_Kohs175M$7MU%sZ?Ka3ckD-K-Dp!cL zNfbIyVl97NwBxbedd+uv6+ElbI;iX~1&l?ju>j`vtKu!lRRSqz0`{LpN#6cD8`;bD zJD!+;fYYTKD41%`P2-*Jiio4shbDvNma|YvitXBHe!%RFBZIkMgLbFCH=E9HX_0~@ zzg^r#E82H|9OcBc*2hTk`a+$@~N!hm31gfb=6ujH+9 zw;0w3Z;6RJ9;y>Di!aw6=XxID3~nyaawH195cxcKZtgxXZAIuUAp?plDosBYhjK

NvMk-eunORUHLJ9l6Ke^6>lp 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

H2LPg${Qi)RvDMlO=1vw}yfB5hLsUB{z zKS@W)fur%*sK+9OJm6BO0v2(DU#5O6Tr+%Vkj=HJt@}Co6DBAKqKfyth<_V8Z{J|j z=p^p9Gs5w_G;18pvixpC`Yk@4rT~NR{kPqnOkppTra1ED8xEav_*wgnaL0PAiHE-^ zoyL9?3|z=qvK69%ucf2Y2w!rtv$cJC+z%3Rv^q{JiHUlyu9r<)-liWiKkR^qd?}J6 z?DX+6n+m+BLgDCqcevNHzhF1#l>YA8Q~caZ2H8ljqAmjF;3`hM?eo-IZN3>-y)F3R zJ)JYUyy3z64nipNulf0XzfwmI&Q8ARpdX#fiq!jG9`B#4owKncUIa@sN5rWjV`0tR z-OAJ9mX2!c7^Eks8yFj7f<&NmnmU~in-6i%?Ysg4^X;?>`5ND(s1p?k6!KL{XAaz% z6BXE#hb&x+HJRh(86!6ZDmprzy1&8bb3e?NHZ|Q&sXoEF6=4Q(cvVZs5HV$wB=89` zB2y$PCMG5dhE}mAZbayoK}qAH%5<} z5z-ZEbqlUchpdyfF)yllr$WLiMt=2_G!En>FbZV2$G6$bIKz z%2S?;j(q7cva|y(_76kpc8#r0HYHY;Q@U3Ez z+EZ|)E35d6dK01^yM>B|+V1iye@3SF^Tv!3g~JfuyxOwv@}NH$_t#&BKz`j1F6U^_ z(bD2#oTtnF-u{Bo$E88_+)Lkfh5Q$wBui!bkCKvI@cP52R^R$6ogI^)a)Vkvhm*Bs z;Ud}G)cfUHzI5esZ985P;M6pk=`@OIaS6VvCk(ki>{ZVZvKAE=BY?oWUMI_fTh($s zJq9XJfdKerkWQs1A{}7jR7ca^`Hm0={4l0Y#*5q(1aIq_K1429aHlHC=2n-Kq!l=f z9eWmU^%qp?cEQL%WP<*G+r3tC*db>)w4Vz{ZA35Pkiugk_7C=x6B7fjwwdMLDg+zy ze7<~-CB7`)5d<87VfJsv$X)L+OX9F<{j$%sLuNM3j#6-jXF<=)A~jK|!QKxE6#lmx z^Yg@+Dh#1vWDpcOT@q4d`h+3-iptW`vLbX0JHN8sZ)ikG`Gsib{7_iPYb@|hH`-9h z&zCP=gv#oLnWAH006~a}|I^(Vxp1ge!_1n0LPmx=G~lM5{S176qD;=OojYL*?2~q` zMsZ&9su_5TRom{m*-cU0_~+f62}nX+v)`%n(PM8s<=3y+O6q}gj?P;%J)6|60lzzvG3lB#|M%QG-*NFIq1TNSgj*QF{_N>rv*6@z}&3Led zC7$Mce@q^2yBFEh`mBTKeEjHzv9}ppsABmB1UKO+Gl8syngl4*SbU}w^+NqrHu%rW zkafG~voL@A=(mg|%cgDTW7!<8@T38$6d_eypWITa%J;a)Pg9iI|%Vm(*YcmOLT z@M>EpF)>j}2@jV9em@Ai(iwq_l&tPgmt)62v`Ic6)%sr~U9 z1COCeovYora_;ck+*nNf)6)~JTnsML&){6S*bITd?uj$ctLaj^FFsW-o?;X{dBn;fn{|h9)Rq2=Od_pa}Zt9d(&s{i5L1kl?GKg5ooqjFM zT=p!uG=Ar4q;6~+_~}_};~iZ*>%)Lu$ai;Mh%KC!=QX;$Z8TS^5-GnucB7bsg1i#3 zuw&WT&W`T2^Xvv8X%(x1vG$JlO<`Vvv3Ryl&Vz zPb(Lj>U_9W`REn&W~B?f8#(q;qN!t)VNfDK07Oi(1dclWFa7IIOjW9Td4iM+fR6Jb z?iz-@j%Y%Ar*oR^^eFN~XxZ_4Fm|hLZDlY0ner&weOEFn0QAuH1`|E-IxVB*3LPb#BF~tfAmW zSoG7zNx^Q$fFNE3y*9_(jO-qOXn*ZeF3irl<2ryShF~s1AgR|3`Lkg7B`5s$#<4%n;v5`miFNuzmm4J}q$S;r*6gTFB?jwB!^x1_$l-Yn2Wu`Vmlc*q8{*6Nq4U%CuQzbN+>}mU^>xim$NEV3&?AA}L2^d|$If-L$>4C#?M!q2{No zG&C7D9nt_`nbXYzYj2UJiAQ|(O7LrRcX#)zpy$>u;T|@TUHN`Q5MZBAlp|564+%zX zk7}#+D zKu|!#GF6G5>wCRYioY8K>CN4l3xDWK&R3QWyFX`&13*;5omJ%~s64;Sy3Lm0`3!&u zgR@3U3$K5e^qbUcv1@EjIqQJAN;ACzw3UO22}gh5|M5YSIavx@ig5tM6=o_OaVHQ9 zfMnz2fZ~mOb~85zLRLWY38V50T3TvyF7_sJ$+aU4Mg5z`4gD5oTc=;FxwzHyAu1}8ys}=%VHf4y5-u@uijd-!$;YS05`@tS8 zEDG+wi-#5DDZj`F2?=p=$;fbN5{3+!)_{Z@-1YUn{+O&&oPK$dn)0`FrT*9ub!uqo zkVQ#lC4agSP~8A#ru;c-MnlKIK;78o`e*?YcfyZGyz`HX7y`n{+n}@18o^OE-pb>@ zKB?8FT%ZN2#0D-D#am~>Upk;Vu47RV<}oC0jJy0@giV8o6Mq{q&}H2I-1Rj3KN+}E z);Y?7u>&HIDF40>hM5%aPAq6U?a-)f z^myDTB|%%}Uu(0~Kbp?B?=D1XVj?as-fp}mE+z(GL$~2LVq#*V1c^YbFKo<$uW<8K z9i)4<*qy+jBCx2Jud%{|b znP;`}_s<^|loKSB00u`E^rWbuuxfZ27FAyHD^JisSO)*3U@+STKHl~IS+}W*O1yKF zo{J*bRe!u~i#h`J+op*Vkc`!@1wFS9%}*;Z99jR1-_1X`aDk3i>knbukb0pNw#F6s zpm|*1zjYWEcek&jGV78ZRB=2WYp1nrZwk)7w8Xw5CRFF zzt{QZd{dJZ*k`8&g@5k~KC82&vP7PI(|renWd%*-ll+K+d}=T*!3PnC3A)~%*`X1F zh5fW?;v0N&@MRDXBGtwh*gVsL^t3H$Nc0iiGf~?0oDgOf;PAaLmhyF~+?@Tg@Z;pw z`hL4`hvT&TAspJouG#ALRNe?E;0o?zhIi95rXnJ|JLwK$VX_m*0HS300${ zt$Snoc99Fnj+zZCw0#x>7wC;?LMkdr7o>hjaZ7RNpY<9F3C+f+XrwCb93GCrgXk0X zYqufEg&bP~v9`t*hV%)=nzs2G8(W8^Sm@}}haN(i3i*=*B9fWUEjPNqv8p?5zp_wBAp=?WY{Vvm$xC51hW`dt@~fTYg_s!JT7$Kn2DwGOU~Baz+WN1yXWu5@y}uJ2)QoC0T}G3f5w|__8irU>Wq0n>%WT+5jR_jT}eM9j_QWBDq(h>lHzj~* zo1o0LOzNWr+wNu1RSMGcZIdqt=8mFO|Bnl>{KDCP8+uPk?w(n-duFwo;dhy;8e19Q zbMU!PG@t9H{i$s#nh`M4d)&P>tdJVA0hH&PzmQZ{ns|BN9>Z5SE@W2q`Y!dVI!% zHo0Si05k=D=)|hcg0PMt_5QuMK{L0HT8%EHj)K(P?Z4LB+aF88^Lchk6~Jr~CcMwq zyP`8YGle|3#W~ihU)(zhdyR+RyV^D4OiTn}N zRM6skm{fj$a|#_At0*hN?hv+4Gk(}H_?Pnp?)`*V;>(0azNKRbxaq+f@AIRMm(R#@ z`1g@ngIH{}%DK+x3AwFG@-<16)%%j`tE+dsm9G{dZsVTc{Ks=4bMzJWm$1|8Om?>N zS6UL!b|+0vv+qBC|7PUB7(pH|HO^7%h6`?^&5sf7Tn%We!BH36_^uIzu0&+1cb+0>P7W^jW3E4v3{e)DAgk+Qx^I1 zkZ1&wcybJRnfIDGDS9&M-WgC&^7E{if^Aoy8>Lf54i0hO%OdAbsnQqloH-yr^A zz${T>b8hdX|H~m78axNCO~m2rA&ZOpAS(Q;{m$pdJ{=Q}r35>9>+r4m{E2SYjJ+OW z6lx%@U*N$5%NI96Cg3PZVl8F0NpuTF_m>_k59u&%-_B4qST^T7DEB zR;sF=GDSo%4bUPNwR!Ye8a9w=S#s;8zd!cliQOW@i;8DirmU7!*VOD}bP{Qrzfx~o z&YUO`fnr_7Kl4)a>L2d^m?A``mh9^4wo90>z7oJF0Wfw{GKbIV6B9!#0!Yc&K#1gh z8gTP3V^)5=0;Q(vWqJ-&v9iqGrVtlWmjJ-c68QjbF)x##&4&1Mwk-wvsEeVoF&M_D zkUxA5u5?fHJ85V`BO)19R>W91z9w~Q`EYO^Y}0t98cuXV2898|I8iN(P&F)Eq2zCO zW!k0nNVLr0-D6Kbb93{6fc6FZj%Y%+#s&M8Dv-CyW+0~m@*5EgM(pk=wBW0mmvnTa z5lX_3aW9ZH)V99jE6N^VEt71}!iH1!Zf^@dGy+j@Ma3BDck5&SF`wPCQ-#wJ;zIz= zJzneVEW0bN17gV0(-*t5l0c-1CYx)n+fW>ro0peYu^LnF+QKRzARv;%v?dINg~1g` zkgp`+!G8)6Gp??#zyrDO-qpwMVkDVvZr%Io{$|*O-+bRWLM0kA+3QN93T=7Q!wJNx z7Tj;pUju08pzNID{IO?LpBX1H5U2rEmr-@50(_4UIS0=7-_Zk)ujcezk{fA5_mf9t zn}m8z6ymYz2nv46+T=+NS3n&+7k+nz)Nu2lQ1hi9Uk)W~^~b;#v)OBOSz`sDg6GO! zll$%(5z^khZb~uslKC1i5f06`0-I{tp=gy4rbl(sIf%hElarAb7ez&k>Ix^pn#>+= zt2cdC$L7fkUxT{3x&V|TpNtC0k%gxbf?KY6TPo9 z99{%eIGsA+BtS)~w9`m*YeG(I)u|AD8cFEu%aAUKn_h#q?Ch=y<*8N80f~U$=Jl70 zX(ZLN5g4eLk+Bg>92`iXga!Xk&rk8WvJo1tNC3q6u5w-$S)EyHtnldY5O5&OBa69= zwmquiqG#hF3i;{pGY};5HPnA01|xtXq&2j)!9T{$&rLe_ti^+ZOff&2n{RQD@{4-+ z`TxObnMBT9{|c^9Ohz$B0mAqnHhD7r>0q`6bbimC=04tz`1xpL$tS|}(e4q{`(|6he zGx4vn`d2YYbv&494Mb%|ZcYy+SXx?k+ePL`Nl9e{0f3uh3TM|u;E&|=*6m~y5gV28 z=y2!W8t&AiKOK)otP&$h=s;aet^rYMP>wP%B{P%F><*~hOIVxW6aMNTqg6)&?>g`q z0J`HOfJ4oXCYy88^=VoQz$`_LBX|7NY`24a3Z`|(!EI^r zu%mESk;3WoQ@fDBn4gz-B>nzqRs8wL_ROkU*Kj$%tn8@gW{p36v|AkI4^Zc9=u>Il z{;celRMFqxe?!<=djoAZDv7HN>P0$|-}l6S*g?@KEx95+~kpkm+88KnZg1Dv<+aWS5`B^-nF(H#o11Ayz{yCks$ zkypi3Yi(mp97dw>J!{Mq{)Sc6XQl^usm{j+*oogsL`1}dbv;mC+4TW$=_XkMFXGmf z)p=_!5)1(C;9ql%G8gMO^!N9tSLrxUR!#>6W7h<(#M##Gbljh!p`y}+*6?^Lnwy7o zsp)IDUff2M8#DlRJb21I2eu3Cyz3|Bp$O2doU==(1p@~#7WDLrGShKzJo_Bw{OJh+ z;R^x~T*Z#up+odZNTItKCY?_MTXJ!N3TP-OJ{QG>i>0;)P1SZC&yw@EnT@LrQ`dH~ zakJLbbt@`M_MWXP7Tf@X0r1;i`>1aj7~)7gMo)}dLxTv32t?24M~3)Uy;rX0WMr>C zmd*e!8;q0NWz9*VB%9D>opOIvJN6AYE_Uadln|bP6oozk#S7r{ z>FDTC3j4ZW@vi~THdwvEltGUk5ij-GZ7ODL*B;j&ce4UiycvK465%uNA8lNAg*Y?< zGS8yL9GT+{-qnrW)=dI1avYU4Yu-ishQrEEvA36IDs}7Y>l?IT$@oNQNRNl(*?mqO zcY7nL@ZVI$n!}vY`4eWmB+SDQXog^|OAi#_QjiS@Ix3ueTh=lu^Zg6dH>BRbQ$ZPA zevjPju{b?bs)H6{hOmCN(c`6*>3dJ^m6ul#CKt+r53tOXm&&!HCV;#PT{d^fwq8Wk z&(n#t`5U(F7gJ)>KbERGy#}Gun%%mSj$`V!_4@Z`-;Q6?Am)0;E@^4#&N8Y%%M9t! z4t!m`5vZkE67PoaahU66nV*n<0*g*IdAx6R;_wuCBolHz8~+l*-Ml;?$B-~A04#lW zjMEb^iTd-=yFN*l2}l^Ow8XE1?@v0No>$wY?B2+zKR;eC9WOP=$jYF6EIv3qu(h=f z4JBeDgyA zHyG9OHS$G;b*7%mKPZ#8`fSlte-wh$oVLSa^~-1PZgtrRH*hj`?^*%md*x>BaB~0t z?k<1y(ctsvf@Yru_b}52$Aub)Lv3oAB54{IkvP=a(lSB_z*z9;zz3i#fFT3WHAKAE zC16Z=QsGkBKZYzspn9y@VWvJ{H^cYfT_mhG7yao@4R6eqQv_TVj~k!cOha9B^2z|9pW@jeY`Q&(Yu*SpPA4}SLRzK%2VvNs5L$jHAvzjS- zXp`-ef-N<5x%kd*w%kY&Cr6_cg<6W7#QJj@iF@$Mfj)|ZXir!c%$(T^>AYZd3G zC$!8Yt-xjS=XYsjSxfx-);jC+b5?cjus39co0ZzFjZPmUrPuYqor|jCJf7<_fxZfD zH#ySGnRmBab)(l4NR&cBdB?m)A6Fdbm_gW5BlNX?NXj3S1HAh$zQ*ODuk2Az`vOm8wAmj4;Wy*3H81?u z(C{nd6DRNE^(GSxK($#3cRj)~f=DSTpMm<1si~=h_J?e>hr<{dEv>{>uVYWlZhLKm z#QNOJ46=cRy`(ig92}g}fZMK&!YmuO5N)qDFIdDSXS*06v7z^%6LV7Wv7gKxFOfq; zlamuOg0SYLf^ZIoD%<8H)Kh|VXI}8l6UuPZ6_+Qy}^y6qw})0UDMj_~I9at}ZJ*yoOHwu-dbZ?uX<+VT;@fBnG2sE3j-q!(Rf zlHj;GsOkQ0RTQdc&G^A!H5z%i7POL9Z#9;|&c?LuvnojD@>h`#KOaCr-G$0k3a4yl z()eiXrR1fhL$P1L@9vm>Ik0{O#LRQG4KO|)c5|6x9>ccvLYMBgZZ+YDx>gTkzwP)vmHUorrNtP%w z@k!Mrg);{BN)5nRJ&Fh)sZO^BvN; zq!?GmMcb$05t2=+*SdIn-?n7U2Ek_Lg2;t5)zm&&TRS_x_I#V6Q91*V&6w*KZL5X8 z0;Oih6kaZI{z@Qr&CbpS;IZoJ>V1p!sC5MISlUp;?}!6^SCssL%h7TaGk5O1rAE0E zWpMbRhuy6GYTf>F^HIsL!V3wq#)9UVAG+du26-GDh4Z%{t2xs(!wPvwqtbM?0 zFbiA6*KmR{C#I41!#QQJ(q~#wM3-dOXsuQ+4fR{e-TTJduJ`^gWmsnccfGr-qpi8K zyPcSr#HUZf6!{ZEAe1=^g_=<2&F#Ab@J2bX$;>g@ho`4&@HmzMmu4b%MwK@Y&^S<1 zvO~KxGc#%DwtT$PiI)VZa4*a@kz<(s%Lk@wkO&QM*35 z6S^iQPJF4MAA#(?tfT~x{CR7VVC#2j<7ZVse$Z}ANrAI*ir?y00>mdxMMY(0BH|p4 zH93=G^Gs5|6cV)y(U8xukf-hHNfQ6zURnzIV*>+obB*l`S1pT+ZEbCU$^axw!cr~v zgja~KWv`+DXy@Vv3mFsTy1kC5BPS=PspubG%E6&pIk$H6=Vfda)V4k;DXC`EB%Z6f z+A&XTL!bi4#BzYa+S)ife-|`_S?V@Ro$T%skq{Ql9liq!{G|jZnK0Tn11>-m9YfE9 zz3wAMLnD=Y{NOgqsRX2<<7ylULjdgT_o0O_lwGcyMnXq3#M&=Z(t6dqqkh4DY{hQS zA?H;t??A!v!ky*Vn?|I(TSkHp)LpKpyx%!RZifGq7KUCYYz^`O+}40(PN_?7eUVyH z`M0=0mg9(5T*auFMH#jvDjhZQ_m{=$51ezMtTBEExfGS%e0h9OUZ}q3({x%6j4V(9 zV1-|)cYpFV9X3~;%O}E-jgJ%_MXrsElpp!r{Wti}_7=m?G8}|h^h=T)a{^{a3n^nC zY}hhD0`6>>yp8_ZOC5cT95l^X6&CtUyN%A`OoEMX|r!Gj#zWuDR4$|XWP*mWmZ(k&sZiQ@bNN(9PTg0~MN z!vO3?rq6^D|70fjAB}gVQYJ@eI!&4ehl`w>FDg>;0b7kN@zL$?yl}2rSN#(l6eA?R zyMpN(yL%dQ0Jz29xal(gR!B`Y5FtiVql(MF!T36N^FqNd&yeQMbJ78dPF!|!w2D-s z325nRQAo(wpCV+LXMmEfRAavU$Cx(~h^^HxCyr>5TGraEN;fuKJs{JOf9n)S@oace zg}EpfkU0F*oXyO&|cQCrggCd?*v+O~2sq`{+uEfo$36bnVZ zP0N+p^dO!wmaHInO-ujsWTKOMIFgK()ju)vNzylFIVyhd9YPmR4j$6|>rd&x^_c9A zzo;g=Qy2gRi(&N$pDd3>s1hj`^yek9_0qx7)brr74s<>sF|v1^Du0d;KBZu(+DE61 z88Pk7)r0?U0T9!-2d`6=3MQ<3nMdvU%+u?dT4;$0&i-VTl?U#gbqT#m5j-!<#sLL2 zyH-^`e{>mooTlM$jstkxSFg280TS4?I~zL>po_(tksA0=m~1Z2Ky+m3{a5ubnOiEn z(j-702#jUHHf=3{w`~c5fN)+PQ2!t<3+?M@4L>&R^ z)9$=&`e^U?=dj`$y-3y_lJuE=Zn86cY6S(7JWbi+x-Y2t`ZNTa>P#{lB0j)i0TlGE z7l>oV2amoob9@LtKK^u0B#~H|^7zM}_a#an4MX^`|iH!ypTIwC{_>Uwce>d82-L(X6;!fmwS)wftAttmIlX) z!-eeoO`l(jXO?FkuyfRrOWH&p4O)wsSf#VCr#7xiFRX=5d#;fBINB7C`rU-5c2!AJ z8GFA1-xwuNCg>0dZ*W_|Lzz92C*?ah-+nVQJRcewiu|hQG6%9%%_FSyWcgysDeH9a z)x=)x8rtBZ;e(F#0BjMiK$EYSBlb?Qu2mm0~;ilG`L>WYSoca z<;^$a5>7swi5Tx_box>^mcwo+!QOrlPp;#$R?4_Uh`C9dUmX$FYHHfZ2dsZ4?iFkfloi;`A_`YZhCXfbFLl&CX}C7a6E0RE>zx(*F9Fe)RObHD3DZJ zC-RbBIzdkVy&sWF8`>L|u-8WyFE5?(C-kLk`*7wbUe7T%tl=+DfwQFs?F&MSXfsDC zUR%)^eZMnr_albiLw1xDFx3PDi&4W#1J@4IwY?M{EO@6u;i3Yh%}VU5Dwuja3h!Qn z${HwPzAa+N*$~QDLW1P*(VSqmT;rXw@|yeAry(q?;cPfeuCmB^x@s#6E1xLD|Do`E z!|0o`1AnCH#vz)3Nq%}^&QxsGHm+(4zEgV>o(yseEI@51U~#aCs;@6c zhmxyq^)K@PA)Zy?Y1=24<_np_HPy=3@__;SA3qq`#2r24WsZr^i6Z@RHHAGyC|l)3 zr3l3iHi0k>%4qIg?I^yky~sR4-L?c9PC$~mU%Y#WAB~!``@MR zPbIj=et2|t`KkkZR731j&IlON<5v!A<$C-Hb0%BbcWv?z%(S(t6!;+)VDWMG%Z?k- zcW&v41^Q!XCDD#dKAHWSLH*%_NuJ29K-)AXkD2R>@b@j^)AhFw}Vw)n8I#q{`k#7(+R!^i@NJEh71 zapgFu2=YXZ)$fMTc;drSa`ElRamSQ{{MVRW1KWVg@TP&*F6I84>vOo)wmf>^bQ z!eT$SvaXa=Q&NRkhtEWL1fDpa)z1#W0dx>HBf&9C<&OKpZ*dHp8G8yjTzt*1WUi$4 zjVJ8N{AWgt-+1z!I8>QF4Kx1yO{EVOU;WhT;k9p-ud+mJyZfjC{;BBHAer)2K}}7J zHCbJb(gFKW6I;qCcr<4d*Xx*$jPt;Og4aw=+2p#f+?CN7z9%k>B^RZ=CQNh8pWwge zmOjQ$qF7T4G`CWv?fbQ?qI_7=s;AXQcWhyEBTrAy>uZ@Fgip6Q#|vLQc+8gA7Jr;i zHnnQ+OvHNywsbCQCLS`vr_x`fH5M7@tkq$t zZ%TB~%2aqwpy2VhVesctZjIBaj<6oT-0MHJXl@7O`D8+$^%dS*{HVB-P}lO3%^)u$ z_D;>GkOeLS>JcUz;t~O#c+9vvN!{bvu}@|A13dz-CQwWf)L@hM&&663`RyiGc_xU1+u(O6(kQwLCLDGgUL-C z-K>(L1E9m-{H((bs{)q%3rllJMBGLz6$hHsKe4Pme|n}jADTuG+P&Tk9Y|(N7p%yd zf@+3XJ#T#wlziNlCJ}}t@uXw~@Iuv+t5~BPoHlW*_3$}~uf(v@ zC36ab9)`RH@jC;a56MGwuZ_f6i(jWa^sg}>HNf*(uc~zhK;z;T7A+q`0>s<=NT}}0 zRd?%ze|!{Zdl*V>tQTp4zDr=0H<0uab=6Pl5IBf=n z)6Ia)jRRQgw`bz%lrsfaV{i>@4_g{Opu|pd{f`Sk$YU)4A`?hF9Wz~S%Rq5Ev2od{ zDeYH!4TAgI_{`Zl|Nh+~xF6uMPU&%zT-_%@FAH}dE+=5hm+XNnP)J}NDvD4JYPPAlTl}>dP zebn^oSD=zZ;jFrBm}+gLrBz4 z%@EW3#n`^1{^6SO{?#hiPFM7k*uX8k)$w1g$t_}% z@4yl+iPdWbn+r&XWA8R~*rY^5Oo{(n_5O)Z2TSg?`kV(Z$Dj;KZO5&})q3F#YsypC zuiVN8@z%meijx#cbC3wKCY4wYEKs9ajBl|sbL!Z6Hkyj}gjv+SSnO=~Lr(_coZGP9 zd7jbk@V1MAyFBS*%R4{g&c}*AztZ$ce#lkz#M4A>SoiAI36dj%>y!-?A z==7kgFCkK0^f)%8sPsVJ&Rg?5&S?(|zh7N*Dak8{-*wPxURX#q5hFjf8EW5j^(De` zSYuUr=+xp4S`$pjv2~g{4Nr>6x$P_={pgwKREGN~fSet>UZR)-{AUHgkzdfLu!OVj z7ZXs%CHH*UrEp>)!$6686`ZjaM_AwN_0i0afH<0_ZFF&m;1D#y zA-KD{d+^}F-Q9yESa5fD_u%dXcXxL^L+T1VXdYDDJMYj~a znr04(bEWDdqI4oS0iU;0iz3xF*Qs)JXlx?_j-^?|ver`gc&B14!EsS)T7PnMDR?!3sth%}g$gr__tJU%E*LPD^>3}7qMAtU`=1XmW>RW5e^!eo8ZimmZ%603g z+!2+A)T@lo3(TiQ_%@$Te+l1dg#48Z??Fx8_?T{sy>Z>0!=9o)T39w79Pwl+s@&kv(ohvze39$&2lF`xDA4Vx5(fzXm>p+w!l7RvPQ2M zh6900aWz;Ae#(`VMV$D=A%aM0+}Bt|3eUO(`tfZ4>>G=ZCXnI zAp{Q{Y}u^g6wQi~k^@&Cl$o%y?&Bzi+g?KlT@#DGyfXLv0hD3a78*MGmbH~nFTTc9 zYJPTtTzJRVTD|T2+LKWWiXkPvH>X|^S_~TrX}G`8y{uL_3gW*S-@26}jVL-3&TJueUmh2y|#L^L69v;{%7eTp?I|wI-PZVvLZnrgW zylhxmdM=e3fgYb)3_b&uMkm>AsKPLdUA5(0$xMkOj)bHvXYIH760!94cViX9w~!6I zyhlT?VOfZZsW&v4LBBWhg2l6f?4F;Zq5Rm7#yI>@h2F9s*u{*-7ul2KyyeVvuR4#BpcK(ADWu;`D7ni0#v}# z2k_~^q8M>`*WGD?y&9k7Rci6V@!H5+@5gfmUX{I_?cWPR&)km)ZdLoXl%v4IR1Al~ zGB0!a6(&8}r33%Y+Af-3$YTqmnWVCZQiA-mw zf*B7c!2tF3vj3wiNWa4j{HzR@b z`O-Fo9A1B4M_5rB+fOkBW*{u&fH3!NS(q<{(`)AyvIPy}lHZy4i7uCCY3b!)=aQuy z?BNfuJKG7;r05${b@pO0oE-xnacT2l_{RI<>}IYxI!53g>*K{UudzmilSqgK zz0NOKli!U7JGlj<`2-X6^IA7*=JIn;PM`O_*%^S9pxl%-&sG#1`F}uz>1H5T_Mj4q zN&(_&G*{wSkGPCjJ~9%XKqo4nhZ%D`?16!B(lF+xSq0OoGgvA?44OH|2SC`7NB{!8 zTYyR{h7Zs9R^sHTU*|sNkoWfVTsw^Kpo?Bec=Vm~ZR_IPAxOa8g&!beIOMK0ox&4I zl^SP2T+IQV|E`x%^=Yak|CSi9ArD;g1^oB&v`t^~=tFwO%E(lfTvc)5Z%4ALK+ZMW7id zvDv@X{3B?)O8&Ld$@^)zA^Wud%7bQvgnC3E8WI=Gmn*qs7Yt+oQKu#Tv_2|SIR1z3 zn+=fo{BB|6%~;ujE9;q3akf6Rps0JcA1TNc!GQ$u(rrg?DNXvhuN(wIBU&S2(G`43!nnSae`#;hqCw5C*%(Nd~*S_)yEB`Sx0mU&;bD=Uv zT=GN)xc|R@e^YS;(7i9in@ERp|5a-LQVbTgDh0qCTX6sHwZZImlB8&MS6rd081_*A zclVo$Hn8Sb2mc2Eu87D81q#f3!!V_HdLMBB**p!V%>kUsTeSKJ=l_VI{@nr)vs*N= z>=l&%xTAFXD8tYij8#grqr(4#7@P6xBf=zlBmk@Y1n&R3DnJo6l-mQY<)DS=|5ZQ) zAX>4<59$1V*Zq$%>d+d11|9-3SR*A@$2>QRyA1?fA`~LlZzxSMIw;P1E+@i6C-(vi)bz-oceTw7%ce99s z+tk>&scP;ADhUtgJDJrCj6|Li5aMt24(|JgitvXHv>~`_n7YtCx*7j zv3t2n^4&P5^Gu7%=dlw?TlwMuL5+2QAG(g(JB=|6-^awKaspkLs1@%V#?#{7FQ!kml7ex50z>ui-|l&}&) z!(`?5IF&9ABWICKt0S#RGBJ`VQ+E%-2l#7uUTb&c54%qFNc$F34%E60URVD8(W)d;3E0s+ej2s~f$oj}njHTG zq8t&)6+4Y{sxr%HB1v)-ZST)F@}v`DNDu6r45DMBBK;=ogPp|(p-7v$PQg*dM3u{5 zyCJ&4K$&JXqgAsn!w&1uZ4BDf66YjJPs?uj#=SuPPekAMme>56tdo`mTO$@FZMwN? zk zfPoGgUS6v31o{I1Vj#qJycz%9tq?qVoy&hQqX(@|J#Zh?Af=q5g>7k1%2!f^y}x(# zz1MsZ#FoN|e4g#PgOQ$YPXbU*ml|w<&-}0Vh8W=Jv$)3Uw15+C7NjB^UY>D;i#QUZN4I7s_y+o zogy&5BE&*~*o^ZV>3Q6rKL!Dgn{*xxVLv!r+?_jn>S{rYr2U`ibT#b{5D6~)j}45_ zqKb~su{TOtkBZLG$^gpV*84@){5fJ9n;~zF$71xb0}&?k;L42Te3R_`4Lw1*xunOD zVqm&bK8(@6V};+f6uoJcvqs|YHuS?KH+H0`gssBi!9eQ?>ejij_epo&)QvQ5`>`bl zAE(vHzqw$%a|GE56in-zvcT8U;`FlSoJePL=IAmd6k+bD(_TFNO3qwVbTX2;hp9QB z1SO2E^}VF-FcC*m-W}; zX-E*s1ZtJX-D%|<5JO4*IjIVHFP+!J;P*dSM-;nyzgo*=;FGLR}L zqY8tEH?uj}1Ya>O%LV!p_0RvRh7EB&EZXEQ<*NGHyPbG=3sIJ~nS~%#?kFfWb*< z6o0m0$%yfc!ikYSf_Z4_iAERqHa;>kG7^#;i@EX@fIenqLjf+N&vcvTC0aGU+qvSKOu;v$%eBO@aPuICKg+fexuoZ*jQTA9q0ePfDo0N0^B7M~**x?B)JV#{nYts=ll6q{HBQU= z2}Nim|7v(gQr+${d&ciOZwJ=1A4rKC6B9Ju?=r6?EG}c#Ukh}+PP3z=yu3WX9`m(d z1$eV#RQ|IUv9sq;PU#2 zYKiuooq&y^N7v^6z^FZ%7C}`EOZlxKT}!0eCs}L zAL-TZw^08{UTty;sW<*n;=@R5l(RNxKK832*nq59mVtANLefY5BPnL}2@oygatI>r z*dX<|mjH2r4aT(YY#*yV$&c#6q2&5%u+1EqDclN~?n`L2THDAM-N71~vW|EUNRm^6 z54`nqp`X%9tfvlQV0h|s_*ip_4ui5*kXVHGe4P9i#6yb^cyGr}Mw&j!+q{WR1fLC? z1~cs$AxQXaRQN%u&@Dt|)sar0`vB4{A(r%TR7E&n|e?G;#|3<+=NrNAaA7;T*9O3|WS#>F2)V!alqFSz@F&+8tj(u8%d5iz5JNR~d3bV$&QfDD1CT1Ul ze4HH1j1a|_F}_zq)!{JQlS1s{IG5Tr+TbObk!)u^_9d6w{dh~h;Jt^bYaJSA&kU9)Ixpdr$^ac zjp&xUFQFm!mv2O8VDUxlOdC@UT6$hU^zK;XLL{qVb(8bo{nDhFl%=k?6K#%7zDbG} zAC%vS(6~_Ft=G|3Ax?F26}}4eqik<|QJCfpmK`{9*!(l;5G77GEBuQ=O`rSv@S-LT zonP@+=b*E8(@(wnu`%4cNyyjQue@ROsJ=3@hR4UIPwiFT4Uta5%EhpeXRI}^Cj%R> z8KrJ;xStmAvWRWY`WKWZk}AndS8Dg`RwqPo84PcR@Sw5`>kPj@7s)3+V2BZ$&i*+3 z8by#9g37d8vZS{9qfx_oq>GPBVtP1Cnbir^%cEXG)L1vNswU%PSgT~V>GpmroSf>1 zI-|r;tYV%4%YKs3xXh}``&>BA5yNMHZ`-vI(MHB`YG`*Yc4VgGFX{ymL%i$Ve)U)7 zkOK-+sCig@q!}$YXgEPU=CIWuA4@2A$qse7)VTs6IImz|;H;m9FDWf8IyyPo{bAme zfH`!WZ>irsr|tE=kJ*q83gjzolg`=SdK7R1Yg4<{Jxp{e|8_N%)ezZn7$V%yY2!T5 z#cqlu2|TzOySqrYbbpa>1M)p&9e?q7UhJ53N^@UNDxOGh}1jP9oQfXk) zdWu0}Sac727+c>9z)xL6)U0gZ3)f1;oDM8Xne6D9soYzPO@3}IdncAOaGfcXv;q^Mck{x|{J+9yK!?lynEb zCY-rbVm-8Vdo68M<^Mwgjr)`*RnF(;=+m9sVq@~$C=sp^qRlHnjRyEA7j@{(ljdLJ zGxUGG9NVEZnx>*U^+$Dj#r~}gX+1WLY_Z##S=GLLTkL;bDXfM_3xISIYctAPF)k@A z)+Eu^cw1_%Gv%B+-g=@pjl3#3crX$NY%>vgI$X5?#8qNHKDOVPZPm$p?a|@rVr2r5 zset`N-$@oTCd3~J7+am&Ek;7cjFayp@~&MCC&dESDY?8aG48E!P~biqPD%uvLO686 z@Io%3Q8wQ4T(Ii*)b@H#;UqC4Jnau8>gvPypswPTIhE&i1=6Y_D{aT3PSe$n|F&@1 z?oiL&cY-9oE;2Jq!Yy*olph?Bk(O50e7hb_7)NA9eEU|NGB`cnNR9Rv)_-DGG_xGL zocQsD)n!RuhEGQ`B#%=V>oil#a6yqpq)+xVkTjQhs4*Kk+|}5NbcSp0v=uDQ>Wq&; zoXugzbWzjA$U_(@u_}N|D|AS7kUI0zUi(9Csylw#0<`bLYz3!RXUnsj;iLiJ@Yh+ zB8Cl0)&PudBbCTXu)h#^>HlOc{b@JWo_etdTzZ7<$GY&vyE!@S*I)mr{Uh6@m{1GU z$^vIsmAeBH`9Vcv4}+3@ix@mqq3#hTU*#M3i&)BTH%xs8_fbr`(x}KY8YT0u_irFsfoN;PY5C2iLes!STp+L zcvr}AD_ZYBs^>^7Sxe%#A_x$O!tLk3doK(T69R|fCNsBkt>VxDi~VZZzptf|BGFMH z1@~byrKJW(MviC8@Y+VbrKE<8A)s}f%-1Fn0F~zba5)}{Fnukte>qv?z&1C{={|?x z(&5N;tP}RUo2;0$M!+}q0R(7pG89|0$9J9Xt;_QW=M3j{6VsI3s$JCZkUm*=o=s5( zsrQ96mHDtX<(RG}2X=8I-C11L-sW3OR{uPd&9t-?tUqNgmH-^{?2$foy;MjTVIHBO z-b#J-8D1D`G|1PhG%y_{L@KxU7d*TOxONSxEEe-k45Z+Pp7YDKw;=g~RGN*U@6tru zVc!<)c}475QdJ?DaH}lGq=?y|ho{50Op$(*_b#l#T0R-Jr*Ke(>w1r??J$?TB#lcd z$_LS+^J441ew(l0s)aLl@<&1ggBSj6F6+$~^BKzN|6&1xVgdh*Oc?9V2p%IV`MYxf zq}cA-OxeYUTMyR<9ir?#Hun(_?9+axEImCkZyEH+SJ?_UbrrNcHsz|J@vir=eC&B0 z*KZa7q|94GN+zze=#qKgBd2szTvr^r!I(8}IEL>(gw^3_{v2zz1dCZ5?Is zIMS3=X0GxwK@@qt{=Gf@=ZuNDg_sj*X|^uk!GkV|D8kO^$qBeTkdjy^`T^ezk=>^0 zwKvIp>Y60uiet|@V{s`eI$H39O~ibDt*<1K7m=sojw=1Ctp+;)gScAEQ&Y;0*NLjf z5_SMdo3}M*yk>1jWx77NUS!HFk1D&uTjydAJdl`6Eigdgy9OZ(5usP??`FN}gU;I_ z@*y(}3gB;d8Pw<)0g*fETS*Va!E^FF|{r zy?Y)_%}%%a4I@@~`#;S7tw%b|&L=}{iNA?IoPGUVtJww#j2RB1IT<`U_dM=9nn-5O zPKy@DfSg!GrSES5Yp%Y6&`}q*<}Q6SN|4YZd`}N}`M;_olIsQvWzD`{YGY56M>pCW{A&N7UXAK-%y4-YHf z&c5#Jc*r>2CSlJ((>8O{&h8*U0HkR!X`6>E22Y10%Tmew53H0aRR9jiSdjgSCfI;o>`SlSH9-$^&0ofqLgcM+WC*if>EvHz?m(PdIsGp0`(#zk(I zD@s{R%!K$@y=Ct`?Im*O!N-An#`cJz4gnH~^%$IBv!v@(oTwdaGotjMyG3@wkNv?> z>F-*S1l5U%%r3odfh0&||3Svfb%r<}BPydM=>`_Wr+!1>#2D1s%G^M|nM6SvaVaS%SCtsbn4Yu}RiCUYB2^|5Be0v`_fg}0Bk4Ac*3yxt zEm`!_=J@){vo&OAzqouWe-;9TBBwcZ+1|7DwAV}RhQ`cI*ps8bk zK+V@3yN7zkC>hX>5@Dh^$!OcRd5ScTwK82q@k4t4d_%##smBY1D4y3OGhB|w?=tPd za~_>ACzChih@dyT46|n9IoV6PGafI?8lhcfF+O#K#6qHe4CDJNOqbOuLP>?NZkD=l2afVn!niV~cweB{@GLIG2+C zO1}<$rmM%fF2x_lePbU#*(OuL!u7sdt{?gw&`BIB@<Rtqj# zG>U|R+XK>dP+Qr|4<=@mb9O*b0H-I$6vBve`xCdc&Mr7Ywr!{OGrHZH@kpW+4={Ayr=!fS-J5?o8_xo)1eQu ze`C;>0TnsJz+3qWnTs>>AdB+zzQ*hYt{FLNP^X$Zqy6`#)rby9JUTERB&3?W$8wRP zemo&MRKE=`tH$)u1ntJy#gaRuo4)G`dLH)>eKzJ_&VUjfkV09?eS{|<5WZzAaiYOaH?^cdrFw^oP8jkmma7(1^I2`uMhj$1DFrSI_++~PI0M2 z-5OLvaYxR)l_rgULN3NK{w%9ESm;kZ4~|&fv|#^iUa8q`Tw9x;zZc%&J$rfOS_LcB z@O(=V=DU6HY$u}LJHX~|HQ)y&k)$u&2=DO3Zlq5M^XZ-NpS4!&k564VOiQ6Wcmi;o z78A7oMKJH#S%#3_`c=Gk&Al~c|Jl>|LiH!2C(ri7?5s@Sc`XV8H^27lkRgw`mB33Y z;R5!RxwE3*K#>_|&kxFP_cx(W!(f<*fkK}^W2G6c5B^7Ym%)TXk7-b;Ui12>k;{~; z{%i90lr|Ti?pv#&e7#=~isiB1 zBuY{9t|kDs87Gu4R(%Aghve&F0t7+Ndao8vQ*{uJ5bG6R4&7c1t_o@|#!of(o2A!N zSYuHvTjutz(jICn`RTd$HQbhRQ7khaUmLc?8fDwec4uo^bO$AyFI3q*`y5FujMnKl^XmC1tg+bRs zkYUBeAHK{gThiV-+i@S$Zj!6aYl=3^;3|EQqW+psRiu+ia*0%&%8SW%5GY95NhGFA z{9R_b8g>;ok1~wwbyX*6uC2NuiJzVqyxW6TWh*@)xbn^9?KHmQ74n(B$%E`q!;eJ) ziR=p0)TkCTF1l$xhxrCOrYKdEX7$CWbNnwtYA*~i4~F1ry=n1`Dj1JHWJuPcY_^R2 zKzWSDasjqjneQulm!Rk9{(+O5@53(kA8yF5BQo^?xDS$weDDOQ>7`3`wo&`?YS@!h z_i9B$=h4$gmmtr0n~Uq_R_U`f+tQuqhpUJJ4niZ^=dcIt6Wd1-Q%}BCT+9qb2*($c zxGS0Sw$+#m+Z3kkpi$wuG=87gf|IKX+;evfiH+|JyK#!@DBJYI|1!jRv7dy2Y$VUXrE70++WN5C5*|Pjjrdx zuw!S9{qOw~NfT&TLxtiv#S|r^>_MTNEkv7zb4SqiJO|a8Kj>5niYF5xv|)Z#T{g~C z#i>lx=+)ykwpH1bkXl$sM|aC;k}R~qSxGR$dT3l)7PjMa6C*Q!q=hnBRboP-9w+>Y z)NBi8Y&8QI&jz&^i3{_Ea~g-)9%62*3=aOW>C0i&-H~f&gd98df<@2KeC%>fHv_LZ zWuVkIb89S@w}Ckn66>?umXeAnLfP>u`qnZ_sf=?AgGLPdz8`xX%Izxf_zbz|tJ?N> zWQ|3)=bfvOt9}s^I`fx3`;w~RZ#0_VJrub}+SfIC&KZ_)rtg|@-?HIRSEs8_Fe~tx zY>QmB{oHC)(ze}x3p#7eUoM2?qZbCAc`>i&`CK)X7e^>GCH0q$=X~5)KK7KSKhjAc zj)oh!*VoAs^M$RB(%~~vqSobfUFV^z5r#*O4E?s$a4puq-cMIV@c-qrAranu4DrDUVkVw4X<;P z>j}<%20(*-J1ZZv5YN?s5N{3kCcQ(l1nu_-5$LV!QCmadM_sBM7?E*)9#;8|d-8zG z7;?TRI!g$gV5HWFw%x{l>4G<;0cqUiUp_9ov#rree<{O?OBU{4qOrxuDVUWrk8Z+I zz^E~DnS4z3Y^R%_b?p!AuLnAbX}|hhKXpGZ`xit{eNhs5v7EdaCTzcIvtk7W{vx=y z-p$2CFF5f$8%|cJ+ztN;L)7#l1`?uZUo%Z-`51<9SYw^$Xmzupe7xrPjFH`RU3H{4 z;Of*c^xfcmKl;Tiav8Z_KGvq)LGzp5iQ)`rrnU6A3+k2v(}{b zKzIntZ&hPvCO+^4zZ;H`ld^ms#iGMnbjS=YwK%*vP;J?P>Z2oCTaFb4^)v}%3}t>d zI0iJB*@soU8!lmvoRTm#2G#;P!%2u#|4*d{O64l$@lHN+W4-)N@SCdz z3X#auQ*o$vhOGp67E}2BxJ@$S!PRpEn@XtK>>bV%SHPbY)GfraGW%}mNj;fLfO z0`tQfa@pDQN`G6&l+@?%EgA_2i#us@SYQnTaIAXSs+Zw_A3hBFKHnxQiqHCZ!&0O2 zRyn+2LxkMQE7A(ZU#ITfD!2did?Atf=k;t?ym1|_{1CBa6w&BBfG8)7P=NwXyxh*Y zAPmWwxiNX5N;c>kybj(+p`OAfbFD9a!fs#DslITa+mxcOq1U)@w?sI|nHC2l!-Ln? zp2WPn!749}{K2fnPs!e1t*UvO#JA41@b{MY)Z3wyccw>2=){&pSS}651aFwyvIqHSdn?qs6SXXcH&h8N6cM$pui*A>M4H(fTAjZzazeyZMdPfeu6{1qHYh9Bc1$4U*l;(LUf@1nt zd4aQDLebZ1+6&9aS?8MBPJI)eW<0Cxh?qtsVvXorkFI{+MZ1K?(+y^w@!NjCTGCp# zc<@&R`JQX<_uN@8Jbe~N|70a-z?~x|?T4!w3GO>>?ELvfI*rXS&qKwK?IUP zef9t=SdmIjipo}C1&TRjS+UTv{C=6_+_C#(O#2+3fuCVI!nRqwiCE@Fts zwlUa_|B2`PI5lGz#V1hj>)X9y#%kLH-xpO~p|8oyK3*TIEJK`|6Zdn7%T~8vmud&5 zuPHWIMVZMNA1vKE6CyJ1lw4j3qvH=a$2(sO1s1MSOl%LnFV<~-_VUMi2LVK*e)Dw< z>S`lREG=EpqBMI+WeiQp!eouOK4vZzT-(K*_0<=}mhu@uwyAEuo75SB@v?)6tlMGW zokNeXH>M8Dz+RNei=lRM`!MB`f6DAma*=i{45{DZ?dbk-k;;rX_*DHgOO#T(x|y?K!L0kDr&;xt1!-+{((z8VURZ zuz8=(z7tuIrA`jSrG1~jTHHK;3Ng85#r0al=M;W(s<))G2jdlYU;C7=*UtTl?%;&V zbcZ}sGJS<2KiYEKO_5EoxTL5`-~*}oL9LGQQdFb(Z~%yJLmjx-FkyaB0S?ClYc3M7S`h| z<~OOfV+U%_wh?4IDfx!+;*krbfFPdUdD^m>j+viMS)lL@6&wU9 zm1{HErY&>&@3)-hhjfQ{PXiL17dh+%Mu#j*WP9@s)Mf2VBv&ov6)2q3Lw4uAzBMBK z0oM&FwzqQ&Sj8q|*{)wZ9-iU)fK%T#JaU^OlAlh|*BExZ+3{-x%^rvIJssV3Zl5=5 zKPJ%c(-xB*Gnr2&99e57tVuT?ARiS^8MLgnyI`Dr=@Jhe#@6m&Z*H5VZ}Yj)OD4)R z)_Z%btbQrQ$QshIx%~H87UO0;SAxm3%lwucdh1#B z`I}2Z|16FMHi~}%`7rODk8xd(K8-nbeK<1+ta#XQsXbXX$ZjdvR6M2(5s#K57+m9P z_qZMt_m=||iO2k+U>~yeWRmTkxK~}uO=w-afg=vFkl`)hsy0H)l>_c3g+Jq8wbPVq z(QCnWJ;(3&{zegD_@_2IAPyqv;bVZ%*cTE(G!^n8u6W0BkJU=IWGz`jZntGUmo}!v zR_WFTbWrJ&8+MCv%qmNb!Cd%H%bndFsH6=0;klr)y9GA8IT}t+^m9((@s!B5!N-AY zER37qm@yI`FjhaZDwhn zt;=jaQ~1M(T(m@HFQ|M<+hMX*6e9=MG0DvITm&7VS@Wi29=*3mzydJErKSD3DB<8l z|N1oSbZ6Lj>3_fLYv#(FJBp2ox&CremWIZaMh6^E#@NsG^{TheYu79XeGdI6I$V0b zJZ!3Sr$wxc`5`7_8VY?ANareSGLifajplhbXEK&={4G4phzB|V2)o?f-ObO>@41#k z3#~C`CJK_0HI2~1^0a4DCjEd2eE=WQ{ORk>4^0X%$wc)F=D_et*Gdy zQ@~hH{vYZOY%#E~vm46S(q>T(o!w1K_c(G95YXfiOxH&Op_h-_+eY{p@*n=g@)O5` z*3JrOJ}zlRk4aNv0ErRT$k3326GhKJNbRDq6&^uMZ0w6SKk_efa5%-b$##yDk@^J9 zbutI;c|7JN0szxT^369yrIPyDP+)MLYQLD{K~k~_nD7pZiTyzMK_*CzjHh&qN2R*}i4 zGYgr(vG4dC${LPyLn-V;R}Z&zuU>b~YWXifG;4kuBY^6)^7~1B`4wn8yBP)IaQ*{j zA2E4XsuOTX0K1bVtw5uRPZ)yhZAyQ2`Ym#5uWy3vlpFxw!nmsFEX>9V6Ykg`J^=d` z$;kkrV({Y52>1Sz`&eC7Ydia={kV?X+zKNh_MH;BIN6hxe7kjEQ)Y2-ab{);4cu=x z7^v;}-GJ?AtVdxo|C(dy`nSgQLYKcd-92|Itw0?ds7XgwEQFt6#2MiZBiskI z={i2o+Ymab9GJJnwooKocO?wzRi77xnniuy4?`0XJ}SWD3WA}`teLH=_sX7?C8DwcTA zC;;=fjVzI=v}A)o6WkVG=U&!ak2ERG$E!P`vGo16i>iy-jO8v0PytqC{UR+j9?zyr zOv~%4V0GB$C~NB_;4xi1&c7sRq+xDdOi3;8L&R(JxceADzv@Xg$Kgrf*dp+_xSYFfr)tfF_vLT- zoY)6QAfC#6KqI2C^Sb<_A^8!JI3ETUxDK&9l@^KvGuuHDRS^UT#(MGT<`_E^jUBB7~TAD@IEu zk0I0O}!RnI@>E?F{xEGCJcXdx}`9VD=c zJ@+E&sfV^0AZiN##MV|%K~bspeln=!*q1ab`9>yo3kZR_>g7f?eP*1~*Uuh@`oaVJ zn;X#lUxXIM?9o7{^6&28*eT3sAK#r|WJdT1OxFAJ=%j)T(;&#c|37H;m-}*u3CdwC zK9{Q6s_~2bd)(udl~=y`;z?=EmM)v?z_m)h-R*Bd($2OKrk8zXz1LjeL{U|?n^Sqh zm&;gPN#M*cVvwYWpb7;p?ll<`{149i&C#I#biOW~d2HvEq5Lv}?aPP#Lr~V?T3qe? zQx{*VyI@y(;#+df1BLGPwY)b}+vQyPbGeacLMOfF1G-0~$B7R)(YKV-x4RT8$^#o7 zRokmj6dDuH~eD>RvtC6c6<&xOB|+f z+E($|1TrKl@Fs}rK%ksaeP-}S+3{Lw?RCClTIAEpNu_Vl*7R>0gDVD9_KBiC z@a5ZSg2x>aaJAW$kSMl+_ zEUHYu|=Kb#S__;}EPoE#U=Yx05dT52m^eZ!AjQRd*UD?3S;hl`~wM0ta{} z`@JYBGT|6u2tNH+t+sckR#tV zNWDnoGq&a5@-5T)wwdp3mu`({gS$uiERKKAj;P}IQq!WZHLh=`r)jPi;EH`f45F0E zKfHUo8Gj>rI%yy>(H(Fejn(3N+OnBQ0B88sa-RI8=U)2|8wNqW6=t~V8em8L^I=@x z;&FTJ`ez^Ru!i1M_0ehTNtj{!Rx}JX37Y-xeOAV!&Qb@0*F&S}p5r={Lg;&jS+64w z`fHKrv2XYw;bbCY=Vv+sW1;>ioW@8I&Q@La*iM7W73ZL(?l+m(&GB8gVo6&;rH2o>D>CLQ;d$mI<&I(Y;m?n5@#y|OY%2#S!XeMLiZLNA zTrGVF(!$sS)iJ1faG1Otcs;@6(+!jQ+;Xh<%mY%Jf~ao}*=uQr^iUK9x2bEV#XFwX z8e#g1JlM~DH#Bq>e2!qjKbWt48heTD$~%*dReLK{(nb`b!!z|MezMql>TT_z29w}1 z{Vj(*S%e4v2+QmsNgqtjWt%8{4#(?=NcGtgvr8?}5mSmHrz}jiGfPM5wPZ*`F8GCY z+(S>-lu98i+dObu?t^E!jMDc^=!5PVu`7$Y7t0t1T*%` z)g$?-4AR4MAI-&!SV3KV@9M;eB;eFI07PHTw<85x#oyfBck8S?OZKfYT|NP&0)0W7l3+sO@x#nhHayZ zAH5w{!O{Vi)d!-5)Q4J{MWG*dea;!0hwHkv{plBK)btgCMTf(H8Wk9>ve!x`v6`3IjB)C0xLGTf{ zol)80wtt=Iz_W2bml3*fpcQ@VE=+evo_W_xf6c_imDBz|rrtWPs_*$8z6eM+NOwvj zAt}-!jdV+QN=gdS(%l`>-5@F5Al)gQ(!Y)G&-Zyf=P%;idv={YGi%mfb8zrh6LH9< zeDcrsmvME{)e8T+6O7DzDXNN3AJLc(LwFh2<1VFxT{eDp7cKV^X&%Vi%3IGQ zfD!{MdCL34*4nFj$Ir}7+8;x#toJHC1HBFIzTED**sa!`JxH5b-s`n^#AY|W2+GBa zpU$|s(#t3iY`g(Qz0FeK8?6>Yjy;@U{n{|s98vR zA1iqo9WcIg2!Y73b|RKb=Y1C1?=H*?8driyr{#9w;;``J)XB%kyjThCcMxJwMmp3U zj-^JsRfs=sQ-4|7NGAWM=EqPh0$(5zwgwn(df$q`zx)m@v9YS@Y-1GY1xq2C4m1mv zQ5IsT>@C^;%VFYT4JnWLwZdNmNbo=E#RXy~zi?_(wor*n6TxLA3;q<8?XehO3|Hfi zBX{MTH%C0K95JDC7Q}$!1(OW2Ma<39BO_ipY<K&hXe8bz8iqX#d;WYQ@|#(X95Usq$F3 z%k{bnQIhA^565Dw<%3}d=3MnlW#e-b+d5%iiCRB>2gXS+Wv;{5W$Xn5uecM-kT7@r z1m)s}Qh)`l0xTY}1S1q>m3udh6(Kc}1(#T^j-&zV2oZee z+Bet%ncT`ZRZep%aMY)%ijYNZ&M2IZ3J01g^uWP07DuWIO%&A(2rp%3fa$5+K4e4< zNsvI#hjtr+W09t9D;fT%xhf<<7Z(3QzrLBAMYPg=D%zCt@QUbXU#kKwpg9Eksn)aJ z!Jxw^!q5CN+4p}_LN+Q~F22gBnT{(KFK?r;NliIi2pJ$bd+_W1Ae6fvR;hwb%3Ar^ ziYqt7p;fhQ2$6n@rX?Vael%uK{6lyVf3Asy*wu?(1?ScGBkFN!8;JQ`<)z=j)VeP= z&Wp|`u+7h~641{pXo#sys@w)Kp#3+ukdus8@%)64`{NusQvIGREDgoeE(to7#~66y zo?j*blQvb&{(@VHS4JC9kLa0xrRzDZX9=zIPscJ$xrUL@1JOG}d<~aL#pa$#1X822 z6ljHNkMj{3r>(B%aRZl`8I_pd?m11*aiif2{a$=}*4h7;?}4BZ&Qq zRcdVSGFm{Qv1-Up%I^6?mjds)p9!Ng#P!U+Zw`u$=@5h*Z%a=fc7+JQrrD0gGIK16 z{*h?8JYdb>igm3mq{YaFT%GbyJge6HDlR6QzK1?0@hRnZbK85b_U&>F3+2S{xQ~tD z)|8TzoqNxYb%|+Fa_uL^q@<()t(lDFv)>hcEE??+_v_R=FMQvNh(KS3#?G~Rxcs&h z4=2Eh8WIf))0L5wKL1V}ImiQzZo@t?0#^q|Gafbf0(Vkwteo?>BYjsatSMwV=p<01 z*U5cU;z_!FXT);aEgRwkFpPWQd}s2aR-A?g11=(Ls-+)Bo(v{hf(IV|8cVVV8Y}pZ zhK9kY*-3?bR6A;0$4t!`KJkw^85!H{_GYEs`Ff#CmEC^qSg-lvySGuhr?BzISaKeo z##$An4c}mP?l%N1sm^|6B{p%E-mON=LWu`F_Buohz8zN`RQyo_>xo>MwY50S7{Ngv zbNXKm4rn8awOz|eZg=3ER+TnzEN!`1dwzX2(Yw7u*y-A@Bi@pflk5FCUI#ZD_M9@! z@0$FF0rre?H&JKP*_4)mB=JOxf2Eg5B!s|6y!t3h`#o%_t>HaM}e~MFcSh%j6E!{V` z%{_p#ySxe`@Dc);q4st)B45)KsX36;5i3*__$!&aad-k=!R1|v2{M`l2C7N?==n2( zJ_5Ua&w*v+5gc zZJAgRe7D(!3MSW9%~GCEeJ2dzQ2DH#P!YrVM-O(&u*Je8S3eQr0xmw&*QIv5BW9Km-8V?Yg+xpHdd^1}Rzs4ed>dV9oSLR+6r6F9C_ zL>_?dNP`Mkub}+LvZg*}fC62v+nyazhL=n5v6&E)gsXXD4r176f>i|I7Vyjtn$wop zaBUs*>*`bAcqFQ&PJZ0{EgNaL4MNu~1omxlEu1aDM`7g3Xp1*bcN5~AZ0fQOpC`SH z-kP&oZ|M}*ym=$n<^kjzc2mCugb&|+zVQWJcBce5Wvof(Yu-mw-G01?i}SW{xy8l2a#Ivy@G`#y;ca z8C%K~X`hFcUvv3P$OPM92e3&q)K;6K*4hrLS6=OcBtI4X$ZFz(qb5KjMkulD4j?;F zStAJi?_-u=(>P79LmS=7XPNBw;kg+ZUg~!EU%kHZ7i8&E+GLGUy?o66N8-R4%vpt% zAt=#1>dOVv zR+_WlD%Rr{S6n=66`63Mlo@aFAjbfm)tgD?kUjj4=o*=S{!1c@d4E#s zHyeUlr&+ydW~{k=k+HbJYH36WpvWvbL|ev?C`uArxSpTmTx4J%fWtG?H<9X`7xp$H z@0Wq|9MYUmRs!Ru<7rK|@Vms_njcy^MjYD})G2Gpqt#GmY&a*;SG%7Q9;$|07*Jhw z#Z7-+3de!fXmg{SOAtKNkH=y4xwJYpcIxGO*c#n8^{w?cfbLJ6;yMNUiJ4}-vI>PS z=adFKS-++}>Tc_`2uZ(buK4et~B(F`bzk*XS z*qEFIhP%p(P`i!(bU3Vzvn7AaO@a3BolG8VI=vYVj!r4P{h5C&SDZ;sPO_h-T3HTQ z%vzWowXd1^(VeIgF5LTJmGrP#(|L72MgVP*0MyXB`d5kmG;%)`@14gQgjbE=o>c>Q zVZ7SgK{1Qxb;Aoles*@{g=^Z{x;jwx*@CWOqw4=BTo8=+9|c|<5jFJF6rj7WpE@fL z5rvcf$Lg1X>o~r)JTlN{Rt1))F2x%I#+N~21MYu2A5=#=owxvyPaqjgtdV^%d4!I}VmHrrz9|CeE+m(K-{0(R`f8b* zZtV2P@Nej7pYtb@{Hfyr&DZWLA?KFOIN?6@OXxKOhw%%@Ny;aFI&rOIX!j+#TT6+W za;&wohzt3V9gJMfP(!T_|BPxd3HD4x8?hG5&Ue|=Wz>)dMHFy%4RO{(F>6_D zb*j;mRD5WQViANTX<=?5L}6y`@n8U-eI5^Mq~Eqtl;w%EQ2&fykEnyJs+6K zpdcpI_>kbY^f;@Zkcq8S7U(X6A5~zG0k+FRLb^neEi! z-WhKCjh>jT93;V7ll=}v21>g8J zlhFe&5Wf-b4F_!CdxQ6yrG9wnlXCA(p~y;83rtk74q`fQvR~y;M_h^HimHs8RtgWK~({6#65&L`uxQZjB8#bD!xPjaAlt3tSrIF&`reM> zg+5FkUeYItjQ17~3G3Os1!u-89@FO(Vf6ZI_A4qZDl|N2US8o`jKcglDd~$sX|Okm zQ2nib`JLDffw=%Y;6mtB+n|*4k?rSe`bk-E>VgK)QHMkCS<{HxSIVvJu-l10GVq46 z58QHv!AW+x#ktk|(XYV~F~!gDABl97i(dF+jMYDcaEY%mLrY(QN3Nr8`;po%9}o4) zo*8?mTf>Trx$YP7aRQ1*;(?SVWeyu$Tl|AEnVtoz6xItuiLY~OS;X&Ao&EpXyy4o^ za`b4u_jy(ALTJr}dO0@CK=iN_#wJ5kA#{dRe^rp*}=T#wq@barGv4FVN z^LxE^H8u4yt*om-3E@Lu9oZi}a*UlW{AXJUAQY5qsCho+t;i}QH}#AlhWxM&LV(RH z_O9+yQ~mG5<2%Sc56)fuar$uouVQ z#EtuLEsn1@q$0XQJh1U9qyy+qcqv$7*bB^r)l;>^+mJ0q=Bdz-!Z zXD;L!39rBCFUt2e#}jReRtF$hSuLl(D& z?^X5eP2oE=i^G!uEWsFJ5{zA!l6~?uA~#0CLnlzl0Tt80h=_~7WTpy_{eBqs0*qg1ycLe!GGP#?Q=Ldm z!rWUYUIByXhtz}^iYRs};|~diR?W&N4Y`{&NQGH0f^4y0{w|JH4yRonNCOJ_4ncNZ_g5fpQMx&I3x)!8BmT#nE{s+ zModw0rNwkK9fMN5`z~Hm|Jq8Jm-^XC?7?$zajnaWvz=1@r%HoDa8}*DZu$8oue;II zhxxVe!P3HA>VMlgCik0=V1}Bnzh9zI)6$w6837j`z%@|v~9yeS^ zY@MJ3wM%v8Wx>5_7lsMgH2MF_7}w+#6%~OKO~4Lorvg2wc}goiPP&lw0jUMG7?`Y$ zg1zVSVU8HSju(mCQ6Mc2QkU>|%;TX)dvvdL*I@oPjLODP=&J~{pMh8i}8c{!(9oldgj{B&De zCX<;VLoeFZ5}q@y)mrzVZlBvo9O=l&YS>JyI_T0*Ru?S~%L(Qw@LPOza-B6Owg|?I zDw%fRxq0b8|5)|opSMd-nzeL0`?P5VoH)fiKn_jVcGOpDD8d_=hW`Yq{lw^qj{A_t zLr#pZg|&Xt@w9yZHUQv*z{hQ4;^Dl?fLFacj=<&BmS_F^#^}ypY6E_b27TB`{)3UMKP3GL|3F zrLzxi!4jh0flid4((crx-S9+sj_`a_lUupRdNBC%_@nc1BT(RWZgjOc@Nt;Loj0_7 z)%&Ss1F`1*C?~<%duKj+Z`2-jCxDnm@P4O^p)ImCz**6w-MvrfYH)RvuRTF`if6?s z!|p$ACtjwsFZdGciq)Q;mlvi5rnU7X;~+BfHm6MAV6lEK zZyL{E|A-DrWw?Dj5ITFS{rkEc4K_jhGE(H|cH}Dm4s|z7SVMlT+@7wB1?$x3EBQsa z8)xT%+&GJO(zjJa=;oNm1yIRMGjE+*2V)m@B_P(X2_p;k3*wyeJek~4?9%JV%+9tj zsT)U!he&*+IVd0Iukt|~8V(!SKNSX{2$LG2=U$&SNCc*0kK3~(eMmfDUad;uHVn-# z$YgwP-$~|Pm>H^HRS~Ed9&AMSk(vw}*nCW!D0!$Q%O?D7_nRp6qTs6RlTVL~cXT;{ zsw4hYxV3|hs0$&j>Fo--4zZ(+e)AQqlAP*)r00IoX_>!ftQRGPg;=FnvJNfPSu;Bh zC@DefAqT*z=oL;8VL5E(W^P1qQori@)i|B$ek8f;{7AEAUGWL5>>!Q+<2Q%!gr;TR z?!KzJ-QDi*Z8R1ZUfd>QhISmSul`tvyyjq>Md{fV?Zs%Om%NX2Or$tr+BXkjWfERjh)01U4dxG9w-mHtanD+snC*2YaU0ubKvN1M#Q5t<)I={~>0^4|)#7uq-ojme7+m<@P9jw3# z!34ApdZtyzz5LHH&WLPB0lMIqawdLP5NwD&{z9cGxe?r`kC zP3>ldx!i=$d1*1~#c_zrQ2#d?6?0AHDmQdm2Y0Eqyj=0c21oZ-+HL#iDlF}&1}<4F z1dr}2UHJp54oi^C{O-;ame#OGP9=Chp_5eoj2Mjg(R3+%c@64W5E<;@f zUCr5H=e+7|m@1uUq@_=X1s5Amk3Y=yBJN-Oi8{3Jt2crG(+u1wy#K4U(vK^ zh>6@&zc4}#+$;ObO#ON+9FaHEgZ`*^SDNz*P~rW=aqh@xv|KTHULaEb;E>_n;Xuw?eTc6z`~!*pQs(ZPD?g<&ET}jjNdJ-0SP2Nc&Werblo-f| zW^F}&M|sOWfrOKSwM3IsxfxbrJswrH_nuDVa{^3<3j#~D8fF{M(B<@?1Y%RB8C&eS z#T2@ehy@3E`{QjFt|Q#Z=M37F$``CwusVgW8RMr5-~AsK00Ad88y&ZBZIe&60hO~P z@mxCKFi*)akFZtuZN*mB2KR1yahuXy z+r4lEJjU{GmIdTR-OFud0$PN9poM8I%_}uXM(C)*b96FWIwXzvPPzZ>tGyE8lx{PI z{I3Rf%zBl9bUM3U?3y>ZxurJ!=G!=Zo6((u`pC~9+2k*LHjW5~uxDhNtpp72OS zM7|p3v8W4xn&1^xgpgT?%M%ro@EC_RSSh-5SPsQTO$W;T;>dNML%;2;7#3?|EXu#q zf^2mF*~R$N$)EML+Nmc^P$50ZXR>E=r}vnR&DHsW9|F}>%3>of+%GDiAX5v7)8Ses zOr#poEHO*7e=hg-%%ynO3!+ceCe9nK@0U}&=aIyiaOA{B(2nK2T{GrV5m=0s^6!8^ zcw()xT5)ic!{9MX8`p%Vl>x|Pyk zCzA-oh%Tm*6|rjtRoug%apB~!<}=1kOzl$)0b z13FuD>*v3La_Mk><2{ob*%4k4V zvHsT{aed>JxOh{r4}0=F0D5yyl{6UIXaNpCgGHtf1joDb0KxQ9 zS2B#DY4bY+NEJ^{a9XO*$lpZ_xL4p_J^bnVwwEHP2ET*}69q&hqN38Xq5aeM?jEn( zeN@?}NtafjEW}wbO{dIWvm(t%aUYJCTh?Lo=o{!(Marx8b5TuZe%YS`=v^6fAG)>V zG3Gs!H)Yz?@D@(JWo@&2>=jCatIJQzH;9M7u&V)d$7~AKVIHWICB1>o0P;gVv)=FP zu$}4SgL}!*5w0yaeRR~I zX-{3AX@(G`+qK<-Hb8p7uy)|Snu;AlsoyaJfA7-H;4~Tdwclv#zz6+)I4h88+a|Mn zteMRG;fNyn*;&OtkR63IX1}kf(iNH*M9N&rg{O(K$eD; zkG=}GV%of8O{ixB;EY4`-v2PlswJH7$|dSV*tW)gti-9X!cQ{C7wr!6dw482BvAYzk08|Ffy<`^Py?VATxf9|XQE&00^S}FBlv&%6 zU9TM9!Wgcmrl-Zf3amG)`sdv?zca#trDZute7n6CVlaA7nz3!VIIh+BMxl)>jx5oI zNyn9xcj|7M4|U@6i(M{A!+ut@3o{FW3_6UEcJ*zOv3y_(0l)$Ad;1r4;W7sKl6D=ZH2KiHN16JZ>z-|RWz)dc(n)ip;uCAHvtY z->a*V>-K{sTa+Jkzn|b_Tl>isd;l7sA3lWU&?0sOs4xPW5&%F4tVxk7FY3X30kF&1 z@jWL1iMY46Stnb9WSqG<0E!ICWq>%EoSdAL)Sex4(aNzrU=jQer4}9=8|$DJVZ+0C z{rWWsx)FAR9YApG;NZ}5l@tPfk?qmGLaHeL|3Sv^c1GBg5}-RRxFcO4sD6&Cao6006Xk>S+f9RVpXE9RV-b{zbgyTVfrLsJ2QTiqxyW1GABJb)I~YK{?|H zLvlHn)h`K0%y;SrzEwTq~87KVh^m3OLjab9U2ZL1WHXfo6d)!g@P+;>UIz zKM=OI{`MH^k|jTZln#nQ6A%(^Q9gucyc5k`soPc`nzsOJ!4eq5;N54+@QatXiMScj zZQ(uU3F(1tT&z)mDPFn7ng}CyW5oS6VZBldM#;4FqoNrAAPr<~*B=GPw=W+d4 zvh23wI(9j^(h&op*9ezgyr`~^Vo3Aoy~`?1dZGm%9~<3^_)|ZlpjU5{VU3sMTdn3$g^?jc(X6vIZUC_$4J=MkO6M16u1%4p$Zs|9FnL`~sA_dTKXG4j z7wo6HUo_{fTxZ~4x;=~>rxrh69=njVUYgsOnwnZ$m(m9Y9~~T6Sy^c}J0H&1T9&RK z{szTN8Xr9tSMIXJyZBctFiz?2Ccc(&RWCe6TkIU2&o|@&}jwXlZCIglcX3WPO z&P0mk(DN$wg|dwwAI}G{d9IPIwX4hRnKEYB#Ti@z+`0KRO74ZnxMOFbZHtQkK!GB` zT3TA*O$1lmpU>(K`&t1zPe*S{+CkwrP^tU_sB%C!2ViF%ogSLk1{+7s*P}zV)j4p* z3+kL5G<%N^ch|=&D@#i{8JFe57GX_6XHs#t=)q?#NipXJ@icAxZio~r_G6~CQ@^AX zh#(LW(VN`liQcr?vaE3WLPRyWnsheAsj#c7tL5coULwngpOu&MjjY5mH3xZxmj$3I zU46aF)8m7HfIzM8)BJreop3gH(AfuuMW;PcpW1yRg6JqNLj8{zpmfh{$mnVJ#Ri^c zGO!R7l;+&HNj48F!=kft*s4x-gfeO{QI#{8DA@E%T82zFy8K(m6Vq-m$HOSwOo z=|Vq}R4WzcQ(k1erf)vq)I|NmKI#r_DUg{0$=c_p-*2WMd% zLl36-l0y$1(zwJ1`Tx?%X19paefPz6&VGX}Fc$U_S}#NC?96|Q7Im!Zxbm9I;x0VC zAnzUA-F`-6$rF1Y^$v~0y`!TBuQBt7DdD=EZ3;o-CBYhgrc3H-Bs21qi<-e0ni6r% zvdA)wIH^g;8q?^>$ohPB zqhwIb-iQ{`gWM5$W$^e$<)40`Odye&jrVabswonX>yUjPLk=MKN49qE zK*ilQViHMS5_OsSDfxhg5jXB8@`2VwJk{h?rBOD2hGQE~C00SEbT+>{3^zWxnx}=i zpEi@Qm*Y!w?k~R&$|5#+pPvNp$NK%V#O$vlOv_cbGXhQCEads7RP_Nmw@;b)(7hAL zwnhT_ml<*Jyk+@7bZLptPr7Z)R`Fr^rrQmB@q0A&&|+pZdN#6|XoI}y zCi6J{mbBhG87Jy2vRZUY$wo=q5+TkS`#YLtR;EC5_!q*Cb8SXfV|x_aHJ>j>&n}X_ zB8`|%cI9r>M9QY~e4=GCis(Vc9W`FMdPRdacPQ1bEP;Cxkt%ThZWw7*YYRsOtXt4@(4!ac&t7t9zOH1zg3O5J$Vk&7^r(i*9 zzXAg)b9!9em#wMWU5VRFe z{g{#%Gg^<1!H70a%ER`qg<_vGFf{C{wRpiT@NMk3`Czd$?^FGgNt~d7xc7nTPsX34 zavQIS$Yi?-ay}2MaSMwO4T~x@$bYge>d+K+lD)oY+(}>Xy4@I}9lj8-x}Y3M*PCr~ z5ZrIPn;e}-`1hm7O3ciYgu;r4!)Rr0+kJR&Le5Sa_aCpb(TwKNN1sRAYn}L3DO#P! z-tNI7s;b0?r;Phe+8=^9cTbO`L`gD}i+=+J8`7|o!Y8B5=7ReeO}t3|3Qzq|EB`i> z$j$M?6g%(;MyNxuS|q-woN=pPupc*7>#XB_h!)hL>*ut>Y>xHrLKIIL7PKBsZ0fWQ zlJfTv&aP)^RDI}Wj9l31Mx2-qCwOCoe#JUKeyh>PTRRn{Iyxa}js)A~iT=X%AyQ(DO0 z+(Y(zB36U4y{eKjFp0VWrScXZs%&QPH6CkI|5s4Y{Cc>i7M7XV#LE<>{k4bfv1zOj zA)(g7uf@4FlB;FUJ|-52r~QV#)xp`e^8;#K8y2kuX6-MRgkEf}4!{wKl_n(Q6L7f@ z6dVXgvuW^r-p~d2@7->Gz7=qO8a7%?_r+Q}cE9mxC$2@4f%z1uT+5thp=ucWHvp^6 zkn>SZ*=UgkMkFJ+{k>l7jx2-fG?gjU%@+>F%Y9`;7D9q>a~z}SAH(r zeCiN?oK}QPNYcAvsPO;2bKa@)^t3rh3OZ8#+7%7tQ|c5X(`saVgj<=gKE zs51eJ>Bf`a<`dcW3%@#R>qJSB5$&m1Y62ev448*AR3_x2-kn$J#6ncA0!?&^H= zPoUs)o5#}-$J@s>-#%Sg7V6r$dtmM=o*r@MqV5G7T6sUYI6H&7DQLD$~@h&^vo{rkdtJ!r;*jbT>})~p{g9iy#Z#g#V>*e* zr0F*LzV{yaeMYh3CBfbRvkoZk@r_gTl0C7ic7*8{i4G`IWKFCShsVLDhp0vP2OP3B zDd`e@8{XIc>&hv9MCV>muFN!?nqsS;2BTE0TImG_G zW-|Eo1wq!jNLgO5=0+=L)T|U#n(-UcQ~_2t=w5hor@z1OlD&S-i&yh|4lSKj^QT=N zyYhZ9LY75~Nx8yLLg#vrqYEV+va*x&ozUAh}Mz-4C~-beBN*YiNL#&Dlrf5o7(G#1xjz9 z>q*;pm%FE@-WAG6?mOK~5gD!#wbSX+NG2YA94=flYHG8yvpO*~=N*GKdUsc6h`T>XRDbTp2Vzf5~ z99DYN{wI?fyV9@*UplU>t;{yOw6NXUq+AiJXN>2I$wc$D zYgthGDYyd8t~AA@hWpF*^vnNTdOv7}5-~lFGATbORZXdP*gy_k*i3F z*UL*XNifM%D^RfJYu^Gqj`xA9zF+9~y3<>^>EL-+Pbq7!Jg<8?Ywn6573}7#E4nL- z@I1R|Ujh?1>(ce(-IZ7IE#&_2DBqS2)t)u*y@?~Mn+PO0=7 zc<$MdveHrjKV*$$JeA6O|2=zni{wWJ|MYmgeDH1WZAQgAoU4_mY+CPwbDyJTwYvdJ z!CMP(qG_#Q`L1Q9Ts3hy!V&f3B44V%tIhsQ53fUh36e1VYb}9f5yVZR%<Z!#vd*zhF`iY!)Zx68)q+^MPKvz1eX`z7%7)Bcwf4-&fS|{H zfN93VD#J!RHcZxJzQy`fx@?Mqa<%GC0vG;YvKC=C7N?fF;&Hu~!{k6W|FaR=yQ|Aa z54-xx`|cuOc_p;^F%NflcaM(BQp{_?)z)T1x<5xY^SI5B;jyeW+FK`zA)Ie+17dY6 zD{h*iF~r$81I3)gT#xn2zCY8SQg=jziFUQskeOu5yrk4z0tg`xS@|5@cPOx773+5U=2TR>P&oSYyyEgVl#ElC#Z%F)dK(L+{eiEa$#tQe*O4Z zg1iWnAqVDLV$rVtwvvaP-+Rf#SSp^+ngh+vmY%bX&~>7O4#Zz}hSx(2j`mWj9uL|= zTX$=aD{?Axa-f?@wAPFEz)*l|f4{TSV+8RxyCo>g2Jy)^_3*eGh+_zke^H3Dw6qjQ z1i?-KBCbGg>6?1?=bYA7;*{La)yE6~+<;5T{oYIvrU1s^%TW(thPJjgpw64Ios4y0X;n%rH_W`E zOYuAAmFw4!$Q?KXJq5`#=hhVd1bp4u{G_MS9FSG0-rcgSk@#KkMSxpRQZFh6&_93? zmgzK$>oCm6`k4OL>G_3)h532sYt-FV$UB)Dn^4@8b@jV^BaeTVpLx!didijiU_X)8gq=1ZbvNcmMbll02)r&M zjyqmkhzgjHikO$B2T+GwarRD_J96guLg>A4h$9&z z3D!Z~ISq)}kpS3|6PFxG$`>4x%A2Q`qWj8Ipd5-nP-q9biMi!gotdv7&GZ{*Z4p5$ z07Rc*^Ees_V_4QFB?ul3L#e51n0}EJ7qTjS#NR3mHuJ>@=)8v#sX@QPFxYD5eEpV^ zON=FXg2sRK8`a+%63d(V>Rdz+ng+FY;XDq(=$n~mH)DOkryd<9%!EKzCsRiV$tZMm zbXr?mKNE_6Ni>TI%u4YGD^ID&2>^Z5)6@C9)!7>1itDKSB80P>sfJ3Dv{L}Pd%Ktl z%O@KvKVi-ly^oEX7lp0Kf$WbyQ4rgb>eyBxrV-auaiw%so1M*1?(sBbaw&*!*FsHRzn@D(F-a z=rb@sKO^_kwb%GXdUFKrf9>TB8R=~LMO1H>&CmYj#8AYkJ~RJCao(n{0ve|GT)lDz^SpULwq4U>4V9(0Zmdg{xg>w2eIgXwD)> zZC_20sEz#M{Md$KZu~lfAnKQ`k)p`e`{{>jN#Q~V{T-@UjV@n9B%-zu{f+1Jxrql6(UNDvZUPH6xcJOC0uSBuI{Y6IL;PtW#cx$ckZ+S&lw z38kVby1_QEuR}wRcLm*+Y@g5L-O8kgW%CBDlQCG~6$o&}&7wC$0puy+kj}76QNGbG z&=<-Q@4Y!o>dnunUj!A-uJm}0Ud-p9`5e@ zlZ(3VX4;%Nu7In=DlP`b%x3R>lkhP#`BfB*kiF&qaRH3*R(^cMn-~*ck}Pl%mc{5n zksS{kyP{7>?DO?49K;oK6CS}L^Z}7Vzl1gofdD-NN`KIz!>7FhnZI2==!hiZa++=y zsF7U-1Qw!&f=)dslO7V3S6>j&>&}>~;5|(a;tQN864EH-70&%+LPN?v-cYdV<4%l^ zE7qH_C?J4kv15>JSK@itvES-`X_4l_UF*XZBI?$bN#Sz2{04L z49WPZR4bMiLNP2+?f5btHg>Y$tV0!Mw)QEQWi{e8cp@pzCAqa#`;P#6`PpcCYe$HI z`9UO3H0Y+wN8yC^Y+^v!(T7NPT7i;cfOe_ZB=8rjKxrXzNK5vc*f-61Z@Sk;!)mMh zk0uY$8&iFcnA2WDrn5^+QBrf5gluhXfpPMj?+gs~qt>+iMJ#v8CKV8`Yip3^0gc;M z!zI)OFQRR1k*V+po>hGa@nZ^D?O?AqdktYGCMJGf5_iKupIw7KF8L&ENJqXv_RtUK zq!|n1Bh7k|^u=P6NV84G1{1%Vo}JH^+QE(pdC!Q7oGBx$a3IJNDhHKG&;cbc zD+%l;xY8nO;o#h2Ru`QjT63p0bD=OmMGQ!$)ZFrNoAV(-s>L$@#l?k_i_7dGtD;r# zOcqlnGB7>G#?gLkh~Q2R7GO#$Jeu%3`sTsc%@*){3FRfCnZJkkTRpI&Z^vZ)&*B>! z8X5wNJFax_85pPL=H@@niMt~ss6=a14j-%bNd7bMMQ%rpJ7h{L>Q{#gYCDF9f85r2 z6~s>R@4kQku8!396YsfHQF8L-@?Li(bM+Cozx{C9`oef5Ql+Lyt2(HUS)6ziegiXmzQU zXpAbLnOVaK*2L$9_t4J+J&CEKrw5m@tE>Odal;{<+18+(2Nh+Zf0~&1SQjz)0Si1@ zaFZ7|KM)WZA8d8j`wO&xjDE^`YhCvxDGlg`7U_p6j^Wx`&IWyRd0*KHP~$L8ntBHp z9xsRLwq$lFm$jzGI52<8Z%;u%xfVlWWbLKH8xU^YhmCNu85)6MHWLGW-m3GM=2y?` zOLNPAX(p8KwJRyEVC8ukPNB>en8FmG_wG~SfF zZr2fgfWjyT1mcm@r1Mk_XKrZ;>dEQ}Fe!4+kaU<1=r%i--h?ZrB#pz=!D0xz8BbLK zSNYH@tOl7_Xcos;QcQACe^OHIKI=-ss=kO{NHaT~KlLSs`JX0pj8f+2t*H)QhpO$k zFv^&$JaYs^T`BG>;2LOz1m}r=xA{s~_a|JC9;^j&`W6TD)p3v^nw(S~H9VZ1nd!iU zW#!}JV=@feKL5(&O*HSUs;Owft8Zhrk<6l)G96{WNI~(FLsmsNdjI41uP6TXSmc}7 zRar~k=ELx2V^a2=cMW+hZ*VqArr$K2$u!^)rA}i?#_``);pB%Ko=WU}WPgjP6+VRK z2`%>aK)=W|eMKGuxq9PA*1?ruJUKo-4(9c3&-!0R?FyL4nlQFxC3#8sMtd{=NK3K- zk>7UMTL=`NjS=|P8AXQA2AK%H(S_#*`J0_OC=wwLc3O|gNrg+sCmFjKjt9Mpig$eX zhud9MOW>U+xfa$<9*)Eptx0%tsra5m3!^_=4F-g2V0VF7b?|8FJnudp9)Ie1tiW4r zZH>aL{(>Jof1tZACec+V=@s}i)xdcjF1A7QYd;}(eMqxTtp+iHL_$-b4qy8^7aJ;Y?((;M|`d!-PyqgNVxr<)yiR zclkR|P86S9wBZecNI2_^nc*A1zzOGV3?v}|{00CSZC4405w({A$eu4f zMBq>xMx;Sx`);3pQYiQ2;N*gM+q4P`8aR2nZjI8TP2wH%%~{-CN~v?Fwa>EUc1Zo)XdU5y)4!qfeePH7XaJp2WgU4edhBw zOCX0AwzWNO^Ss-iOhxFMhsHpsPa*{yUGv6wJ_{Wx)T>5e2h;OFm&9gU8?kHrJ03wAmm9kk_sm}@C`DJRrtc*gFy((=pc@AQJPd+y*O@`mjsm&&yu5BNSQIU+fwEe$-E&!th5D6cY z%`9L`z%|l0FVcWR_ci`3Y+rgRbkkS0V{Bq34}$y&rLGMAKKVWzv4jt0KRhe%52aWa zX-bFvaw&|NK(Ezh$FxyeZ^M`VLCW5)`Q>HZ(zHvE(`q}=zOSJ%aMRq6$xpx#2KM^0 zQC~_8MyS8Eyu7@$bb1-HbPrEk=kG5Dpl^4pds>-CPz$j$<2C%K2J>p#6FS-jPUYvD ztTu!eVzY*EX!KoaVf-PC*`ybcvD6b%)d4z}6MFUac-UAt}uJv49mX??Xi8Bc~87hE2|$^ywI62I`HYNYo%%ZUZ`%HiX%3k zUA)wC_*6{db*=t`N^5fnFguv+l+HbWQ7_@uCi{N2_6pFR+f>072jz;VP<*QlQi6uF zU_r08pD(2zI4`xtgyZ8QJPNh8$sU>M-5}m>k|xFJ53x*y*^9T0Ipf9JK&F7JJeC84|P<=#d3PU=BN z%cfyR1a0LyhDFdh>*tIa5f9W^GBMIkwB0Nd*ZyVER zJQaLj1wN>wYkkX{nKR!k5QX}GRK0avmCg4BdO$%^x*L%cB&9(*r9-+Uq`N`7kq+tZ zMmnUsQ@T5(q`8Cd_jm8-p8tUJ9G>&c%$~j1T6@izfy$O}zKezannTLK*vot_uD7M7 z&LL>Yf0ZQ5JakRdth?1h9HhQ`NK?DSs-g?i8xH3bER$t4nkP6RDhe{?Hv^>NWPr)#4TkIJ2rqX2fe&TnCAEPu^?lQw$p| z^-&zLy-{#KX0^n3Y+W;4z>zYfT&gaKPso3(vW&}Cv3qp1hl;xHDSNg&45gUD8Kfd8 zCf57t#NUI{`=+5zY@SmP5o&2Y5=AQXdG@cF`sD=%LlZi1|C{34?>^x_-Qrj^U+8xQ%TW_s-Yw#DOEzqH%` zQ>h4R_BK|T2 zA3nq*8*RMas=GJCw^I{-ibyo@O&S65lxF|PaP{-htE#Y$_IihEI@_grr6=fqI`|Tv z$)SE1BPABp*P%^c`IX??yq1!O0TI560yV$olRN=7;V=};dzSIBFRxN)jp$=$kgPU5 z#oP<3@QBVj%;)w(bXDc#(EixRGL`$@krz6DH*~EeDSsuOwy>Ji`D5=_=U}>JeEsI( z_NU#C!{m>HA;)+$L6;XW$VSux4-w;<9(nJ?l%6{Av!8mNL<vjvs zMt(x!#_rerX?1;{6IWw>nz?3LCntw52=@skol^nO)VH7cN5W(lJ`q+WAik58vLwpy zJq(GN7gAaF?-vrblmB`;F+cCJPC{mqETn43jc)b0O%Vx(55sgslA44N#ey?Tq>BCbQKs4&_TnwX_ zeY^5hdUBxlC;w^EIL$F344>pS)$8_S<>&ils)%M(t;cnb_dnxf!TV1R%I^+~X%5Cl zG@Gvrb*@9}7JvWMziH&JeY!8T3E*_Sv1+qdaXj^z6ruc}5L z0FMzk_;fuLdF%Dq-dcsAic+nXIUAOo7u>ed7rA)S?2}B6ZN`kaalaV|!?smGmV@u* zu}M`%xXh1WhQRc)QnBx6pHEr@Z0*Q#VO_Dz7Qnq}q1_H~(h%iCCXlj48#&qQ%2P%9g@WQKhS_59mZ=HlWznJsPS21u|d#_R09&2fL zfYwPxRW(#oQ^QD)IXF6^0h1T`DP3GEQJPCaNv|sG{rH{UCca6Tc1vSz?bfdE-E_wc z`CmHkJB#aqNczW3D(TXS+M(;NY(DL`zYN-k@p72+&VZC*oK5su(6F9=EO4xoCVWDL z!|#BC&VNA>wSE)BuwV1h^WKB^;C{2yzpdO@qz*qTOIHt9^M28Tf8!lS?j!Z%QXmRB zg@=!7m1RweaqN+yCSba41f+c~8lpB6R8pQmR&;lZQ1<0N04Dw!Oi&SBn~IWywY$f9 zy4ZJ@04;uU4kbQH@rrxGNZXOLk&k(Cypz8;$GvCc@#{N#_aBY2x%6MrrS&DP&mLU?711Ta-=yM?icg7E!-iJPuD*^iv; zkxDaCfan4ug*){Q$omV+nTGW-i1Ow%FGR?=6gan7kjUVBB zJ}wWjCLA3|rE%e-ACw`bH+>>G5QDpn+!LV_J;IL-qzdg`@7Q#))iP+tX9PJ7NMy!e zV{FM9U8zDg)|v~IXf4Pv*t#pP1}7ELo67X`nAL20ZmUk(2fIiD4}bi1;cmletlP_D z_gH)4m?ybWcDz`+48z9pJi(exSPI;)lzK+U3h%>+zvQm2cqzIYgldHI{{n2bGA~8n zn6dM6GT64sL)Udzi9O}h*guW(dEHi5xtZ^>+{CKuF1)CbrjyEv3AP+z#O5`63)pkB zSepn~A+g&M?Bihs(e&x>DWRrv$_B38CUaWcL^F(gZ!ROnw}jm+(bh+wCmX?X#ojh0 z%qJ5RG?m;y_~3yQAt+_8hcbP;X+U7RKuerTT!RW8Za6hb)F8jobjN57v*E_`Cg~h) z(!`Q0F^9zs$ROV_NKd%j_Fm}!xxh{4W5H>IJ$wKR8x=hpj~hB_r0l4?4|54# zn*9=TARs?3w4LIhq^(=>p4F*w>*0?uF#W^)=%Z(`@_c0#?5cWJvVN8JAQ+(jP!fG} zIEeB__V@@-xjw2hW2VJ4zH8!pSRWBaK%NQgpZD+w)n{(fBDBaBu?R3^W`lTWsIf8y zIotiV&6Op%gpW=;K4peAv94Ln<@j43`Hj2bYv@V_t}v`&sN}bWba=h`2^QIQt>syFl6~eUIkry~{(fbBaJmm}OoAwpCk85f9+p5r) zj1C-E8l67VquC}tuVK*GMqWqhk*l9)?^58==YX^`sUmHBB*Xi%W&D6=Plc0PxSQvw zyF83UC{DIKBLFSh4nqpwsqy?vq|=uzLpP>0V_~0L!FK|PTIXng8jzDY7n6uto4-vC z&vIA^zB2oOUqc$41SNrlH;||dd&%A0kSsI*o9CM+z}sBibgm*~AIJtM|IjwDhy^iv zJSDa=?okj4MT$zF3m6|Cxb0?GIC3Hi#N3Oo*Q6P>oiX{n=EHzMiCLgawV?3AjK| z%UwARrjG*P2OvL_H(l_0js>snM!9A@?W$*xq1;lI*70B1-c)U3NOHnGV(X@5CO z3q34WypRp}91t|0BM6y%T6wLiJwX0I-T(D#@y}rh1mJQJqR%H9qYiz8ij{y1c`&}u zP0|*&ksE{?QAt%A^Rdvy`6laX*6r+(O24N70&bjR2MPv^_deu5!j-oRkc894ss!{R zNMIDpnvNSA*;HxcfZY1m2gp7=F7S|qv@d7*U$Fov6BOQvzXF3*B0+wyQ1~(cO3t|b zAM$!dINTx6TC`~6fDaBnJsddII}5C{gM)+N zLIl9WBQ)(H(`&U5^cSnE9l@b1swO`LT;&O>Ugwv;%Jo;Ml@EzhQ+@f(ZpPh%sm30r z$Y54=Nn3&Ag3 zc-jD!UwS8hNC7K_X+m168+NR6SeKVTd8YzN2C^_?>}emUW8qH_Dni>BSz!o zFPrDsFa6}i{v(r(0zfc6+TSmdwF4gqMh@9{7B`!I{SO7&mJ3D<&_LSFkYrlPOZcA* z!=f3)cjM<4B&o)qnaG)sjk$5XA2=Ux`|~4lf4>IaBkWzW&d}Mi?SG~A`YeD$s;yR9 zzi{|qlC1efW)!_}exu6rJ_Y6q4z#6%0jQYWJ3PdE3B;Az-}pMXNGKZtJxm*?zAg@> zJ3E<^CQt5@I-}w&@iV<=Y(h}5Kf5`aRK-&b@z-3GRTegLxV+2QjxdJdaeKVM+RUcu zbm4~*dOEw0A{_lUi6vtPFAeje8p$Yhi^AT}mw(FhCUn^y!Jy$QJ)Ly!JfXNX#?#XC z&o*QSrHxl=BRL!%G8sLFQGIh!=k&hJE38ynY54B%@#8M#35itjbwcL2vU#K7y-sps zgnuEaRXg69xgPHk(qAfBgxSnsHA=_Iqnu#h-5@&K*X!GAhb;b0X>+r*8BMg&MyEE1 zju}`B5vy4nkr(;D6)8NmLEK@FSY4^PfsfY=wD;Ynhs2yu%3d<+#?76JX4u> zMzPI53S{X&eWroIqJbCbh3fQ47NpVNO2{03jhRASA5XTz_fNFlXnI^noNX&yCd54K z!(}3s1r*p)Nd!vhBMC{hFNZ_g5Dcdx?|DTP#3MBqb=yeo|5U5IIjxQ z*|GHh0+?iu#>`1`He7WsCYE*gdXt0bT;`uO0v5L~FD}wpx*7!~xfs*N7X_hA5A_^% zhpWsY(4nejB;L;0`Mt)Ga&R#sl)d|3?P&0TH5D6ggTOpJu*Wmr{ZXPLZgbOsCkbq| zHEJDIYnq=(J{&*hPnRo>(2=4mS^QCDJdMSiqF+7_L|y0OB4nx!D|L0{vtG6%9zbU{ z|2;OK*H*2y%ek-Ym1DUZV*NKuopBh@4|Gp%tDrp0W30_+Y9atQ?G^JUldye7N$nBN z6Lbms0vdK@g;Y#=e2GjE--i~;fueJS_yEIL9d#Rwt(0?5z^L7Q>McuA4>X+pYO;Y( zvjxuiq4wV!EAltRGc3)rvmtIi7fZ%vf$=ug0IUD`{WKA64g@~vNYnnd!8J6 zTgF9UG!}9{_@GQVgn*DrJp|KinGNz{@-wqohI%$;v5en4YCL zqlFJp*LjmB;MulRjCWZ|iuXEuz-f!OLARcTFw$>f976Bz^{yjdBi5l+rI{qt$@ZO^ z3bnf#i@i}`57M>=@=33oV5-)$D_jeukE*JwAQEV3@U}(()>TJGM?>QUhzPXFqquBg zA`~bvEyAfLQa<)c%x1P7T$`izenD8rKpUs)eLtY5#4vzit1U71D>E*)8EfavFhNGQ zPI0TL(1b}%*gW)i8d->Mj)y=F6DyE^eP%zDX9^Rcqu!QhRWy2M>igRe?!=$1i%v?q zPR#^gaof7+gJk8tFjg@Gb<*8tES*Ho>zlI%**xlSj+uhY(M`4YLEmkR`Agqrwdxch zYe)zzt@C*$IxkesHDsHXy(8<)M-~|4=7x@H6!i58#3g<90r9tz5V6fTjOL!p9 z_wZ=p8;1I5556IC7a*7jEHX9oSivf`G@(|nNzrM@`TgguZHvM{@O!n@^ke53cgwN5 znhMydYG8Rz1HqnqXjf2k$fY+9^3_AJ+hYAiEs}q0Rtj6jX12>h9p5|Zb6ztc?AgYO zlCo1CB0Qm)?T?hdXjyZ4_^%TOvCasDO8|QJ1_pP)NPV=MNa>*_@=;T(^6JJ1jCK&` z`M5C~a~XzTHH5yz_BltV;WThPFbwywio{e3YR0g{p11~lB{$L7@AdZ`8%}bZzkemZw~JH_nk}u^DMt{V(&QA3=eKYU4Y(0Lf-S}o{I_dN zqdb~hS(R76{axeq2?}BcdZx;0$Y)2JDye9@%uTX|OZ+4F6!i$JU_q_t;-upPT8U=d z!ZjK)X*{js0nbv9irc7fwWN1FWoB=RUQN$GwVZ}Die0HIO8j`a-%$7B5q<*(WziK^;z0{c03H&_tLpcP_THkrXUT^DNViz zlPa)7ue?sb%`JO4@u&ilWm@@XCKV(rfJ}oBg2^HxuJFfGV(1VRjXp++z0HL2|Mdkh z!u%x1{i7t#MNpmPJ9WBJREvzMV;_dObYa6c8@D4=1;lugAf216>P;G7Ta_S**d0Vm z0v|>`-8K=T@n>}hBU;prq&%o=v0Q)}cHEJJ%Y8NzH z2S^LE#_X|F-q%5cWDul;vszACv59WR#yii%&zInM$+s24->{+orIYv|8xj=63v1{n zDIm$>5Pye2Z!B+&I0>kGucya@vZe?p=-7YqljpA)$um-U1u`1?mn9&91t>dh#WRdw zSqTUmphn^U6>qW>VcpUDyR*k7V+-<6J_jl4@97aOR06yR5XO|AS6)@v5SxhnmkoOF zgXefeaM9Dh^)E z5wT`7?Tdkiyv)sFwQQp&GQXO6QH^Y{gir-d4k|`?~ut6pRcZh0+@KuTJ(dDT{EE4dMRV!5>6x|$yZXOtUlR}zSpPz= zXfky)z$5}}i(h?Hn7Kh54_3b;12cR6g5FbebcVEKfNzZa=Sso9whYcFanu{r1)%|Y z^I3COSm2Tg&S&KKNZFDKqzvPJCsuYTlu+TW<_c zSCAEk7?(?sZn(NiSqh6XjX4a|Rl&BgXfM15W`4n@4P>ZV+>;%%j}MHN%(NTrt^?>#IYcy>0f5OI7SKyd<8er;)Wg0Oxazw~VT#5{BrEreUxmyvv-*-s&H8w;f%~{VdqzYZ z8*~N6He<{@Pi$}0pLKPBRRt{UA&q(m%kSiuWm?bo0zm<*(G7!9v^cW%5TpD$)@~K+ z`S*e#{P?~_I27Qbedh%}v#76<`y1q$;Ya-j)+AVUfbMaN37H84auhJ7$v_$V%pr%L zx2M5$JVz8k4L)`fyVRuP%*t^s&{` z;%{^Iw)Q2fcPv{_`E01*-@(@CH!egtU-x}7j;@L>fI(dLU_ky*J zbFgY*E|O@nqqyTFtL59t*8Jimbj3WV!O0D38W~Hck`v25ex0=MDR7|YZndS!#V%8QHTrv(D}|Vg6uya(w61_BUKK zRNjG>_jh3&sA`|F=*?o`*q!M!x+!D8L+a`BFqNf#Pwpar>7AFgA4vqM;b@uxnusV< zu2B;XVfRQ!5>$aA>M|-1$^LW8@b|Ke&mCzJ&v3f2y#nNLe-%!qhYrv7j@Pto!w+dt zO<6%3OO(q$Zhp9D<_9Rq&@niJU+1u;ioZFBDe|7w(#F&JrHWtz4!u3T#()ozp@yr= z-MIZ@T=GkoU2L1;7M&wGARZfCl6Wi5k18o2SB7rQFAR^3ZE-Y-wkG84i^K~OZZn7l za>JQ3X3uezVpL3(pciqDh$9wT;=)u`z<}mOGv59~3X@v4H2D>GAiKm|10D*c*LBAV z4d%TQ`DOBI!WmAMHkEuD#y-8@_g-8rFy`j(@^{W^ro{YeOLJrYKlZU?PR&dJCn_&t zi4|~Bv43vV@XoH$bM~a9EyTz^vRbjGV|K^+KbXQ0forb&%IDN9otKPNPz4DZoHU*~ zQ9TEaLL=`7kjw9esxbYXKjFP)Rg_tOVkn##*h0%kvXoh^C1cb`O3f4DBcDK@cD zAHkhw&fd)->YZiIc^fU@=ts+5K+(~Pd#P61$nF5jC}LG=WIz-SobzoU@NIR`A<`*2R^iJ3mC@rLx^1V+z;1KIl z*Y?Xhu|Ozm4J6l3e$9g7K4~%Dw2O=nXEm`r4)&SPt+Czy{pXz?^PGNQ+D{rF&WmkZ z#~Y}zHZZzepBh#=yCP*)PIhMz4W|uWA!kq4JO8=JG>@*>!mjFP5i5zU^gm?cHey&{ zclVsIJYN;+oGddmcPa5ZBx}lj#eigt&Yn;CJsHPtVJruFKJ2->_TUt-onI>!`cm z=v?VpzdvPp?=pLoli6_lr|!O-9V@hZP>0y9mjuV_`Ub7$BvpqFJM`&dO;seSPz12l zths45+M!^%z4_{r2O>mQ_mAfS)kcQ-0G~sS_g9~lnrUCuJ7H@-ifJ^BSWyL&@zbEA zKQ;T+>R@jqt9d4_vv!NSDc>zc73)#~;g7iM8#mtEO^NPe7`E==Aw>by@d4}Jm9ZYS zIs_o0(ymT9oA7@P3v>#oYK8QPdhZ%zBx9!w9{C-qTuPM?h^t-d^p&xNNXXcO#Z7&B zN2XJ$W&4)-Ej(Nr!zkFqia2jEzbI~ti=;1f3E^MG>L<{yDE6hTR~j?8p;s<*LdE!* zi{nSjr)hFX^CTYjpbA#5(4=7dYGbUGqoN<%b(D8d{G1r&jTVWCjJVurdu|*3*SsgV z`;QtEnjD-byE(B-+EXl)83LT`r>jb?BXVcP z-z4b#WOg*7cpCH`zmg9EL< zA*y^m558Aa7>=n_{^x3a3HrOAtefiDw!}IUlbDdD)DfXP6gR2&5iYz1s9x%$02)#s2Xoqt#GDD zWC3w}nI+sAyGrP79WGQ%Yn0p2YtDsw%eDk!gmQY*z+4Oav@LwJRvvm8zX%7T$NJ~h z{i?O{4%_4d8K6~o8Btl9%^Pss>A3sxJ(x(2UD<&!-c6^T6VE@?5DV#HJzA!~M3ntI zU$G0!cR;(m>^kb0pt+{I6@Q^`6ntSZrk*vqFEVyL2_I0|S4^?5OA3UMVXs2YnoYN9 zk&6Or(OM=oLtTQ;ElK)j{rw+T6^HS9c2ewutH{Wv^l z&bYQSAL2K>wC>69*jF^`uVo$guc`-Ewy>8Tmvy|>n)cXRZ$Ij)-wi*!f8%=wml;mg z`k|VAG4myOa@{&M*=2uaRRz5kL(kl0W_aA7km$n^*RjMmeVn!WzehPkZ`T>+z}PE& zUK|r`(36qJ7hq`={hg(!6_x?*hnj``$2jpPwZ+HX5Q611vta3B`kKIS?#BuEIB7Tq zn^oPhVF{s&MMhQ;mLM3*lVviP!_?jj&?`)5njFT_Vi+D+@ zXsPsy|9tOx=F#=jCA5s&%fj#y}7E2P5YK~RUmYOp!`0~J5LE25D4J-Q{s^h*~5b)n1@ zg4nWr1d;DlK4$z5W!Ec1_UM=+7-himk>FwHqUBJB#xfn2bV&lD5I4&|_l+OwKO)b~ z;#-PA0FKB_POY*UDo8VA?MIKkQI*DXgf3c(M{z&ecK{Rx^#>v@lB}J5xj&$DbC*QK z{E+kyCe&1MT5Ww6LP%`oiOV>2hfGo_zQ9s2iTguMff>K$Ld(R&qENuIeS{Y48t|0v0WM#^c{O6wy=Itf{*{o}+F&rNmPbBF5eUzS1? z?Wfba)M~8~5_V|Og_r!Ncu%bU7C145>H2Lc+ z>ckuOKBXA()B7*eqYW=184aJ+1c&cmpEdiple~n$g^jCZ?$3%SBWg*+6#qgWrp#-? zM|=9E@fQ&s5FnuNV~B3R3o)sSEE`ghFhA%tSqJxA*~cm%o)4P{5#fGJ^2?a>vc*pQ z@LHa5irJ@Hku3Ma9|TanIllR3MRp46I3KoWS`RYzTq~AqHcWutlZ-zf6Q3aq2tI_v z7XtaeS`PBurSbp!p&IcS{7~J%x8`e9ShyVw{2n&tlbQR~8pmV$vV;!pwI>R#W&opM zWytUHH1T2CB%fhmW}B3i`Cvw}w&-=`o8S+7|eJ;JPh z9wny}E2l8hNx866!y?D=hGm z@4rGG6Yn++n-LtSnzG&~)GiAk&K&+F{JRZGww+%gKBGn+B=oXU9{q>V>V1@Vx7IMk zVl-MLXtH%@4RO)_11|#)gRz&cn)RbnZTOA7_NC4l5UK2o;l2g2y0sf2*pGt(ISEr zPceMb)~;%ypM)+(v8)C`-$GTt<_MrztQ^I1R>sPo-Cxy&>8dKoMMYcJ6w(R8&c|)S z+JspBlqfRYHuT#}$-Zo-$uqM4>e=RcT2bXz1W3$8ztiQ0cam@HNf#URxy={f3?XWv z!Li{?b%$MUecJQG`^Z>hgUqi4Qp<^+LLKSj(;Y*VYABEG776Fr-1A)wRs}_YOQs#t zqQgR?j)QmbyvtW}*2Yi>o~YSvK`&r^7MS&$5mHh9SwoF9(v2p_^t}-o2UFbwn=*!) z6BKH=WHme<+p9j8H+z+PdZw;;#y^>{LPb1ERutzof4)!Rwf|jse9vgKL2npi95JWN zU>K=v89~m<>yE^0CdJP?q%PA4Ou&%S(={t{xw@`voNwhm?5U^Psa19T3Jl0k{q1?J z+N*t;iIaZo3FBPK6}w7TP2;+o-m!dY*s%*yzu&Ds0Vlqx0)ZpJKq=sarU+2@+;^Je9M%KH zbcUHRysY=b*_UG6exx2nu|8f5;dpMdTxUxeucYwb9{eP_Cy-f|GTy@BR2DWV=r*A- zm}111H(P}~3dJfr8gheb4;^IwqVf4J-6)PQ!PY%=LP>lmc}W8k<jxe>v$A3r7=IbX|rdGiWqCGSDyj0+TX@9!f|Je}4n-#;MtQZ_xtQ7wD@ zMZSJwU%t7Wz299}xnt^>*{y5)Yv6}^`wK&d%j@sl!4ZEs@7>Z4^WSC97v%-r7HyX@ z+(!fK{7g5aZTzK+W=6cxyld)>uT@)bg8R>b)j@*H&+M3i(ylf*sp+R|#3yIGOu8rM zKl3I<3&P)fWV7d-7wBb_uJ?&g^jui$t zAa6$cej>mXj`Y@rYjxOUXXldpH$)^l(Oim{)=pWdM4(*qZi)x3TVPP~Gtk9mI!TlF zHSJsQ>4v~OmIZT{z@b%&M)*y9{~E4dX@kjkymsEfuK-qXRtYC@0AGv~J+U8k>@a(>^-Q_EwR)=pRCt{Hn;IxLgi>?lH6Oi?jOEQYlZYXG=*$NpUOeK#kxl&{ER4W7G@(l z%C$J2{UmnsI@6C@pCYjOxC`U-Fb=a5^S*6s<&F8y66;xZq-KraMtSAe`adJHZQF(X zUiW)+vZ)?t@xTMC?I_Jn%IooZ(vNH2!!BZUvU)FgNff|(;^gYK0T7@%H#`;aDn`Vzr$h-$L&P8 zLHKr8awC{R&yQ+!v;X5;ls&(YFZM6?s*{}*0>a%sHtGm~S%>nsLX9`K!nY3lNX!pmj@pW#NaDb2A@iMyE7qjBjIL^vlA ze`IY6h}#+Gp&6x!uI2eI_uZXglT2qv zC7lcbxH0pj?Y#OkeLHZ=)bq3}q1AKY#LM5PV##^uK<7Lgam8i*Fs5$N?GAbOcHy|e zX~qBK9*CN&E#y7rQSPif)%;92S!Uz%M?5b%$sJF&ZdE<4Go9C>W_777YXa0GC|RGS zty_&BK(BcNZnDCajO?CYauztS&eoT5yd~jT${LO=|1uVMoW{O9@*cw){OYSp80GfX z{;lH3)m?Mb=jAa`tmOn4tTrkKqQzG-ZSxO*j~ydlMbw}-;ktj4yW(Z5V1i8zbowZ0 zFKqF)pM#(LG==hm?QV!xw$;yZDcXS~eIsPLw_>XsWZ@ZjT5l}I88bWle+qVY#qZ0% z9aXE8?zqhqE%|LXJ!HvdmYyv)l8wnBaPwZ!^53c_wX<*5xN#vDBcVk&aW+u%`696D z`*~Rrom5sW^-LMx$rU75+KQM9-#84zD@yk%FWW9qjiF4RK)s7Np92gyU2Z>vfc#A^ z)RAFuSv?ZxTeMsBsk+a`p9mCQcXwt}i}nr$qx=MqbOmJ+KE5Q1D^+?~l0k;K+NG;x zb}9gWHl^}P@!uDt$kJ;XeQ18#PEZSFL?c}Rl?Q|rA|WiGB*@Qk$-q40xsh7Ve=+YA z=6Qu}?ymh5t&#Q@{3KIK?B0*8Ct_Omyh6P8t51?iKQtQ$uQsM4zFPKLlDYX+SaN%D z`MzQ6i!sf9oYiBLe(zGbyWReq)>4Uad^67EG2HIZ-lO(~LsYz_&-U1`Msf_=q@uAq zl~~kppz|gx=6*H4W#ZjpK(tb`^3rQG24oRuk2T(@(5^FjGT%OLs<=df2HV#mGSA`! zZtLJF=zIiuzCBtaM{2Y7$OWq&NP`sOpf7QK*N}HP1L!onie_@vH9&IhYMnSA4@~*- zS#s`R#?UEb!UwM7+WY*xgL@(6t@|7`e1oIA_t{YDTTKNlG>)fk(+rQ;fD8uo?wLk>|}V^BY9Ac2KNM=HMbm- z^~l|1^m6}``Pxqlpy;c~N_g`aIS!~Ar|cQwsZVwub#_-kRA(uEq6fFh*Ibea&U+lPl5$L<^BwF-^PDUI&;- zT2tBe-mqq3wJVZFGx>BKhp5d2Uq^v~>j!pr0($@Mvl@FOUi_7=m)^8vVQJ&HayOFF zqzH6U1;jf=GvSS?m!!0#3JH7BiDQ8k^*5F z>bfY=X7P66@^-72i?#N~u3?qEyx^NQ~62~uoDx%5u#Rg^7dxLXT_8=#m>XfjKsxMgfmTPc&Eqq z+iRT{jZ$Vhxdx(E26E`^9owLT1ue4$Ouupcy4;wm$)oZ4K7JV5?W^UBw6=uc;$uyP zkm_IRq9K{xJi{sCo;;HzNh&p|7>@mJ-SF(z(`5t0gBc2fn)LH$-W>w0cfgpvtPgOd zhGFx6_OFH#27;F)ky>4STVF{Z({v!r%tYEQG5_b6^Od5U;!9x3{xcKiRoPR}A)xv) zOIpU!EQEsf?bD%mjLrF{Y$CrL`a#`^-`=p()A5L?z!nwjxBz;Uj~2{YB+=x0V4|lPtfqk1g`m%#a#D zg&XJ8S>Z6vbv}?N}03i z*jP7I`8-1XN*TZWvZ}^H4>wfBFO^fG`KUWZdTm2-iPs-kXMp2UE5x86z}%(LcnnLx z+MvR*zKvMCi0A@YD`Vaq^eLe-_rbgM+Sq(3xE!dVUnoX2pZki7&z=N<1i`!`fPp;U zJN0YVjfLBi9!Ku+8V8@dEM@cNKs;;2%)~ zUV-N83s^xoGOd1-uxWp1nABLeq12sL>tgtLGoMdkVhslo6byDNu z^g0J20%bt+zkAa)hS?*732MdP^>Ml;*=eY(;{`xK7)w7O6iOP=lUBCTZ(wWb`TT+HM9N`q&!U`wVK78j_*HC>ljQileky5;vGZc-QA7 z(fy;2sanYBrNxc%vwcv@#CL`m0S-l_%wX*_`}5J)@=?m>l#HX3nHsWS!&)cNYZVVR z#Y%WK&ygb(q2i5Nj!h=OtxJ$3ul;xx{sUWa%+ps^D_~E*y)ivFUI; z0MV|0u)zh548$%o1CfQv|K8XRO9}a?8#Y1sKNNHz^Xu*Nipwwu)+Yq?&=6MQ&;Lba zyA$F-Z~uQc>2uQb^IkU44y$Lo)G8B8XHl zZ(~gwf{tMm2Gm_re-&lQEKmGao%c6-Z`IHFqsRePF8Ml~I!9A^WG>y|2WcqvcIh$PpVzutJQu zQ{1SKC5k(fd}o%yt}~#a^OgwSs82~Dq{wEB%akssDv#NS)pk#dx=)joXpd^!qbRiK z8$G)y%m~n;N=vBs*^4(mSNrxUV$l&{ zF@GjMJ38M_dGVW_BPhtEVY6Go%)F`+3DKMN5A0=K1jH=3 z43NOVZku6jdG`1kY*aXQ6&SjhPX^9P;%bm)>Y+otSp|}a^Qz1x zMBmUB*rvvOs0%f+5sdF#`BQ51AyY9tsH?`0N13qKMlf(gqD^NgqbO%utGl1Ae6e*{ znOJi5CuL=%@=&xt3V9v$sES2hbRsjt0mHrIMBExx?rFTlZG;_@u`v^^QTD%kFeJw} zYM}laX*Im_PX1l{*Tk0)QgB%Bi(VU7F{q5lp--azB?ToxLCquR5~DiRLKdRIU0*^O z;LqJHvk2*X`1o=`}{o^D=fN#!)`v}8pS~&s(+Og(0 zr4gyG$+KcS`O`zs)M}UL9igiI6!Qj1+`yN)kMVGF~z{EaY|% z=|ahm{gl`xEJOWyVIYKch*Ip37D;s@_pj!;lP)_VQ9uq~1MJWJ7PlpHI3tNEYS~tP z1!vN{Xq)V}{!_A!(%uM_V#-6I{=lgA`c40v)U56i(T({ibH(c`i)Dskk>D+t+V`+A6Yp&=> zrMo{HD{fw#3Gp`_>-vQP2if+lWCLRSYR4KHuDT`XMzjE*Y7Acx>JOS?X}Zz?zln3NjpI z#@6O+!G2E7Pyg|-EZ$xzI!DN<$yI=-m57F7o3^1oKn%OH)i{!sNTRjIE<%* zC7r`YzdHnLFYPVtqp6gjej*TVyfU)L%5JP=`Lg8~$ht=YmJbgsENnnv;AI4~ZkCbl z=q^PU&M$+@mtSc0YVr=R8xVqA~qBPM8o3+Tv*Waz%F%9^t`|@QuGpA@Z)Nt}Re}zdprPd4u~hB#5@` zvmc3>u3C>3S5PV0@JqjPORB)Rj;yV$urFn>RdBcjLT2(pM`r7{g)6!MPMb$lh9#K%bk&=*> z5TsKnX{5WC?v@r&x}+QF2I&TA76halWa;jP~NUZ_Jr9@twJIXLLQD#TDMB z56U~bL59OuMpO3pY~hQ%I~D6(W&I5w?vOs)wZvF+EeVw`QC`H3y`Vr!{SU|8!*9~P zS-vcUcHiMCgUd658 zQrf?;N{lId0f~e2J)fw%B8O)bRwg>b^yV8E`zPJ^=T~SDw+0(Fy{G?dj;D(*_MI$5>mi?4syqzs>h@Q-GIy!5xkHKax8p^@TaHfS4GTL-& z5Vg5wD8QLTO9p}L-qE9r@=5x255!Zmi1EpbG(xtzMte+*W6M-d`z2V?ylrdL=e%y3 z`}qgwRx4EJN?jA?)-6I7i{f6j-9{))*E)_!sw){z1|^p|zk}jCPGJ5F!isfO7&n^k z*`RyXMv1S0I#V0Be#AE;gD9Yijn42YT>1z~s_n@Cy0yE|*6Dkml6T9`=^I)2zXaXn z?UCZo&`+pL#qu}w$2aO{hw?DA1Wm0cW}les89MTbX{J5&5| zu9R-3iswn-MkQyBG&8|-QY^3PlG=}m%1fYhFPi3i<)kq~Q?;GbBw7h!aa_AZAKg9V zmEkG-wuCW=k>OPJGHVL@T|K1K`cIl+4GW#Vc0xy;&#-G`>8!1Z=1#qkN=B8~v?BJ8 zf;~E_B)sXlRm1Xx=q2Xp&y1ZZg_;$orJoTjR$ZjeB|gr$o?9|}7l&pLSgx*DG|iy! zCW~fb=^v;G5F2+BI#@&Z!*~b?tx^U!b1MyTn)SNy+z(M5ikRiLHdPxT^sKx$@}3NB z`GvA*?aPJHLnZZuxPqO=WOB*Fhmg9keqPAGAT;8|p00$dB)5aoH&fAXYbWD;58n#Q z$XPA;a-`E}DXr{TE$GUxHEG{mUu(4A0loWI!RlHkog8^G#1LE+P97V=Ji#GEFhtoI z`nfJ3XkvA#l(NXZuDo2KWL$R2XuSHa=GjFt=&t1Lt-ssQ zxJDqfpVTV2`MrWVb?ZH+m>Eo-)DcrU zuRqdLXK#2@RZc}FCk^(R>p~B1WaYB!jI|`Ueqntfxp;5x)-b_8E9P3q6EmS_lN|#; zl&w89mn}9f&k;XMT7{iV@=M){9EUIYMq8VRcl6o@s^0WF*IadJb^<%lkK3!wn|ly_ zG0E>es=2ohh8zV+9*4*^4cLTUt3DqAkn)FhLxEg=K>?E^zw}khjTaN4>ZT~VU1?Bw zZf(Zpv@0KVIKZ>kPJR0X!DU?J%!CqU((yRrM+l?eY$Tx{oMf*evpNj(<>v%KJ{s>{ z=JcxVrePa95Z5)-#>rlKV2hr%55vMLIIgnQx1uRowG}*HY`L2*{R_IRD;&*CJ|udz zV?O%1nx@IbC|$H7W~3^-&91~Q1dYYJlLbBZQ5jj8!*oXT zeXl0C?`t-M_f20wmhGg?PMED=)>T3v8C@b zaXbwd9S)ZJW|mENaeq)lFxe5mR1V)xBM`0}p@!(PH|m`4#{stJXBb~56j)KINoARZ zXsD=6rPU@q^|_Zi=9TFiOB`eqD-St!fJ^hgEV?V4P{XVc6?r-;MN232p8rDf} zI5ya%7bQ;U)*#Z?9+4=4e8VVLBy8j*f9vM6u34g@(6RbK<8CZt2C3*J+HIK_VGn#( zBKFG5>PS2m(~qU4{h7(r5A2uTG!OM1=n@D-;{|WyT z)|Z1;58DH+Hbb3`p|%av{*2Bg5q63tIl`0lTvxmM$34B(nEyouhXlos-xlT7lnG?T zFL}eqe<#eT4-wW4C3`^SJ}qB+clC-by=m#Bw^)XrVQQ?xqGXX6qFkT_R4E#!FpEu^ zLr=yA`@51(o3YBD-%N4(195g00vDK7Y;!~da5RbhVER9p)=J$ZZkbsft|bR-WM|Kn zFYV&$pt$nxYi^-^+dQ=6@A7}I2bl;UK?x;?ZKmM%DOfo-tXBJJyHaYTpPn97Y`QbxPMYa zFZn9LQSD;ruFD+*=LnjxN?n9!bZuuh&SJni1)p#`*7f}XF&*f#uw;oL|GMb8--i3Kk+IO|RLHOzaVcNU$f-{13LXkZ0D& zn&`}FGp9|)>z}z>lTL=2VPS2hmq`4)yPdNl{17>OIoU%8!}qV`XtZxNvfIU3jU^F; z0yl1TfY?qy<8Ay;`=d$=FHND*{J_OsPj($I^}OjPIrG#~5~b&sDb9U7?rGB5cQjHT z#MZ_SW=K^xeuQOS@D+b+F#1D1+}i$ObWZyA6`ikofbu%|+N@e$fovIysf!rg&?x-H zU65YBbWLQroT@a^qPja(&z(hkLVb789Y93rm_I{a=Fm_kbXpJ-6uJv*?oCt=@BD!0 zMlUdr{=|zcdwz`k>9ixxHF9CCR(yUqYwwec-s+)lcQ#x4<+y7=Pq)R)x+P-d>y;6o zmQTr=t{LLww=8-ldKR6jc&+2#d5&-->UDeF4(^xOGN+fBsbV6}bUlpkjKkNOGQxR_l;sUA-0Od{ z>XLmAdYu=Eg+DRV)7VpW>zjRRFgW(i*L&MnYCrD2T>YL`qri_$)&iJ~h4MSa z!b~Ntcci(hRP1F3-mBIf1e2Lz@s!Zs^u0>nRBwNOBY{Mf8=uPAV(69t z(b0ywZJnfeN%|<463-VZ!m?&hs*4`e+2TUiBmsvmCX$SwpE-JPGx~@AHu9j)2^29* zxQsi7h_l4wqSRXEtE{%`i|>;EnoY?R#A(YByEb*Zo8MAJmCBe#DD!k5x`@0?4wYP;)1C_$d-_V4CY%SlgMOC7w z6fJ%hUM=2IgT>47LZ3C1@-Q(y;)Lw=NuTn;&+qyQ`Y~gLetipS4SF84fPCh!pTWP% zcRJ2|OL*aPnw@{Zg^h1}Eqr$3(~C*#U6rMnE303>mA2y9WpS`jbx1dtk+`NdbFspan6+CM<`1J7jXM))?gWEiL@UDbEzk7B7OdObJ zEnZ>#oJQKKnih64^BlL^QkR8@eUsWPAdcvrpwQoxSqou5Y{)KAnXbHi#EkVMKgt)5 zjzo|d{1V0mI^CSC%iXpFKv4uAiYURT0gfyuJX$vDhrg1BAnXpi|6L-)U_?KhFKfm@ z?4T_ut5%-^iYqBl69?CqmknF85q?hTdG6Wy-lXiRD+nBQy&D$5wey#v$)=M}l{6^B zf1^fS^$A?h5EM=-E`-u}v!&qc0lNB&24aYNx0((nhm&yA{xzr+%sw)W4_+$=k2YXW zl8}(F*B0D-BP{H@(i47ncPFHOnMS~sVgvrFG98ru*=|kpZu>&TF*U$VAs_!f-c#2& zC@6gw&TnXq=PwPuGL zxR|kXgK4B3IdYhydu{H2*+0(3x)9(=zG2va5O0Xr0QPVn54M!SzK@im^Da}mtj&jC zJC}dWSVOC0+Mxx53)qiYEN*R4g&3ik0&4%&nT5wg^_%LO^cA||!c zrYol8YyEo5htPFk$0DMZy+8>0fH{ z(8#wASXCKJ>Du0dP+zAKS)X@=;3yk2+LejVvOnFU6Y*`ksn`UqOa(|bM`}j9V z6*3`FxTb57^@>oP$I1R!+IN&8iDN1+9wc0ZP|no+8`E{5`P7VzBZw}6fPf$NPz!+2 zXCRS{-R+Uc!OlYz_p5v=y$)0wp%G5B+5)tPUY8usPF#{Gml-%DiLhu^x0eW z@aonq{*;Z3(1&7LMHK1&6-wzJGh+X7@iJXN(@QTA;X5y*D{Wzx8q-i&pQwm zIp^cnT;e79^i#+}aJ9(z$-L&hShQ^FaIj|?wo1yu2Wd7=`z!`ZJ7lbjZWM1FZue z6EdO8^0a-flHe68LSA zzVO?bDzhCjRM3+;Yaar}6L0VAbmr>CgUbg7QUpC~9{@Qoc-A z%4lkW!jz?ZSSagI(mT2&*QhXgW}y2U!x?RB@!FX$v^+|D{sBF>@XUT zKqX+h+{}(#AI`|IN9Aq18nja)#Dp}0p*2@08^g(*sL#kWNqg7U*X2{V!gX^_F_017 zzlnDSX)K%(?jo{nhk?SNfyYv{#h5<)IN?PoW0Unv<=SsQM7JgH zi;?V)xVF@L7fa}9Xuq&S{Qdn!MMc{UqR?xG1_r7$m`>IQQ+Sfp7qP*DliygdPt|8(3t6>PSl=|={AmKYIp@wCykcqAB{?})yD5$6t zy*3m@v0^T;uHeeb%C7Y^rN7`Ul%^#FvO2)p)|IOlnkbn4?AbFhyp!@1JhKXopn~#p z)@1^vrFX<=NDp1C8ylzl@MEAHKt{mP+}sStAp5ri@c}yrjxzl=pSAwD%*;$M#~_rD zl>i+0__?5VHwe@pLE zr$~|zO*HFRzV#O;H@Azev3ziYkdP3y5)Ev1S8=g-W<$5|i#B~FB@r>u@d6w{mS3|$ zal@A*C8gl>5~WH9rI@gD(38yB*`8EgO-)TlC-u;O5ODjT>7W(7>f(yVQIykA0aPM1 zN`7GX*c#0RHw7LTY=L+v0nNQz0=U z25&zDT>-t@v~3$qmOH*fk;NYPqEiWhZf=@`KFNoNZ?mMW9v<@ZiHQe3kz(G#{{Aas zVk5U@YVz>S+BsY8Uk2^d)6<~g1(vGT!vYZgDk?$E_4Sv4n8?zO0YO)WKbZ$=IRRn! zJ{jabTFm%f08XP&{gkJk>6-)x%ON2qPUf_lOyzZo#-!;7bORcAI5?cIj!nek@*d89 zPu73Rc*;6#3iiEhv&sr7VJ28@EJ(JBR7Tvk#73SWerH?cg4qV*IRW?DS!u9;7Q#p77g(2JR} zRMFGK#$lCWzIay*H(`TN-z8Y322(LHvCfLn_PEs`pgs8yr~Y+Au{gWUxJg?#{*}7X zw$2hiiP}+0KxxCTxQ+}NuAQQQApmdEDLtybbDOJJ5iuV@z?c$ltmms68^r>&CqUDW z05Ce)E-t_JYGnxqo$9z3Kd*opv4A5tDg=fV8bIQJ*tFj4H{nTe^6_2II}K}=PU<$f zlJPkgfF>lx#RD5BEoYP3U~Q!(B`OL(#_;gp@$i5i>TN(DfEWy>a1YzowSnaWn+ZDW z#l^)HhBGc$Vj?~9Q$EA*)#bc(S5)+6#ClFzlDkr-t`)>bqE>2Uknv1@BF1V4tN}Lr zy5k}Zou8y2s};1s*GtV^^FCLs7}^1^j(>Da-Kt;C=da;UIZ$NQ)!qB%NS7iw4gkNj578ojth-0?(Xi=OT^(lKtu|dzOK{*H-f7On$e(S15KZL zQ{LR+v)Jfj1!k)O>!|N@WpXZMWwjH}tW7J#NOgSW^`O?ZHV-Wc-Q61oP5GVH|A4_^ zC2GaP8_EhlaDyyi^XjDD4u$kB{#tqtWQ4c_u-Uzu+nH()EAMBVyE>}4@MP~(AJNVs zq@soN-~=!ygW|gJjz=|i@q?JiL3{f(3MI)})63ejSKK5SA*PlJolj%8-dI|_$r@7( zNrJf6?h4D-AY)E$Cx&m|+IpP3mfYTY#aVzA{;%EQ$<;=Nj8h>{q6hQS-Q6uGC$}w= zByf;IjN&y(d$I+yi@RE{v==c4rz$noIegmZZ1&wv}x@P%L5)Gg2w^v3-7ziaTQRTS@^4wX|yhL#>m0DE(6R06ysJgH! zo2@xDHui$zoJRSfDs5sx)7H$Fnw8$y!a8@cUKmR=mG?X^{H1|Z$Axd}^M@>>@_XTDb(>)C zoSY$BkxVvN604c5esM)Wh{EbWRq^}$po?RN%)b?D(E7hWH*+o@% zTUPJ~eFiFPr&F6J9xvy_!gk>*(OQR{jD}n*ufQ>Cn_+zN_-Dm48^u#oN}`=N%x61N zq_`jP&+>_bb6(T4yUaO)fUJTy@|7U*^QUTMI#5RnP1=}XK?V!pghvStK#$IJUZ()> z+*bes^LU&VbQ@_lIGPk?;VjfU7z3ONfS6ddVQ0X_qNl zZJ80Je-Ye_Bi*5d+%~gaK0d2IK60>7Ehv_1R;>rqrSo`SbI3?wlJT7a0|N~3Mux9Y z%>y!eFWcn0*CCS_yQ!0x!DaJch5mXc*D-LxIN!!$M?93a8i~{uga8P6+L z)b8wLSU90|&=J5Xz*}%A2;WurQ;B5$1>I+Z&D+0%Y4y?^K&r{_Ha`OE4s1LOVK8wN zZvv38=)RE#(9-@)RbUVjGBW7=Y)rcYeFST4pqfc!gWcQ52Y$LWcJp||EjnuI=#ZYU zNik}yjO8m#IR-YJ7v#XNr`k*>k*DtdX^W1Z6^n zr>I!HUbxMN1EMF9G#p1z-j}yV^uWxc&~0ruil=Lo4fAU zFD%7e2H^e>K5;8zwhT)%%AG^=lwzT19(7j1`Z& zb{e;9`~?8uFbQMexhh6deGomQ?)iF$?Cfk~ z5QD@6bPImQ{(t=YGhw{BH2>GbVQ+1{y9$ljB{~jn@HpEBXp&1nK)@I7RXh<=K+E*7 z(5Ub1Eg@m_W1-U0($m^sACzc&ywK0juiljW)hjrVoL1moDk>^KbO8Wpw--b(OPYf5 zF?d;t0E3JwkwGk_q@^V-20YUUf7G9DuAv3Dr8L>sgdoa-;z5y&*be*%Q zTk`Sz;}-^uL;|b+{`NKsl0RuLvu-0t4ZfKeOh=;@R+kFokoD- z)(`dLR+(!`ZJ-1MumH&4DWJ{cX0I-Ivzt&Kl1+Vth;0L6LlBbXCqepv!QHX*BCDu_k0#6wRbo*__R*N^vhv77;U*e+jy18lBdrsEEl z`vR4TWH6C!<-qsell?W4az+?C009qyJ=lxs>BQ?;B!5G|aB*?*-Dmq2-wT2^0pQu% z+9G=OO1}O(bP&Q0;4NknJpzPNXL}+^HV*(pum)v8R2a|xL;Yi|%?$|YffZmgTs8>> z4n;*p<@PAj7X*G%v>CGM?EO#u+d;D8jd~dx8rst*en5N--kQIC`Ny=X+XxD>65Mu+ zfGLiSj-!N#Ss=QREcNsI57HRLxFz?H`!G;4%I>qz$D@W?bG-&dvj|_r!otGP(9y#m zt6Ly6{_RmzUv~LKi`@ z1q6eDj(Kz8xV6>QU!HN}bfWAJhH$InSWzIns@@G2N&clY;Mw%NEW$KL`jse*s&+^M2ra$K31miwrIuLBFlVxRT!HU!4moqD)9=}+6QzsMtX*Dt|uL_|O^B&3sFrn5zjg=(cxA9?YUE#J&Er!L7AR>3 z?YAUFui}3^-1>dMb0mLjCI+R^KkexTb{xdY=>f-k|J%^8jzwExuxCu-`lP)?;XRQ3 z*r{r~T~n~czV}Qt;;e-%(@vtGZ~2Ocm$%U)rSBz39z%L@HNO8qH697RB?mE`b>?HX zi~{wW2xWXUNE^WvKQZ(Md;?alT5Z*Y@^SUn>_9*m-xJ5=fCxW(B(XImruzNk3N|olU|2P43{5g@V^lxC9_OKZ2CFyjYot!Pox-aL@;K diff --git a/docs/reference/images/msi_installer/msi_installer_xpack.png b/docs/reference/images/msi_installer/msi_installer_xpack.png index 3f5f6d975940e2b922b377b372b3296d1cd871e5..e457a578877bb00af0d65b46b2d7ea9e92f9ce69 100644 GIT binary patch literal 94815 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�@gh0H`_E^_+fy(XY0Xk^b2}bUOZP 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 z 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

}DH|7VH3v$=Q53KX`3 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

9^dwQbki$zLn-4iZKI(Dpk@3v@BWdHH}|R?<#yo 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

-ZWDVFcVA}yT>(E0v;c;4YJlTShmj}w%lW% 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%0ASi)V)> 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Нdu8YP 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

^;Hs;)7pO8n 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 diff --git a/docs/reference/setup/install/windows.asciidoc b/docs/reference/setup/install/windows.asciidoc index f5e248598ca9..f2e9077e20ed 100644 --- a/docs/reference/setup/install/windows.asciidoc +++ b/docs/reference/setup/install/windows.asciidoc @@ -47,21 +47,21 @@ aside panel with additional information for each input: image::images/msi_installer/msi_installer_help.png[] Within the first screen, select the directory for the installation. In addition, select directories for where -data, logs and configuration will reside or <>: +data, logs and configuration will be placed or <>: [[msi-installer-locations]] image::images/msi_installer/msi_installer_locations.png[] Then select whether to install as a service or start Elasticsearch manually as needed. When -installing as a service, you can also decide which account to run the service under as well -as whether the service should be started after installation and when Windows is started or -restarted: +installing as a service, you can also configure the Windows account to run the service with, +whether the service should be started after installation and the Windows startup behaviour: [[msi-installer-service]] image::images/msi_installer/msi_installer_service.png[] -IMPORTANT: When selecting an account to run the service with, be sure that the chosen account -has sufficient privileges to access the installation and other deployment directories chosen. +IMPORTANT: When selecting a Windows account to run the service with, be sure that the chosen account +has sufficient privileges to access the installation and other deployment directories chosen. Also +ensure the account is able to run Windows services. Common configuration settings are exposed within the Configuration section, allowing the cluster name, node name and roles to be set, in addition to memory and network settings: @@ -69,28 +69,26 @@ name, node name and roles to be set, in addition to memory and network settings: [[msi-installer-configuration]] image::images/msi_installer/msi_installer_configuration.png[] -A list of common plugins that can be downloaded and installed as -part of the installation, with the option to configure a HTTPS proxy through which to download: +A list of common plugins that can be downloaded and installed as part of the installation, with the option to configure a HTTPS proxy through which to download these plugins. + +TIP: Ensure the installation machine has access to the internet and that any corporate firewalls in place are configured to allow downloads from `artifacts.elastic.co`: [[msi-installer-selected-plugins]] image::images/msi_installer/msi_installer_selected_plugins.png[] -Upon choosing to install {xpack} plugin, an additional step allows a choice of the type of {xpack} -license to install, in addition to {security} configuration and built-in user configuration: +As of version 6.3.0, X-Pack is now https://www.elastic.co/products/x-pack/open[bundled by default]. The final step allows a choice of the type of X-Pack license to install, in addition to security configuration and built-in user configuration: [[msi-installer-xpack]] image::images/msi_installer/msi_installer_xpack.png[] -NOTE: {xpack} includes a choice of a Trial or Basic license for 30 days. After that, you can obtain one of the -https://www.elastic.co/subscriptions[available subscriptions] or {ref}/security-settings.html[disable Security]. -The Basic license is free and includes the https://www.elastic.co/products/x-pack/monitoring[Monitoring] extension. +NOTE: X-Pack includes a choice of a Trial or Basic license. A Trial license is valid for 30 days, after which you can obtain one of the available subscriptions. The Basic license is free and perpetual. Consult the https://www.elastic.co/subscriptions[available subscriptions] for further details on which features are available under which license. -After clicking the install button, the installer will begin installation: +After clicking the install button, the installation will begin: [[msi-installer-installing]] image::images/msi_installer/msi_installer_installing.png[] -and will indicate when it has been successfully installed: +...and will indicate when it has been successfully installed: [[msi-installer-success]] image::images/msi_installer/msi_installer_success.png[] @@ -107,7 +105,7 @@ then running: msiexec.exe /i elasticsearch-{version}.msi /qn -------------------------------------------- -By default, msiexec does not wait for the installation process to complete, since it runs in the +By default, `msiexec.exe` does not wait for the installation process to complete, since it runs in the Windows subsystem. To wait on the process to finish and ensure that `%ERRORLEVEL%` is set accordingly, it is recommended to use `start /wait` to create a process and wait for it to exit @@ -132,13 +130,13 @@ Supported Windows Installer command line arguments can be viewed using msiexec.exe /help -------------------------------------------- -or by consulting the https://msdn.microsoft.com/en-us/library/windows/desktop/aa367988(v=vs.85).aspx[Windows Installer SDK Command-Line Options]. +...or by consulting the https://msdn.microsoft.com/en-us/library/windows/desktop/aa367988(v=vs.85).aspx[Windows Installer SDK Command-Line Options]. [[msi-command-line-options]] ==== Command line options All settings exposed within the GUI are also available as command line arguments (referred to -as _properties_ within Windows Installer documentation) that can be passed to msiexec: +as _properties_ within Windows Installer documentation) that can be passed to `msiexec.exe`: [horizontal] `INSTALLDIR`:: @@ -282,47 +280,46 @@ as _properties_ within Windows Installer documentation) that can be passed to ms `XPACKLICENSE`:: - When installing {xpack} plugin, the type of license to install, - either `Basic` or `Trial`. Defaults to `Basic` + The type of X-Pack license to install, either `Basic` or `Trial`. Defaults to `Basic` `XPACKSECURITYENABLED`:: - When installing {xpack} plugin with a `Trial` license, whether {security} should be enabled. + When installing with a `Trial` license, whether X-Pack Security should be enabled. Defaults to `true` `BOOTSTRAPPASSWORD`:: - When installing {xpack} plugin with a `Trial` license and {security} enabled, the password to + When installing with a `Trial` license and X-Pack Security enabled, the password to used to bootstrap the cluster and persisted as the `bootstrap.password` setting in the keystore. Defaults to a randomized value. `SKIPSETTINGPASSWORDS`:: - When installing {xpack} plugin with a `Trial` license and {security} enabled, whether the + 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`. Defaults to `false` `ELASTICUSERPASSWORD`:: - When installing {xpack} plugin with a `Trial` license and {security} enabled, the password + When installing with a `Trial` license and X-Pack Security enabled, the password to use for the built-in user `elastic`. Defaults to `""` `KIBANAUSERPASSWORD`:: - When installing {xpack} plugin with a `Trial` license and {security} enabled, the password + When installing with a `Trial` license and X-Pack Security enabled, the password to use for the built-in user `kibana`. Defaults to `""` `LOGSTASHSYSTEMUSERPASSWORD`:: - When installing {xpack} plugin with a `Trial` license and {security} enabled, the password + When installing with a `Trial` license and X-Pack Security enabled, the password to use for the built-in user `logstash_system`. Defaults to `""` To pass a value, simply append the property name and value using the format `=""` to -the installation command. For example, to use a different installation directory to the default one and to install https://www.elastic.co/products/x-pack[{xpack}]: +the installation command. For example, to use a different installation directory to the default one and to install https://www.elastic.co/products/x-pack[X-Pack]: ["source","sh",subs="attributes,callouts"] -------------------------------------------- -start /wait msiexec.exe /i elasticsearch-{version}.msi /qn INSTALLDIR="C:\Custom Install Directory" PLUGINS="x-pack" +start /wait msiexec.exe /i elasticsearch-{version}.msi /qn INSTALLDIR="C:\Custom Install Directory\{version}" PLUGINS="x-pack" -------------------------------------------- Consult the https://msdn.microsoft.com/en-us/library/windows/desktop/aa367988(v=vs.85).aspx[Windows Installer SDK Command-Line Options] @@ -330,9 +327,10 @@ for additional rules related to values containing quotation marks. ifdef::include-xpack[] [[msi-installer-enable-indices]] -==== Enable automatic creation of {xpack} indices +==== Enable automatic creation of X-Pack indices + -{xpack} will try to automatically create a number of indices within {es}. +X-Pack will try to automatically create a number of indices within Elasticsearch. include::xpack-indices.asciidoc[] endif::include-xpack[] @@ -344,7 +342,7 @@ include::msi-windows-start.asciidoc[] ==== Configuring Elasticsearch on the command line Elasticsearch loads its configuration from the `%ES_PATH_CONF%\elasticsearch.yml` -file by default. The format of this config file is explained in +file by default. The format of this config file is explained in <>. Any settings that can be specified in the config file can also be specified on @@ -393,10 +391,11 @@ with PowerShell: [source,powershell] -------------------------------------------- -Get-Service Elasticsearch | Stop-Service | Start-Service +Get-Service Elasticsearch | Stop-Service +Get-Service Elasticsearch | Start-Service -------------------------------------------- -Changes can be made to jvm.options and elasticsearch.yml configuration files to configure the +Changes can be made to `jvm.options` and `elasticsearch.yml` configuration files to configure the service after installation. Most changes (like JVM settings) will require a restart of the service in order to take effect. @@ -404,16 +403,16 @@ service in order to take effect. ==== Upgrade using the graphical user interface (GUI) The `.msi` package supports upgrading an installed version of Elasticsearch to a newer -version of Elasticsearch. The upgrade process through the GUI handles upgrading all +version. The upgrade process through the GUI handles upgrading all installed plugins as well as retaining both your data and configuration. -Downloading and clicking on a newer version of the `.msi` package will launch the GUI wizard. -The first step will list the read only properties from the previous installation: +Downloading and double-clicking on a newer version of the `.msi` package will launch the GUI wizard. +The first step will list the read-only properties from the previous installation: [[msi-installer-upgrade-notice]] image::images/msi_installer/msi_installer_upgrade_notice.png[] -The following configuration step allows certain configuration options to be changed: +The next step allows certain configuration options to be changed: [[msi-installer-upgrade-configuration]] image::images/msi_installer/msi_installer_upgrade_configuration.png[] @@ -434,11 +433,11 @@ The `.msi` can also upgrade Elasticsearch using the command line. A command line upgrade requires passing the **same** command line properties as used at first install time; the Windows Installer does not remember these properties. -For example, if you originally installed with the command line options `PLUGINS="x-pack"` and +For example, if you originally installed with the command line options `PLUGINS="ingest-geoip"` and `LOCKMEMORY="true"`, then you must pass these same values when performing an upgrade from the command line. -The **exception** to this is `INSTALLDIR` (if originally specified), which must be a different directory to the +The **exception** to this is the `INSTALLDIR` parameter (if originally specified), which must be a different directory to the current installation. If setting `INSTALLDIR`, the final directory in the path **must** be the version of Elasticsearch e.g. @@ -466,9 +465,8 @@ start /wait msiexec.exe /i elasticsearch-{version}.msi /qn /l upgrade.log The `.msi` package handles uninstallation of all directories and files added as part of installation. -WARNING: Uninstallation will remove **all** directories and their contents created as part of -installation, **including data within the data directory**. If you wish to retain your data upon -uninstallation, it is recommended that you make a copy of the data directory before uninstallation. +WARNING: Uninstallation will remove **all** contents created as part of +installation, **except for data, config or logs directories**. It is recommended that you make a copy of your data directory before upgrading or consider using the snapshot API. MSI installer packages do not provide a GUI for uninstallation. An installed program can be uninstalled by pressing the Windows key and typing `add or remove programs` to open the system settings. From f29f0af7bca1011788718f7f9e8e88136cc6dfa5 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Wed, 29 Aug 2018 09:53:04 +0300 Subject: [PATCH 205/283] Consider multi release jars when running third party audit (#33206) Exclude classes meant for newer versions than what we are auditing against, those classes won't be found. There's no reason to exclude JDK classes from newer versions, with this PR, we will not extract them in the first place. --- .../gradle/precommit/PrecommitTasks.groovy | 1 + .../gradle/precommit/ThirdPartyAuditTask.java | 21 +++++++++++++++++++ server/build.gradle | 15 ------------- test/logger-usage/build.gradle | 21 +------------------ x-pack/plugin/sql/sql-action/build.gradle | 21 +------------------ 5 files changed, 24 insertions(+), 55 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index d82302c84742..60469622484e 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -87,6 +87,7 @@ class PrecommitTasks { dependsOn(buildResources) signatureFile = buildResources.copy("forbidden/third-party-audit.txt") javaHome = project.runtimeJavaHome + targetCompatibility = project.runtimeJavaVersion } return thirdPartyAuditTask } diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java index d1939d5c6526..7e4766ada654 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ThirdPartyAuditTask.java @@ -23,6 +23,7 @@ import org.elasticsearch.test.NamingConventionsCheck; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; +import org.gradle.api.JavaVersion; import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.Input; @@ -66,6 +67,17 @@ public class ThirdPartyAuditTask extends DefaultTask { private String javaHome; + private JavaVersion targetCompatibility; + + @Input + public JavaVersion getTargetCompatibility() { + return targetCompatibility; + } + + public void setTargetCompatibility(JavaVersion targetCompatibility) { + this.targetCompatibility = targetCompatibility; + } + @InputFiles public Configuration getForbiddenAPIsConfiguration() { return getProject().getConfigurations().getByName("forbiddenApisCliJar"); @@ -157,10 +169,19 @@ public void runThirdPartyAudit() throws IOException { private void extractJars(FileCollection jars) { File jarExpandDir = getJarExpandDir(); + // We need to clean up to make sure old dependencies don't linger + getProject().delete(jarExpandDir); jars.forEach(jar -> getProject().copy(spec -> { spec.from(getProject().zipTree(jar)); spec.into(jarExpandDir); + // Exclude classes for multi release jars above target + for (int i = Integer.parseInt(targetCompatibility.getMajorVersion()) + 1; + i <= Integer.parseInt(JavaVersion.VERSION_HIGHER.getMajorVersion()); + i++ + ) { + spec.exclude("META-INF/versions/" + i + "/**"); + } }) ); } diff --git a/server/build.gradle b/server/build.gradle index aaef6d87e617..edc3f427dfda 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -304,21 +304,6 @@ thirdPartyAudit.excludes = [ 'com.google.common.geometry.S2LatLng', ] -if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { - thirdPartyAudit.excludes += [ - // Used by Log4J 2.11.1 - 'java.io.ObjectInputFilter', - 'java.io.ObjectInputFilter$Config', - 'java.io.ObjectInputFilter$FilterInfo', - 'java.io.ObjectInputFilter$Status', - // added in 9 - 'java.lang.ProcessHandle', - 'java.lang.StackWalker', - 'java.lang.StackWalker$Option', - 'java.lang.StackWalker$StackFrame' - ] -} - if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { thirdPartyAudit.excludes += ['javax.xml.bind.DatatypeConverter'] } diff --git a/test/logger-usage/build.gradle b/test/logger-usage/build.gradle index 2da906564143..6ab975fd42eb 100644 --- a/test/logger-usage/build.gradle +++ b/test/logger-usage/build.gradle @@ -42,23 +42,4 @@ thirdPartyAudit.excludes = [ 'org.osgi.framework.SynchronousBundleListener', 'org.osgi.framework.wiring.BundleWire', 'org.osgi.framework.wiring.BundleWiring' -] - -if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { - // Used by Log4J 2.11.1 - thirdPartyAudit.excludes += [ - 'java.io.ObjectInputFilter', - 'java.io.ObjectInputFilter$Config', - 'java.io.ObjectInputFilter$FilterInfo', - 'java.io.ObjectInputFilter$Status' - ] -} - -if (project.runtimeJavaVersion == JavaVersion.VERSION_1_8) { - thirdPartyAudit.excludes += [ - 'java.lang.ProcessHandle', - 'java.lang.StackWalker', - 'java.lang.StackWalker$Option', - 'java.lang.StackWalker$StackFrame' - ] -} \ No newline at end of file +] \ No newline at end of file diff --git a/x-pack/plugin/sql/sql-action/build.gradle b/x-pack/plugin/sql/sql-action/build.gradle index ee99c36b9062..9e53c36bbf60 100644 --- a/x-pack/plugin/sql/sql-action/build.gradle +++ b/x-pack/plugin/sql/sql-action/build.gradle @@ -138,23 +138,4 @@ thirdPartyAudit.excludes = [ 'org.zeromq.ZMQ$Context', 'org.zeromq.ZMQ$Socket', 'org.zeromq.ZMQ' -] - -if (project.runtimeJavaVersion <= JavaVersion.VERSION_1_8) { - // Used by Log4J 2.11.1 - thirdPartyAudit.excludes += [ - 'java.io.ObjectInputFilter', - 'java.io.ObjectInputFilter$Config', - 'java.io.ObjectInputFilter$FilterInfo', - 'java.io.ObjectInputFilter$Status' - ] -} - -if (project.runtimeJavaVersion == JavaVersion.VERSION_1_8) { - thirdPartyAudit.excludes += [ - 'java.lang.ProcessHandle', - 'java.lang.StackWalker', - 'java.lang.StackWalker$Option', - 'java.lang.StackWalker$StackFrame' - ] -} \ No newline at end of file +] \ No newline at end of file From 48b388ce82d0fb43709b7809ce3166b17b1f16d0 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Wed, 29 Aug 2018 10:00:16 +0200 Subject: [PATCH 206/283] Core: Add java time xcontent serializers (#33120) This ensures that the java time class exposed by painless have proper serialization/string representations. Closes #31853 --- .../test/painless/50_script_doc_values.yml | 4 +- .../common/time/DateFormatters.java | 41 +++++- .../XContentElasticsearchExtension.java | 39 ++++++ .../common/xcontent/BaseXContentTestCase.java | 124 +++++++++++++++++- 4 files changed, 203 insertions(+), 5 deletions(-) diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/50_script_doc_values.yml b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/50_script_doc_values.yml index 4c3c204d2d9c..617b8df61b6b 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/50_script_doc_values.yml +++ b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/50_script_doc_values.yml @@ -95,7 +95,7 @@ setup: field: script: source: "doc.date.get(0)" - - match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' } + - match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' } - do: search: @@ -104,7 +104,7 @@ setup: field: script: source: "doc.date.value" - - match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' } + - match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' } --- "geo_point": diff --git a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java index 37efff5a0beb..4017e43b071f 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java @@ -48,6 +48,7 @@ import static java.time.temporal.ChronoField.MILLI_OF_SECOND; import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; public class DateFormatters { @@ -81,7 +82,7 @@ public class DateFormatters { .appendFraction(MILLI_OF_SECOND, 3, 3, true) .optionalEnd() .optionalStart() - .appendOffset("+HHmm", "Z") + .appendZoneOrOffsetId() .optionalEnd() .optionalEnd() .toFormatter(Locale.ROOT); @@ -95,7 +96,7 @@ public class DateFormatters { .appendFraction(MILLI_OF_SECOND, 3, 3, true) .optionalEnd() .optionalStart() - .appendZoneOrOffsetId() + .appendOffset("+HHmm", "Z") .optionalEnd() .optionalEnd() .toFormatter(Locale.ROOT); @@ -106,6 +107,40 @@ public class DateFormatters { private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME = new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_2); + private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1 = new DateTimeFormatterBuilder() + .append(STRICT_YEAR_MONTH_DAY_FORMATTER) + .optionalStart() + .appendLiteral('T') + .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER) + .optionalStart() + .appendFraction(NANO_OF_SECOND, 3, 9, true) + .optionalEnd() + .optionalStart() + .appendZoneOrOffsetId() + .optionalEnd() + .optionalEnd() + .toFormatter(Locale.ROOT); + + private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2 = new DateTimeFormatterBuilder() + .append(STRICT_YEAR_MONTH_DAY_FORMATTER) + .optionalStart() + .appendLiteral('T') + .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER) + .optionalStart() + .appendFraction(NANO_OF_SECOND, 3, 9, true) + .optionalEnd() + .optionalStart() + .appendOffset("+HHmm", "Z") + .optionalEnd() + .optionalEnd() + .toFormatter(Locale.ROOT); + + /** + * Returns a generic ISO datetime parser where the date is mandatory and the time is optional with nanosecond resolution. + */ + private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME_NANOS = + new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2); + ///////////////////////////////////////// // // BEGIN basic time formatters @@ -1326,6 +1361,8 @@ public static CompoundDateTimeFormatter forPattern(String input, Locale locale) return STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS; } else if ("strictDateOptionalTime".equals(input) || "strict_date_optional_time".equals(input)) { return STRICT_DATE_OPTIONAL_TIME; + } else if ("strictDateOptionalTimeNanos".equals(input) || "strict_date_optional_time_nanos".equals(input)) { + return STRICT_DATE_OPTIONAL_TIME_NANOS; } else if ("strictDateTime".equals(input) || "strict_date_time".equals(input)) { return STRICT_DATE_TIME; } else if ("strictDateTimeNoMillis".equals(input) || "strict_date_time_no_millis".equals(input)) { diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java b/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java index 38abe90ad46d..5793bcf8a0e4 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java @@ -21,6 +21,8 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.joda.time.DateTime; @@ -33,6 +35,19 @@ import org.joda.time.tz.CachedDateTimeZone; import org.joda.time.tz.FixedDateTimeZone; +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -49,6 +64,9 @@ public class XContentElasticsearchExtension implements XContentBuilderExtension { public static final DateTimeFormatter DEFAULT_DATE_PRINTER = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC); + public static final CompoundDateTimeFormatter DEFAULT_FORMATTER = DateFormatters.forPattern("strict_date_optional_time_nanos"); + public static final CompoundDateTimeFormatter LOCAL_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSS"); + public static final CompoundDateTimeFormatter OFFSET_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSSZZZZZ"); @Override public Map, XContentBuilder.Writer> getXContentWriters() { @@ -62,6 +80,19 @@ public Map, XContentBuilder.Writer> getXContentWriters() { writers.put(MutableDateTime.class, XContentBuilder::timeValue); writers.put(DateTime.class, XContentBuilder::timeValue); writers.put(TimeValue.class, (b, v) -> b.value(v.toString())); + writers.put(ZonedDateTime.class, XContentBuilder::timeValue); + writers.put(OffsetDateTime.class, XContentBuilder::timeValue); + writers.put(OffsetTime.class, XContentBuilder::timeValue); + writers.put(java.time.Instant.class, XContentBuilder::timeValue); + writers.put(LocalDateTime.class, XContentBuilder::timeValue); + writers.put(LocalDate.class, XContentBuilder::timeValue); + writers.put(LocalTime.class, XContentBuilder::timeValue); + writers.put(DayOfWeek.class, (b, v) -> b.value(v.toString())); + writers.put(Month.class, (b, v) -> b.value(v.toString())); + writers.put(MonthDay.class, (b, v) -> b.value(v.toString())); + writers.put(Year.class, (b, v) -> b.value(v.toString())); + writers.put(Duration.class, (b, v) -> b.value(v.toString())); + writers.put(Period.class, (b, v) -> b.value(v.toString())); writers.put(BytesReference.class, (b, v) -> { if (v == null) { @@ -102,6 +133,14 @@ public Map, Function> getDateTransformers() { transformers.put(Calendar.class, d -> DEFAULT_DATE_PRINTER.print(((Calendar) d).getTimeInMillis())); transformers.put(GregorianCalendar.class, d -> DEFAULT_DATE_PRINTER.print(((Calendar) d).getTimeInMillis())); transformers.put(Instant.class, d -> DEFAULT_DATE_PRINTER.print((Instant) d)); + transformers.put(ZonedDateTime.class, d -> DEFAULT_FORMATTER.format((ZonedDateTime) d)); + transformers.put(OffsetDateTime.class, d -> DEFAULT_FORMATTER.format((OffsetDateTime) d)); + transformers.put(OffsetTime.class, d -> OFFSET_TIME_FORMATTER.format((OffsetTime) d)); + transformers.put(LocalDateTime.class, d -> DEFAULT_FORMATTER.format((LocalDateTime) d)); + transformers.put(java.time.Instant.class, + d -> DEFAULT_FORMATTER.format(ZonedDateTime.ofInstant((java.time.Instant) d, ZoneOffset.UTC))); + transformers.put(LocalDate.class, d -> ((LocalDate) d).toString()); + transformers.put(LocalTime.class, d -> LOCAL_TIME_FORMATTER.format((LocalTime) d)); return transformers; } } diff --git a/server/src/test/java/org/elasticsearch/common/xcontent/BaseXContentTestCase.java b/server/src/test/java/org/elasticsearch/common/xcontent/BaseXContentTestCase.java index 170ea6cf9313..3fb5f5996be7 100644 --- a/server/src/test/java/org/elasticsearch/common/xcontent/BaseXContentTestCase.java +++ b/server/src/test/java/org/elasticsearch/common/xcontent/BaseXContentTestCase.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; - import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.Constants; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -51,6 +50,19 @@ import java.io.IOException; import java.math.BigInteger; import java.nio.file.Path; +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -459,6 +471,116 @@ public void testCalendar() throws Exception { .endObject()); } + public void testJavaTime() throws Exception { + final ZonedDateTime d1 = ZonedDateTime.of(2016, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + + // ZonedDateTime + assertResult("{'date':null}", () -> builder().startObject().timeField("date", (ZonedDateTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((ZonedDateTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date", (ZonedDateTime) null).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().timeField("d1", d1).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1").timeValue(d1).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1).endObject()); + + // Instant + assertResult("{'date':null}", () -> builder().startObject().timeField("date", (java.time.Instant) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((java.time.Instant) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date", (java.time.Instant) null).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().timeField("d1", d1.toInstant()).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1").timeValue(d1.toInstant()).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1.toInstant()).endObject()); + + // LocalDateTime (no time zone) + assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalDateTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalDateTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalDateTime) null).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000'}", + () -> builder().startObject().timeField("d1", d1.toLocalDateTime()).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000'}", + () -> builder().startObject().field("d1").timeValue(d1.toLocalDateTime()).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000'}", () -> builder().startObject().field("d1", d1.toLocalDateTime()).endObject()); + + // LocalDate (no time, no time zone) + assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalDate) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalDate) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalDate) null).endObject()); + assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().timeField("d1", d1.toLocalDate()).endObject()); + assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().field("d1").timeValue(d1.toLocalDate()).endObject()); + assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().field("d1", d1.toLocalDate()).endObject()); + + // LocalTime (no date, no time zone) + assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalTime) null).endObject()); + assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().timeField("d1", d1.toLocalTime()).endObject()); + assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().field("d1").timeValue(d1.toLocalTime()).endObject()); + assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().field("d1", d1.toLocalTime()).endObject()); + final ZonedDateTime d2 = ZonedDateTime.of(2016, 1, 1, 7, 59, 23, 123_000_000, ZoneOffset.UTC); + assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().timeField("d1", d2.toLocalTime()).endObject()); + assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().field("d1").timeValue(d2.toLocalTime()).endObject()); + assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().field("d1", d2.toLocalTime()).endObject()); + + // OffsetDateTime + assertResult("{'date':null}", () -> builder().startObject().timeField("date", (OffsetDateTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((OffsetDateTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date", (OffsetDateTime) null).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1.toOffsetDateTime()).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", + () -> builder().startObject().timeField("d1", d1.toOffsetDateTime()).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", + () -> builder().startObject().field("d1").timeValue(d1.toOffsetDateTime()).endObject()); + // also test with a date that has a real offset + OffsetDateTime offsetDateTime = d1.withZoneSameLocal(ZoneOffset.ofHours(5)).toOffsetDateTime(); + assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}", () -> builder().startObject().field("d1", offsetDateTime).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}", () -> builder().startObject().timeField("d1", offsetDateTime).endObject()); + assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}", + () -> builder().startObject().field("d1").timeValue(offsetDateTime).endObject()); + + // OffsetTime + assertResult("{'date':null}", () -> builder().startObject().timeField("date", (OffsetTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((OffsetTime) null).endObject()); + assertResult("{'date':null}", () -> builder().startObject().field("date", (OffsetTime) null).endObject()); + final OffsetTime offsetTime = d2.toOffsetDateTime().toOffsetTime(); + assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().timeField("o", offsetTime).endObject()); + assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().field("o").timeValue(offsetTime).endObject()); + assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().field("o", offsetTime).endObject()); + // also test with a date that has a real offset + final OffsetTime zonedOffsetTime = offsetTime.withOffsetSameLocal(ZoneOffset.ofHours(5)); + assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().timeField("o", zonedOffsetTime).endObject()); + assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().field("o").timeValue(zonedOffsetTime).endObject()); + assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().field("o", zonedOffsetTime).endObject()); + + // DayOfWeek enum, not a real time value, but might be used in scripts + assertResult("{'dayOfWeek':null}", () -> builder().startObject().field("dayOfWeek", (DayOfWeek) null).endObject()); + DayOfWeek dayOfWeek = randomFrom(DayOfWeek.values()); + assertResult("{'dayOfWeek':'" + dayOfWeek + "'}", () -> builder().startObject().field("dayOfWeek", dayOfWeek).endObject()); + + // Month + Month month = randomFrom(Month.values()); + assertResult("{'m':null}", () -> builder().startObject().field("m", (Month) null).endObject()); + assertResult("{'m':'" + month + "'}", () -> builder().startObject().field("m", month).endObject()); + + // MonthDay + MonthDay monthDay = MonthDay.of(month, randomIntBetween(1, 28)); + assertResult("{'m':null}", () -> builder().startObject().field("m", (MonthDay) null).endObject()); + assertResult("{'m':'" + monthDay + "'}", () -> builder().startObject().field("m", monthDay).endObject()); + + // Year + Year year = Year.of(randomIntBetween(0, 2300)); + assertResult("{'y':null}", () -> builder().startObject().field("y", (Year) null).endObject()); + assertResult("{'y':'" + year + "'}", () -> builder().startObject().field("y", year).endObject()); + + // Duration + Duration duration = Duration.ofSeconds(randomInt(100000)); + assertResult("{'d':null}", () -> builder().startObject().field("d", (Duration) null).endObject()); + assertResult("{'d':'" + duration + "'}", () -> builder().startObject().field("d", duration).endObject()); + + // Period + Period period = Period.ofDays(randomInt(1000)); + assertResult("{'p':null}", () -> builder().startObject().field("p", (Period) null).endObject()); + assertResult("{'p':'" + period + "'}", () -> builder().startObject().field("p", period).endObject()); + } + public void testGeoPoint() throws Exception { assertResult("{'geo':null}", () -> builder().startObject().field("geo", (GeoPoint) null).endObject()); assertResult("{'geo':{'lat':52.4267578125,'lon':13.271484375}}", () -> builder() From f690b492e7855609d59f836b8d5aef317b131ff7 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 29 Aug 2018 11:03:10 +0200 Subject: [PATCH 207/283] INGEST: Add Pipeline Processor (#32473) * INGEST: Add Pipeline Processor * Adds Processor capable of invoking other pipelines * Closes #31842 --- .../ingest/common/IngestCommonPlugin.java | 1 + .../ingest/common/PipelineProcessor.java | 74 +++++++++++ .../ingest/common/PipelineProcessorTests.java | 115 ++++++++++++++++++ .../test/ingest/210_pipeline_processor.yml | 113 +++++++++++++++++ .../elasticsearch/ingest/IngestDocument.java | 19 +++ .../elasticsearch/ingest/IngestService.java | 2 +- .../org/elasticsearch/ingest/Processor.java | 10 +- 7 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/PipelineProcessor.java create mode 100644 modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/PipelineProcessorTests.java create mode 100644 modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_pipeline_processor.yml diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java index b85bf085dabb..1ed8b6058e6c 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java @@ -82,6 +82,7 @@ public Map getProcessors(Processor.Parameters paramet processors.put(KeyValueProcessor.TYPE, new KeyValueProcessor.Factory()); processors.put(URLDecodeProcessor.TYPE, new URLDecodeProcessor.Factory()); processors.put(BytesProcessor.TYPE, new BytesProcessor.Factory()); + processors.put(PipelineProcessor.TYPE, new PipelineProcessor.Factory(parameters.ingestService)); processors.put(DissectProcessor.TYPE, new DissectProcessor.Factory()); return Collections.unmodifiableMap(processors); } diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/PipelineProcessor.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/PipelineProcessor.java new file mode 100644 index 000000000000..77ffdb919193 --- /dev/null +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/PipelineProcessor.java @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.ingest.common; + +import java.util.Map; +import org.elasticsearch.ingest.AbstractProcessor; +import org.elasticsearch.ingest.ConfigurationUtils; +import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.ingest.IngestService; +import org.elasticsearch.ingest.Pipeline; +import org.elasticsearch.ingest.Processor; + +public class PipelineProcessor extends AbstractProcessor { + + public static final String TYPE = "pipeline"; + + private final String pipelineName; + + private final IngestService ingestService; + + private PipelineProcessor(String tag, String pipelineName, IngestService ingestService) { + super(tag); + this.pipelineName = pipelineName; + this.ingestService = ingestService; + } + + @Override + public void execute(IngestDocument ingestDocument) throws Exception { + Pipeline pipeline = ingestService.getPipeline(pipelineName); + if (pipeline == null) { + throw new IllegalStateException("Pipeline processor configured for non-existent pipeline [" + pipelineName + ']'); + } + ingestDocument.executePipeline(pipeline); + } + + @Override + public String getType() { + return TYPE; + } + + public static final class Factory implements Processor.Factory { + + private final IngestService ingestService; + + public Factory(IngestService ingestService) { + this.ingestService = ingestService; + } + + @Override + public PipelineProcessor create(Map registry, String processorTag, + Map config) throws Exception { + String pipeline = + ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "pipeline"); + return new PipelineProcessor(processorTag, pipeline, ingestService); + } + } +} diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/PipelineProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/PipelineProcessorTests.java new file mode 100644 index 000000000000..5baf3cf822d7 --- /dev/null +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/PipelineProcessorTests.java @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.ingest.common; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ingest.CompoundProcessor; +import org.elasticsearch.ingest.IngestDocument; +import org.elasticsearch.ingest.IngestService; +import org.elasticsearch.ingest.Pipeline; +import org.elasticsearch.ingest.Processor; +import org.elasticsearch.ingest.RandomDocumentPicks; +import org.elasticsearch.test.ESTestCase; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PipelineProcessorTests extends ESTestCase { + + public void testExecutesPipeline() throws Exception { + String pipelineId = "pipeline"; + IngestService ingestService = mock(IngestService.class); + CompletableFuture invoked = new CompletableFuture<>(); + IngestDocument testIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), new HashMap<>()); + Pipeline pipeline = new Pipeline( + pipelineId, null, null, + new CompoundProcessor(new Processor() { + @Override + public void execute(final IngestDocument ingestDocument) throws Exception { + invoked.complete(ingestDocument); + } + + @Override + public String getType() { + return null; + } + + @Override + public String getTag() { + return null; + } + }) + ); + when(ingestService.getPipeline(pipelineId)).thenReturn(pipeline); + PipelineProcessor.Factory factory = new PipelineProcessor.Factory(ingestService); + Map config = new HashMap<>(); + config.put("pipeline", pipelineId); + factory.create(Collections.emptyMap(), null, config).execute(testIngestDocument); + assertEquals(testIngestDocument, invoked.get()); + } + + public void testThrowsOnMissingPipeline() throws Exception { + IngestService ingestService = mock(IngestService.class); + IngestDocument testIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), new HashMap<>()); + PipelineProcessor.Factory factory = new PipelineProcessor.Factory(ingestService); + Map config = new HashMap<>(); + config.put("pipeline", "missingPipelineId"); + IllegalStateException e = expectThrows( + IllegalStateException.class, + () -> factory.create(Collections.emptyMap(), null, config).execute(testIngestDocument) + ); + assertEquals( + "Pipeline processor configured for non-existent pipeline [missingPipelineId]", e.getMessage() + ); + } + + public void testThrowsOnRecursivePipelineInvocations() throws Exception { + String innerPipelineId = "inner"; + String outerPipelineId = "outer"; + IngestService ingestService = mock(IngestService.class); + IngestDocument testIngestDocument = RandomDocumentPicks.randomIngestDocument(random(), new HashMap<>()); + Map outerConfig = new HashMap<>(); + outerConfig.put("pipeline", innerPipelineId); + PipelineProcessor.Factory factory = new PipelineProcessor.Factory(ingestService); + Pipeline outer = new Pipeline( + outerPipelineId, null, null, + new CompoundProcessor(factory.create(Collections.emptyMap(), null, outerConfig)) + ); + Map innerConfig = new HashMap<>(); + innerConfig.put("pipeline", outerPipelineId); + Pipeline inner = new Pipeline( + innerPipelineId, null, null, + new CompoundProcessor(factory.create(Collections.emptyMap(), null, innerConfig)) + ); + when(ingestService.getPipeline(outerPipelineId)).thenReturn(outer); + when(ingestService.getPipeline(innerPipelineId)).thenReturn(inner); + outerConfig.put("pipeline", innerPipelineId); + ElasticsearchException e = expectThrows( + ElasticsearchException.class, + () -> factory.create(Collections.emptyMap(), null, outerConfig).execute(testIngestDocument) + ); + assertEquals( + "Recursive invocation of pipeline [inner] detected.", e.getRootCause().getMessage() + ); + } +} diff --git a/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_pipeline_processor.yml b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_pipeline_processor.yml new file mode 100644 index 000000000000..355ba2d42104 --- /dev/null +++ b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_pipeline_processor.yml @@ -0,0 +1,113 @@ +--- +teardown: +- do: + ingest.delete_pipeline: + id: "inner" + ignore: 404 + +- do: + ingest.delete_pipeline: + id: "outer" + ignore: 404 + +--- +"Test Pipeline Processor with Simple Inner Pipeline": +- do: + ingest.put_pipeline: + id: "inner" + body: > + { + "description" : "inner pipeline", + "processors" : [ + { + "set" : { + "field": "foo", + "value": "bar" + } + }, + { + "set" : { + "field": "baz", + "value": "blub" + } + } + ] + } +- match: { acknowledged: true } + +- do: + ingest.put_pipeline: + id: "outer" + body: > + { + "description" : "outer pipeline", + "processors" : [ + { + "pipeline" : { + "pipeline": "inner" + } + } + ] + } +- match: { acknowledged: true } + +- do: + index: + index: test + type: test + id: 1 + pipeline: "outer" + body: {} + +- do: + get: + index: test + type: test + id: 1 +- match: { _source.foo: "bar" } +- match: { _source.baz: "blub" } + +--- +"Test Pipeline Processor with Circular Pipelines": +- do: + ingest.put_pipeline: + id: "outer" + body: > + { + "description" : "outer pipeline", + "processors" : [ + { + "pipeline" : { + "pipeline": "inner" + } + } + ] + } +- match: { acknowledged: true } + +- do: + ingest.put_pipeline: + id: "inner" + body: > + { + "description" : "inner pipeline", + "processors" : [ + { + "pipeline" : { + "pipeline": "outer" + } + } + ] + } +- match: { acknowledged: true } + +- do: + catch: /illegal_state_exception/ + index: + index: test + type: test + id: 1 + pipeline: "outer" + body: {} +- match: { error.root_cause.0.type: "exception" } +- match: { error.root_cause.0.reason: "java.lang.IllegalArgumentException: java.lang.IllegalStateException: Recursive invocation of pipeline [inner] detected." } diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java index aad55e12ceff..e218168eeb7b 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java @@ -19,6 +19,9 @@ package org.elasticsearch.ingest; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; import org.elasticsearch.common.Strings; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.mapper.IdFieldMapper; @@ -55,6 +58,9 @@ public final class IngestDocument { private final Map sourceAndMetadata; private final Map ingestMetadata; + // Contains all pipelines that have been executed for this document + private final Set executedPipelines = Collections.newSetFromMap(new IdentityHashMap<>()); + public IngestDocument(String index, String type, String id, String routing, Long version, VersionType versionType, Map source) { this.sourceAndMetadata = new HashMap<>(); @@ -632,6 +638,19 @@ private static Object deepCopy(Object value) { } } + /** + * Executes the given pipeline with for this document unless the pipeline has already been executed + * for this document. + * @param pipeline Pipeline to execute + * @throws Exception On exception in pipeline execution + */ + public void executePipeline(Pipeline pipeline) throws Exception { + if (this.executedPipelines.add(pipeline) == false) { + throw new IllegalStateException("Recursive invocation of pipeline [" + pipeline.getId() + "] detected."); + } + pipeline.execute(this); + } + @Override public boolean equals(Object obj) { if (obj == this) { return true; } diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index ae3416ef3b06..eee14e958699 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -92,7 +92,7 @@ public IngestService(ClusterService clusterService, ThreadPool threadPool, threadPool.getThreadContext(), threadPool::relativeTimeInMillis, (delay, command) -> threadPool.schedule( TimeValue.timeValueMillis(delay), ThreadPool.Names.GENERIC, command - ) + ), this ) ); this.threadPool = threadPool; diff --git a/server/src/main/java/org/elasticsearch/ingest/Processor.java b/server/src/main/java/org/elasticsearch/ingest/Processor.java index c318d478814d..15a26d374919 100644 --- a/server/src/main/java/org/elasticsearch/ingest/Processor.java +++ b/server/src/main/java/org/elasticsearch/ingest/Processor.java @@ -97,22 +97,26 @@ class Parameters { * instances that have run prior to in ingest. */ public final ThreadContext threadContext; - + public final LongSupplier relativeTimeSupplier; - + + public final IngestService ingestService; + /** * Provides scheduler support */ public final BiFunction> scheduler; public Parameters(Environment env, ScriptService scriptService, AnalysisRegistry analysisRegistry, ThreadContext threadContext, - LongSupplier relativeTimeSupplier, BiFunction> scheduler) { + LongSupplier relativeTimeSupplier, BiFunction> scheduler, + IngestService ingestService) { this.env = env; this.scriptService = scriptService; this.threadContext = threadContext; this.analysisRegistry = analysisRegistry; this.relativeTimeSupplier = relativeTimeSupplier; this.scheduler = scheduler; + this.ingestService = ingestService; } } From 8c57d4af6a824ddf9ec8e5329a8c33bac27a8ed3 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 29 Aug 2018 12:35:31 +0300 Subject: [PATCH 208/283] Parse PEM Key files leniantly (#33173) Allow for extra non-whitespace before the Header of PEM encoded key files. Resolves #33168 --- .../xpack/core/ssl/PemUtils.java | 4 +++ .../xpack/core/ssl/PemUtilsTests.java | 10 ++++++ .../certs/simple/testnode_with_bagattrs.pem | 32 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_with_bagattrs.pem diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java index a3814a76a3e6..421b30baac7b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/PemUtils.java @@ -58,6 +58,7 @@ public class PemUtils { private static final String OPENSSL_EC_FOOTER = "-----END EC PRIVATE KEY-----"; private static final String OPENSSL_EC_PARAMS_HEADER = "-----BEGIN EC PARAMETERS-----"; private static final String OPENSSL_EC_PARAMS_FOOTER = "-----END EC PARAMETERS-----"; + private static final String HEADER = "-----BEGIN"; private PemUtils() { throw new IllegalStateException("Utility class should not be instantiated"); @@ -74,6 +75,9 @@ private PemUtils() { public static PrivateKey readPrivateKey(Path keyPath, Supplier passwordSupplier) { try (BufferedReader bReader = Files.newBufferedReader(keyPath, StandardCharsets.UTF_8)) { String line = bReader.readLine(); + while (null != line && line.startsWith(HEADER) == false){ + line = bReader.readLine(); + } if (null == line) { throw new IllegalStateException("Error parsing Private Key from: " + keyPath.toString() + ". File is empty"); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/PemUtilsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/PemUtilsTests.java index b82275a88331..3134d42ce362 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/PemUtilsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/PemUtilsTests.java @@ -32,6 +32,16 @@ public void testReadPKCS8RsaKey() throws Exception { assertThat(privateKey, equalTo(key)); } + public void testReadPKCS8RsaKeyWithBagAttrs() throws Exception { + Key key = getKeyFromKeystore("RSA"); + assertThat(key, notNullValue()); + assertThat(key, instanceOf(PrivateKey.class)); + PrivateKey privateKey = PemUtils.readPrivateKey(getDataPath + ("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_with_bagattrs.pem"), ""::toCharArray); + assertThat(privateKey, notNullValue()); + assertThat(privateKey, equalTo(key)); + } + public void testReadPKCS8DsaKey() throws Exception { Key key = getKeyFromKeystore("DSA"); assertThat(key, notNullValue()); diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_with_bagattrs.pem b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_with_bagattrs.pem new file mode 100644 index 000000000000..ce8299cd070f --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode_with_bagattrs.pem @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: testnode_rsa + localKeyID: 54 69 6D 65 20 31 35 32 35 33 33 36 38 32 39 33 39 37 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDesZnVBuxbT4y7 +KtIuYx8MUq0sGQgVbxXSBG66sWDU9Qoo1HUyra0xXCONgRMBT9RjSIpk7OOC9g8q +ENNgFO179YdHVkrgJhW/tNBf+C0VAb+B79zu7SwtyH2nt9t378dmItL+sERkMiiG ++BS/O+cDz44hifDiS7Eqj/mJugAhLjWSUyD+UBObxXvUsxjryKeG3vX9mRCgAcqB +xH3PjI1i9DVaoobwMbwpE5eW2WXexOspuXnMmGfrrR6z/VmdHqe/C3rGdJOX+Y0c +yOR+/Vuzisn+nLeo/GJx2hIif8rKiNRyAdUXfx+4DLYJBN2NUbl9aP2LP6ZC8ubf +6qwhhB0XAgMBAAECggEBAKuzP6qSNfaJNTayY2/EmRHFRSP1ANiV17sgE8f6L3DC +pdypQtuaMSkXo4nc9SxTwqvyKFJ8m0ZENZj3dCJmwFyNCIqmLAD7HFW9MdRs40WJ +HYEv0aaeUyvRo6CHD74/r/w96XTZr0GZssmtyUFRDGNRyoJter7gIW9xprLcKHFr +YTmdaAXbOm5W/K3844EBouTYzYnZYWQjB3jT/g5dIic3AtLb5YfGlpaXXb74xTOU +BqY1uKonGiDCh0aXXRl2Ucyre6FWslNNy4cAAXm6/5GT6iMo7wDXQftvtyK2IszP +IFcOG6xcAaJjgZ5wvM3ch0qNhQi4vL7c4Bm5JS9meoECgYEA88ItaVrfm2osX/6/ +fA8wYxxYU5RQRyOgLuzBXoRkISynLJaLVj2gFOQxVQeUK++xK6R182RQatOJcWFT +WwmIL3CchCwnnXgPvMc51iFKY94DbdvrRatP8c5sSk7IQlpS3aVa7f7DCqexggr5 +3PYysuiLirL+n9I1oZiUxpsS6/cCgYEA6eCcDshQzb7UQfWy//BRMp7u6DDuq+54 +38kJIFsPX0/CGyWsiFYEac8VH7jaGof99j7Zuebeb50TX57ZCBEK2LaHe474ggkY +GGSoo3VWBn44A1P5ADaRGRwJ4/u79qAg0ldnyxFHWtW+Wbn11DoOg40rl+DOnFBJ +W+bWJn4az+ECgYEAzWduDt5lmLfiRs4LG4ZNFudWwq8y6o9ptsEIvRXArnfLM3Z0 +Waq6T4Bu1aD6Sf/EAuul/QAmB67TnbgOnqMsoBU7vuDaTQZT9JbI9Ni+r+Lwbs2n +tuCCEFgKxp8Wf1tPgriJJA3O2xauLNAE9x57YGk21Ry6FYD0coR5sdYRHscCgYEA +lGQM4Fw82K5RoqAwOK/T9RheYTha1v/x9ZtqjPr53/GNKQhYVhCtsCzSLFRvHhJX +EpyCLK/NRmgVWMBC2BloFmSJxd3K00bN4PxM+5mBQZFoHMR04qu8mH/vzpV0h2DG +Mm9+zZti+MFRi0CwNz2248T4ed8LeKaARS1LhxTQEkECgYBFsPNkfGWyP4zsgzFs +3tMgXnIgl3Lh+vnEIzVakASf3RZrSucJhA713u5L9YB64wPdVJp4YZIoEmHebP9J +Jt1f9ghcWk6ffUVBQJPmWuRbB/BU8SI+kgtf50Jnizbfm5qoQEt2UdGUbwU3P1+t +z4SnBvIZ3b2inN+Hwdm5onOBlw== +-----END PRIVATE KEY----- From 034fdbca28c015840b14be8c744dd895090f1870 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 29 Aug 2018 13:51:54 +0200 Subject: [PATCH 209/283] Update BucketUtils#suggestShardSideQueueSize signature (#33210) `BucketUtils#suggestShardSideQueueSize` used to calculate the shard_size based on the number of shards. It returns now a different value only based on whether we are querying a single shard or multiple shards. This commit replaces the numberOfShards argument with a boolean that tells whether we are querying a single shard or not. --- .../search/aggregations/bucket/BucketUtils.java | 14 ++++---------- .../bucket/geogrid/GeoGridAggregationBuilder.java | 2 +- .../SignificantTermsAggregatorFactory.java | 2 +- .../SignificantTextAggregatorFactory.java | 2 +- .../bucket/terms/TermsAggregatorFactory.java | 2 +- .../aggregations/bucket/BucketUtilsTests.java | 12 ++++-------- 6 files changed, 12 insertions(+), 22 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketUtils.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketUtils.java index d145e32c45b3..17b50fa9bef5 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketUtils.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketUtils.java @@ -31,27 +31,21 @@ private BucketUtils() {} * * @param finalSize * The number of terms required in the final reduce phase. - * @param numberOfShards - * The number of shards being queried. + * @param singleShard + * whether a single shard is being queried, or multiple shards * @return A suggested default for the size of any shard-side PriorityQueues */ - public static int suggestShardSideQueueSize(int finalSize, int numberOfShards) { + public static int suggestShardSideQueueSize(int finalSize, boolean singleShard) { if (finalSize < 1) { throw new IllegalArgumentException("size must be positive, got " + finalSize); } - if (numberOfShards < 1) { - throw new IllegalArgumentException("number of shards must be positive, got " + numberOfShards); - } - - if (numberOfShards == 1) { + if (singleShard) { // In the case of a single shard, we do not need to over-request return finalSize; } - // Request 50% more buckets on the shards in order to improve accuracy // as well as a small constant that should help with small values of 'size' final long shardSampleSize = (long) (finalSize * 1.5 + 10); return (int) Math.min(Integer.MAX_VALUE, shardSampleSize); } - } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java index 569845fcdf0e..353f391f213d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/GeoGridAggregationBuilder.java @@ -157,7 +157,7 @@ public int shardSize() { if (shardSize < 0) { // Use default heuristic to avoid any wrong-ranking caused by // distributed counting - shardSize = BucketUtils.suggestShardSideQueueSize(requiredSize, context.numberOfShards()); + shardSize = BucketUtils.suggestShardSideQueueSize(requiredSize, context.numberOfShards() == 1); } if (requiredSize <= 0 || shardSize <= 0) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java index df1bd115e2bf..d612014e0177 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java @@ -195,7 +195,7 @@ protected Aggregator doCreateInternal(ValuesSource valuesSource, Aggregator pare // such are impossible to differentiate from non-significant terms // at that early stage. bucketCountThresholds.setShardSize(2 * BucketUtils.suggestShardSideQueueSize(bucketCountThresholds.getRequiredSize(), - context.numberOfShards())); + context.numberOfShards() == 1)); } if (valuesSource instanceof ValuesSource.Bytes) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java index ea9a8a91aea9..a51a33defdd0 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java @@ -176,7 +176,7 @@ protected Aggregator createInternal(Aggregator parent, boolean collectsFromSingl // such are impossible to differentiate from non-significant terms // at that early stage. bucketCountThresholds.setShardSize(2 * BucketUtils.suggestShardSideQueueSize(bucketCountThresholds.getRequiredSize(), - context.numberOfShards())); + context.numberOfShards() == 1)); } // TODO - need to check with mapping that this is indeed a text field.... diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java index a6481b58ca49..cc2719e5b967 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorFactory.java @@ -122,7 +122,7 @@ protected Aggregator doCreateInternal(ValuesSource valuesSource, Aggregator pare // heuristic to avoid any wrong-ranking caused by distributed // counting bucketCountThresholds.setShardSize(BucketUtils.suggestShardSideQueueSize(bucketCountThresholds.getRequiredSize(), - context.numberOfShards())); + context.numberOfShards() == 1)); } bucketCountThresholds.ensureValidity(); if (valuesSource instanceof ValuesSource.Bytes) { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/BucketUtilsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/BucketUtilsTests.java index aa9068b651e9..35f3175f7cfe 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/BucketUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/BucketUtilsTests.java @@ -27,18 +27,14 @@ public class BucketUtilsTests extends ESTestCase { public void testBadInput() { IllegalArgumentException e = expectThrows(IllegalArgumentException.class, - () -> BucketUtils.suggestShardSideQueueSize(0, 10)); + () -> BucketUtils.suggestShardSideQueueSize(0, randomBoolean())); assertEquals(e.getMessage(), "size must be positive, got 0"); - - e = expectThrows(IllegalArgumentException.class, - () -> BucketUtils.suggestShardSideQueueSize(10, 0)); - assertEquals(e.getMessage(), "number of shards must be positive, got 0"); } public void testOptimizesSingleShard() { for (int iter = 0; iter < 10; ++iter) { final int size = randomIntBetween(1, Integer.MAX_VALUE); - assertEquals(size, BucketUtils.suggestShardSideQueueSize( size, 1)); + assertEquals(size, BucketUtils.suggestShardSideQueueSize( size, true)); } } @@ -46,7 +42,7 @@ public void testOverFlow() { for (int iter = 0; iter < 10; ++iter) { final int size = Integer.MAX_VALUE - randomInt(10); final int numberOfShards = randomIntBetween(1, 10); - final int shardSize = BucketUtils.suggestShardSideQueueSize( size, numberOfShards); + final int shardSize = BucketUtils.suggestShardSideQueueSize( size, numberOfShards == 1); assertThat(shardSize, greaterThanOrEqualTo(shardSize)); } } @@ -55,7 +51,7 @@ public void testShardSizeIsGreaterThanGlobalSize() { for (int iter = 0; iter < 10; ++iter) { final int size = randomIntBetween(1, Integer.MAX_VALUE); final int numberOfShards = randomIntBetween(1, 10); - final int shardSize = BucketUtils.suggestShardSideQueueSize( size, numberOfShards); + final int shardSize = BucketUtils.suggestShardSideQueueSize( size, numberOfShards == 1); assertThat(shardSize, greaterThanOrEqualTo(size)); } } From 49109187e2adb514d90b017e3ace2753f93be3db Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 29 Aug 2018 14:05:41 +0200 Subject: [PATCH 210/283] Remove unsupported group_shard_failures parameter (#33208) We have had support for the `group_shard_failures` parameter in our code for a while, since we introduced failures grouping. When we introduced validation of parameters at REST, we seem to have forgotten to expose such parameter. Given that the parameter is effectively not supported for many months now, that no user has complained about that and that grouping is the expected behaviour, this commit removes support for the parameter. --- .../search/SearchPhaseExecutionException.java | 5 +- .../rest/action/RestActions.java | 3 +- .../SearchPhaseExecutionExceptionTests.java | 53 ------------------- .../AbstractBroadcastResponseTestCase.java | 24 --------- 4 files changed, 3 insertions(+), 82 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseExecutionException.java b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseExecutionException.java index 3d3737b0638c..fa532777e9dc 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseExecutionException.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseExecutionException.java @@ -134,11 +134,10 @@ private static String buildMessage(String phaseName, String msg, ShardSearchFail @Override protected void metadataToXContent(XContentBuilder builder, Params params) throws IOException { builder.field("phase", phaseName); - final boolean group = params.paramAsBoolean("group_shard_failures", true); // we group by default - builder.field("grouped", group); // notify that it's grouped + builder.field("grouped", true); // notify that it's grouped builder.field("failed_shards"); builder.startArray(); - ShardOperationFailedException[] failures = group ? ExceptionsHelper.groupBy(shardFailures) : shardFailures; + ShardOperationFailedException[] failures = ExceptionsHelper.groupBy(shardFailures); for (ShardOperationFailedException failure : failures) { builder.startObject(); failure.toXContent(builder, params); diff --git a/server/src/main/java/org/elasticsearch/rest/action/RestActions.java b/server/src/main/java/org/elasticsearch/rest/action/RestActions.java index 759cd4a773dd..f25fd107e51d 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/RestActions.java +++ b/server/src/main/java/org/elasticsearch/rest/action/RestActions.java @@ -90,8 +90,7 @@ public static void buildBroadcastShardsHeader(XContentBuilder builder, Params pa builder.field(FAILED_FIELD.getPreferredName(), failed); if (shardFailures != null && shardFailures.length > 0) { builder.startArray(FAILURES_FIELD.getPreferredName()); - final boolean group = params.paramAsBoolean("group_shard_failures", true); // we group by default - for (ShardOperationFailedException shardFailure : group ? ExceptionsHelper.groupBy(shardFailures) : shardFailures) { + for (ShardOperationFailedException shardFailure : ExceptionsHelper.groupBy(shardFailures)) { builder.startObject(); shardFailure.toXContent(builder, params); builder.endObject(); diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchPhaseExecutionExceptionTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchPhaseExecutionExceptionTests.java index e96a0975fd46..9fbf3704fff2 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchPhaseExecutionExceptionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchPhaseExecutionExceptionTests.java @@ -26,7 +26,6 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContent; -import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; @@ -38,8 +37,6 @@ import java.io.IOException; -import static java.util.Collections.singletonMap; -import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.Matchers.hasSize; @@ -87,56 +84,6 @@ public void testToXContent() throws IOException { "}" + "}" + "]}", Strings.toString(exception)); - - // Failures are NOT grouped - ToXContent.MapParams params = new ToXContent.MapParams(singletonMap("group_shard_failures", "false")); - try (XContentBuilder builder = jsonBuilder()) { - builder.startObject(); - exception.toXContent(builder, params); - builder.endObject(); - - assertEquals("{" + - "\"type\":\"search_phase_execution_exception\"," + - "\"reason\":\"all shards failed\"," + - "\"phase\":\"test\"," + - "\"grouped\":false," + - "\"failed_shards\":[" + - "{" + - "\"shard\":0," + - "\"index\":\"foo\"," + - "\"node\":\"node_1\"," + - "\"reason\":{" + - "\"type\":\"parsing_exception\"," + - "\"reason\":\"foobar\"," + - "\"line\":1," + - "\"col\":2" + - "}" + - "}," + - "{" + - "\"shard\":1," + - "\"index\":\"foo\"," + - "\"node\":\"node_2\"," + - "\"reason\":{" + - "\"type\":\"index_shard_closed_exception\"," + - "\"reason\":\"CurrentState[CLOSED] Closed\"," + - "\"index_uuid\":\"_na_\"," + - "\"shard\":\"1\"," + - "\"index\":\"foo\"" + - "}" + - "}," + - "{" + - "\"shard\":2," + - "\"index\":\"foo\"," + - "\"node\":\"node_3\"," + - "\"reason\":{" + - "\"type\":\"parsing_exception\"," + - "\"reason\":\"foobar\"," + - "\"line\":5," + - "\"col\":7" + - "}" + - "}" + - "]}", Strings.toString(builder)); - } } public void testToAndFromXContent() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/action/support/broadcast/AbstractBroadcastResponseTestCase.java b/server/src/test/java/org/elasticsearch/action/support/broadcast/AbstractBroadcastResponseTestCase.java index cec5e27f0763..5bf48fa58976 100644 --- a/server/src/test/java/org/elasticsearch/action/support/broadcast/AbstractBroadcastResponseTestCase.java +++ b/server/src/test/java/org/elasticsearch/action/support/broadcast/AbstractBroadcastResponseTestCase.java @@ -33,7 +33,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static org.hamcrest.CoreMatchers.anyOf; @@ -130,29 +129,6 @@ public void testFailuresDeduplication() throws IOException { assertThat(parsedFailures[1].shardId(), equalTo(2)); assertThat(parsedFailures[1].status(), equalTo(RestStatus.INTERNAL_SERVER_ERROR)); assertThat(parsedFailures[1].getCause().getMessage(), containsString("fizz")); - - ToXContent.Params params = new ToXContent.MapParams(Collections.singletonMap("group_shard_failures", "false")); - BytesReference bytesReferenceWithoutDedup = toShuffledXContent(response, xContentType, params, humanReadable); - try(XContentParser parser = createParser(xContentType.xContent(), bytesReferenceWithoutDedup)) { - parsedResponse = doParseInstance(parser); - assertNull(parser.nextToken()); - } - - assertThat(parsedResponse.getShardFailures().length, equalTo(3)); - parsedFailures = parsedResponse.getShardFailures(); - for (int i = 0; i < 3; i++) { - if (i < 2) { - assertThat(parsedFailures[i].index(), equalTo("test")); - assertThat(parsedFailures[i].shardId(), equalTo(i)); - assertThat(parsedFailures[i].status(), equalTo(RestStatus.INTERNAL_SERVER_ERROR)); - assertThat(parsedFailures[i].getCause().getMessage(), containsString("foo")); - } else { - assertThat(parsedFailures[i].index(), equalTo("test")); - assertThat(parsedFailures[i].shardId(), equalTo(i)); - assertThat(parsedFailures[i].status(), equalTo(RestStatus.INTERNAL_SERVER_ERROR)); - assertThat(parsedFailures[i].getCause().getMessage(), containsString("fizz")); - } - } } public void testToXContent() { From 63b2db1d84dd0b31433842600ff278951a0a3681 Mon Sep 17 00:00:00 2001 From: markharwood Date: Wed, 29 Aug 2018 14:13:30 +0100 Subject: [PATCH 211/283] Test fix - Graph HLRC test was missing field name to be excluded from randomisation logic Closes #33231 --- .../protocol/xpack/graph/GraphExploreResponseTests.java | 8 ++++++-- .../protocol/xpack/graph/GraphExploreResponseTests.java | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index 4331bdd37807..e10d7f2b878b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -8,7 +8,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ShardOperationFailedException; import org.elasticsearch.action.search.ShardSearchFailure; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; @@ -75,6 +74,11 @@ protected boolean supportsUnknownFields() { return true; } + @Override + protected String[] getShuffleFieldsExceptions() { + return new String[]{"vertices"}; + } + protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { return field -> field.startsWith("responses"); } @@ -110,7 +114,7 @@ public void testFromXContentWithFailures() throws IOException { boolean supportsUnknownFields = true; //exceptions are not of the same type whenever parsed back boolean assertToXContentEquivalence = false; - AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, getShuffleFieldsExceptions(), getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); } diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index 0f8f055049be..49d70e5b2da1 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ShardOperationFailedException; import org.elasticsearch.action.search.ShardSearchFailure; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; @@ -128,7 +127,7 @@ public void testFromXContentWithFailures() throws IOException { boolean supportsUnknownFields = true; //exceptions are not of the same type whenever parsed back boolean assertToXContentEquivalence = false; - AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, Strings.EMPTY_ARRAY, + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, getShuffleFieldsExceptions(), getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); } From dd1956cf192c958fad250f43633dea5c30493822 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 29 Aug 2018 15:49:35 +0200 Subject: [PATCH 212/283] TESTS: Fix overly long lines (#33240) --- .../protocol/xpack/graph/GraphExploreResponseTests.java | 3 ++- .../protocol/xpack/graph/GraphExploreResponseTests.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index e10d7f2b878b..cfdd23aee74f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -114,7 +114,8 @@ public void testFromXContentWithFailures() throws IOException { boolean supportsUnknownFields = true; //exceptions are not of the same type whenever parsed back boolean assertToXContentEquivalence = false; - AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, getShuffleFieldsExceptions(), + AbstractXContentTestCase.testFromXContent( + NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, getShuffleFieldsExceptions(), getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); } diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index 49d70e5b2da1..a0efb7b3433a 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -127,7 +127,8 @@ public void testFromXContentWithFailures() throws IOException { boolean supportsUnknownFields = true; //exceptions are not of the same type whenever parsed back boolean assertToXContentEquivalence = false; - AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, getShuffleFieldsExceptions(), + AbstractXContentTestCase.testFromXContent( + NUMBER_OF_TEST_RUNS, instanceSupplier, supportsUnknownFields, getShuffleFieldsExceptions(), getRandomFieldsExcludeFilterWhenResultHasErrors(), this::createParser, this::doParseInstance, this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); } From 22415fa2de1d7d07cea7dd5e7263eb1ed4270503 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 29 Aug 2018 14:56:02 +0100 Subject: [PATCH 213/283] [ML] Fix character set finder bug with unencodable charsets (#33234) Some character sets cannot be encoded and this was tripping up the binary data check in the ML log structure character set finder. The fix is to assume that if ICU4J identifies that some bytes correspond to a character set that cannot be encoded and those bytes contain zeroes then the data is binary rather than text. Fixes #33227 --- .../LogStructureFinderManager.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java index 7f18445e505e..a8fd9d7eb895 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java +++ b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java @@ -163,9 +163,15 @@ CharsetMatch findCharset(List explanation, InputStream inputStream) thro // deduction algorithms on binary files is very slow as the binary files generally appear to // have very long lines. boolean spaceEncodingContainsZeroByte = false; - byte[] spaceBytes = " ".getBytes(name); - for (int i = 0; i < spaceBytes.length && spaceEncodingContainsZeroByte == false; ++i) { - spaceEncodingContainsZeroByte = (spaceBytes[i] == 0); + Charset charset = Charset.forName(name); + // Some character sets cannot be encoded. These are extremely rare so it's likely that + // they've been chosen based on incorrectly provided binary data. Therefore, err on + // the side of rejecting binary data. + if (charset.canEncode()) { + byte[] spaceBytes = " ".getBytes(charset); + for (int i = 0; i < spaceBytes.length && spaceEncodingContainsZeroByte == false; ++i) { + spaceEncodingContainsZeroByte = (spaceBytes[i] == 0); + } } if (containsZeroBytes && spaceEncodingContainsZeroByte == false) { explanation.add("Character encoding [" + name + "] matched the input with [" + charsetMatch.getConfidence() + From a5b34c75b08f4cb22ed13e52d250da9418c560df Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 29 Aug 2018 15:03:58 +0100 Subject: [PATCH 214/283] HLRC: Add ML Get Records API (#33085) Relates #29827 --- .../client/MLRequestConverters.java | 15 ++ .../client/MachineLearningClient.java | 38 +++ .../client/ml/GetRecordsRequest.java | 222 ++++++++++++++++++ .../client/ml/GetRecordsResponse.java | 78 ++++++ .../client/MachineLearningGetResultsIT.java | 108 ++++++++- .../MlClientDocumentationIT.java | 93 ++++++++ .../client/ml/GetRecordsRequestTests.java | 72 ++++++ .../client/ml/GetRecordsResponseTests.java | 54 +++++ .../ml/job/results/AnomalyRecordTests.java | 2 +- .../client/ml/job/results/BucketTests.java | 2 +- .../high-level/ml/get-records.asciidoc | 113 +++++++++ .../high-level/supported-apis.asciidoc | 2 + 12 files changed, 784 insertions(+), 15 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsResponseTests.java create mode 100644 docs/java-rest/high-level/ml/get-records.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 3a8fcd534ab6..30e79d1dce2f 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.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.GetBucketsRequest; import org.elasticsearch.client.ml.GetJobRequest; +import org.elasticsearch.client.ml.GetRecordsRequest; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.common.Strings; @@ -124,4 +125,18 @@ static Request getBuckets(GetBucketsRequest getBucketsRequest) throws IOExceptio request.setEntity(createEntity(getBucketsRequest, REQUEST_BODY_CONTENT_TYPE)); return request; } + + static Request getRecords(GetRecordsRequest getRecordsRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(getRecordsRequest.getJobId()) + .addPathPartAsIs("results") + .addPathPartAsIs("records") + .build(); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + request.setEntity(createEntity(getRecordsRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } } 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 34ad9c0d81a4..a972f760d2fd 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 @@ -27,6 +27,8 @@ import org.elasticsearch.client.ml.GetBucketsResponse; import org.elasticsearch.client.ml.GetJobRequest; import org.elasticsearch.client.ml.GetJobResponse; +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.PutJobRequest; @@ -285,4 +287,40 @@ public void getBucketsAsync(GetBucketsRequest request, RequestOptions options, A listener, Collections.emptySet()); } + + /** + * Gets the records for a Machine Learning Job. + *

+ * For additional info + * see ML GET records 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 getRecordsAsync(GetRecordsRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::getRecords, + options, + GetRecordsResponse::fromXContent, + listener, + Collections.emptySet()); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsRequest.java new file mode 100644 index 000000000000..0a701f5a1433 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsRequest.java @@ -0,0 +1,222 @@ +/* + * 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.Validatable; +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.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * A request to retrieve records of a given job + */ +public class GetRecordsRequest implements ToXContentObject, Validatable { + + public static final ParseField EXCLUDE_INTERIM = new ParseField("exclude_interim"); + public static final ParseField START = new ParseField("start"); + public static final ParseField END = new ParseField("end"); + public static final ParseField RECORD_SCORE = new ParseField("record_score"); + public static final ParseField SORT = new ParseField("sort"); + public static final ParseField DESCENDING = new ParseField("desc"); + + public static final ObjectParser PARSER = new ObjectParser<>("get_buckets_request", GetRecordsRequest::new); + + static { + PARSER.declareString((request, jobId) -> request.jobId = jobId, Job.ID); + PARSER.declareBoolean(GetRecordsRequest::setExcludeInterim, EXCLUDE_INTERIM); + PARSER.declareStringOrNull(GetRecordsRequest::setStart, START); + PARSER.declareStringOrNull(GetRecordsRequest::setEnd, END); + PARSER.declareObject(GetRecordsRequest::setPageParams, PageParams.PARSER, PageParams.PAGE); + PARSER.declareDouble(GetRecordsRequest::setRecordScore, RECORD_SCORE); + PARSER.declareString(GetRecordsRequest::setSort, SORT); + PARSER.declareBoolean(GetRecordsRequest::setDescending, DESCENDING); + } + + private String jobId; + private Boolean excludeInterim; + private String start; + private String end; + private PageParams pageParams; + private Double recordScore; + private String sort; + private Boolean descending; + + private GetRecordsRequest() {} + + /** + * Constructs a request to retrieve records of a given job + * @param jobId id of the job to retrieve records of + */ + public GetRecordsRequest(String jobId) { + this.jobId = Objects.requireNonNull(jobId); + } + + public String getJobId() { + return jobId; + } + + public boolean isExcludeInterim() { + return excludeInterim; + } + + /** + * Sets the value of "exclude_interim". + * When {@code true}, interim records will be filtered out. + * @param excludeInterim value of "exclude_interim" to be set + */ + public void setExcludeInterim(boolean excludeInterim) { + this.excludeInterim = excludeInterim; + } + + public String getStart() { + return start; + } + + /** + * Sets the value of "start" which is a timestamp. + * Only records whose timestamp is on or after the "start" value will be returned. + * @param start value of "start" to be set + */ + public void setStart(String start) { + this.start = start; + } + + public String getEnd() { + return end; + } + + /** + * Sets the value of "end" which is a timestamp. + * Only records whose timestamp is before the "end" value will be returned. + * @param end value of "end" to be set + */ + public void setEnd(String end) { + this.end = end; + } + + public PageParams getPageParams() { + return pageParams; + } + + /** + * Sets the paging parameters + * @param pageParams The paging parameters + */ + public void setPageParams(PageParams pageParams) { + this.pageParams = pageParams; + } + + public Double getRecordScore() { + return recordScore; + } + + /** + * Sets the value of "record_score". + * Only records with "record_score" equal or greater will be returned. + * @param recordScore value of "record_score". + */ + public void setRecordScore(double recordScore) { + this.recordScore = recordScore; + } + + public String getSort() { + return sort; + } + + /** + * Sets the value of "sort". + * Specifies the bucket field to sort on. + * @param sort value of "sort". + */ + public void setSort(String sort) { + this.sort = sort; + } + + public boolean isDescending() { + return descending; + } + + /** + * Sets the value of "desc". + * Specifies the sorting order. + * @param descending value of "desc" + */ + public void setDescending(boolean descending) { + this.descending = descending; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Job.ID.getPreferredName(), jobId); + if (excludeInterim != null) { + builder.field(EXCLUDE_INTERIM.getPreferredName(), excludeInterim); + } + if (start != null) { + builder.field(START.getPreferredName(), start); + } + if (end != null) { + builder.field(END.getPreferredName(), end); + } + if (pageParams != null) { + builder.field(PageParams.PAGE.getPreferredName(), pageParams); + } + if (recordScore != null) { + builder.field(RECORD_SCORE.getPreferredName(), recordScore); + } + if (sort != null) { + builder.field(SORT.getPreferredName(), sort); + } + if (descending != null) { + builder.field(DESCENDING.getPreferredName(), descending); + } + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(jobId, excludeInterim, recordScore, pageParams, start, end, sort, descending); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GetRecordsRequest other = (GetRecordsRequest) obj; + return Objects.equals(jobId, other.jobId) && + Objects.equals(excludeInterim, other.excludeInterim) && + Objects.equals(recordScore, other.recordScore) && + Objects.equals(pageParams, other.pageParams) && + Objects.equals(start, other.start) && + Objects.equals(end, other.end) && + Objects.equals(sort, other.sort) && + Objects.equals(descending, other.descending); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsResponse.java new file mode 100644 index 000000000000..99e115242260 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetRecordsResponse.java @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.job.results.AnomalyRecord; +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 buckets + */ +public class GetRecordsResponse extends AbstractResultResponse { + + public static final ParseField RECORDS = new ParseField("records"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_records_response", + true, a -> new GetRecordsResponse((List) a[0], (long) a[1])); + + static { + PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), AnomalyRecord.PARSER, RECORDS); + PARSER.declareLong(ConstructingObjectParser.constructorArg(), COUNT); + } + + public static GetRecordsResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + GetRecordsResponse(List buckets, long count) { + super(RECORDS, buckets, count); + } + + /** + * The retrieved records + * @return the retrieved records + */ + public List records() { + 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; + } + GetRecordsResponse other = (GetRecordsResponse) obj; + return count == other.count && Objects.equals(results, other.results); + } +} 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 4b3d22b451da..6c8ca81cea22 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,8 +23,11 @@ import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.ml.GetBucketsRequest; import org.elasticsearch.client.ml.GetBucketsResponse; +import org.elasticsearch.client.ml.GetRecordsRequest; +import org.elasticsearch.client.ml.GetRecordsResponse; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.results.AnomalyRecord; import org.elasticsearch.client.ml.job.results.Bucket; import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.xcontent.XContentType; @@ -34,7 +37,10 @@ import java.io.IOException; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.lessThanOrEqualTo; public class MachineLearningGetResultsIT extends ESRestHighLevelClientTestCase { @@ -47,7 +53,8 @@ public class MachineLearningGetResultsIT extends ESRestHighLevelClientTestCase { // 2018-08-01T00:00:00Z private static final long START_TIME_EPOCH_MS = 1533081600000L; - private BucketStats bucketStats = new BucketStats(); + private Stats bucketStats = new Stats(); + private Stats recordStats = new Stats(); @Before public void createJobAndIndexResults() throws IOException { @@ -68,7 +75,7 @@ public void createJobAndIndexResults() throws IOException { // Also index an interim bucket addBucketIndexRequest(time, true, bulkRequest); - addRecordIndexRequests(time, true, bulkRequest); + addRecordIndexRequest(time, true, bulkRequest); highLevelClient().bulk(bulkRequest, RequestOptions.DEFAULT); } @@ -91,16 +98,21 @@ private void addRecordIndexRequests(long timestamp, boolean isInterim, BulkReque } int recordCount = randomIntBetween(1, 3); for (int i = 0; i < recordCount; ++i) { - IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC); - double recordScore = randomDoubleBetween(0.0, 100.0, true); - double p = randomDoubleBetween(0.0, 0.05, false); - indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"result_type\":\"record\", \"timestamp\": " + timestamp + "," + - "\"bucket_span\": 3600,\"is_interim\": " + isInterim + ", \"record_score\": " + recordScore + ", \"probability\": " - + p + "}", XContentType.JSON); - bulkRequest.add(indexRequest); + addRecordIndexRequest(timestamp, isInterim, bulkRequest); } } + private void addRecordIndexRequest(long timestamp, boolean isInterim, BulkRequest bulkRequest) { + IndexRequest indexRequest = new IndexRequest(RESULTS_INDEX, DOC); + double recordScore = randomDoubleBetween(0.0, 100.0, true); + recordStats.report(recordScore); + double p = randomDoubleBetween(0.0, 0.05, false); + indexRequest.source("{\"job_id\":\"" + JOB_ID + "\", \"result_type\":\"record\", \"timestamp\": " + timestamp + "," + + "\"bucket_span\": 3600,\"is_interim\": " + isInterim + ", \"record_score\": " + recordScore + ", \"probability\": " + + p + "}", XContentType.JSON); + bulkRequest.add(indexRequest); + } + @After public void deleteJob() throws IOException { new MlRestTestStateCleaner(logger, client()).clearMlMetadata(); @@ -194,7 +206,73 @@ public void testGetBuckets() throws IOException { } } - private static class BucketStats { + public void testGetRecords() throws IOException { + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + + { + GetRecordsRequest request = new GetRecordsRequest(JOB_ID); + + GetRecordsResponse response = execute(request, machineLearningClient::getRecords, machineLearningClient::getRecordsAsync); + + assertThat(response.count(), greaterThan(0L)); + assertThat(response.count(), equalTo(recordStats.totalCount())); + } + { + GetRecordsRequest request = new GetRecordsRequest(JOB_ID); + request.setRecordScore(50.0); + + GetRecordsResponse response = execute(request, machineLearningClient::getRecords, machineLearningClient::getRecordsAsync); + + long majorAndCriticalCount = recordStats.majorCount + recordStats.criticalCount; + assertThat(response.count(), equalTo(majorAndCriticalCount)); + assertThat(response.records().size(), equalTo((int) Math.min(100, majorAndCriticalCount))); + assertThat(response.records().stream().anyMatch(r -> r.getRecordScore() < 50.0), is(false)); + } + { + GetRecordsRequest request = new GetRecordsRequest(JOB_ID); + request.setExcludeInterim(true); + + GetRecordsResponse response = execute(request, machineLearningClient::getRecords, machineLearningClient::getRecordsAsync); + + assertThat(response.count(), equalTo(recordStats.totalCount() - 1)); + } + { + long end = START_TIME_EPOCH_MS + 10 * 3600000; + GetRecordsRequest request = new GetRecordsRequest(JOB_ID); + request.setStart(String.valueOf(START_TIME_EPOCH_MS)); + request.setEnd(String.valueOf(end)); + + GetRecordsResponse response = execute(request, machineLearningClient::getRecords, machineLearningClient::getRecordsAsync); + + for (AnomalyRecord record : response.records()) { + assertThat(record.getTimestamp().getTime(), greaterThanOrEqualTo(START_TIME_EPOCH_MS)); + assertThat(record.getTimestamp().getTime(), lessThan(end)); + } + } + { + GetRecordsRequest request = new GetRecordsRequest(JOB_ID); + request.setPageParams(new PageParams(3, 3)); + + GetRecordsResponse response = execute(request, machineLearningClient::getRecords, machineLearningClient::getRecordsAsync); + + assertThat(response.records().size(), equalTo(3)); + } + { + GetRecordsRequest request = new GetRecordsRequest(JOB_ID); + request.setSort("probability"); + request.setDescending(true); + + GetRecordsResponse response = execute(request, machineLearningClient::getRecords, machineLearningClient::getRecordsAsync); + + double previousProb = 1.0; + for (AnomalyRecord record : response.records()) { + assertThat(record.getProbability(), lessThanOrEqualTo(previousProb)); + previousProb = record.getProbability(); + } + } + } + + private static class Stats { // score < 50.0 private long minorCount; @@ -204,14 +282,18 @@ private static class BucketStats { // score > 75.0 private long criticalCount; - private void report(double anomalyScore) { - if (anomalyScore < 50.0) { + private void report(double score) { + if (score < 50.0) { minorCount++; - } else if (anomalyScore < 75.0) { + } else if (score < 75.0) { majorCount++; } else { criticalCount++; } } + + private long totalCount() { + return minorCount + majorCount + criticalCount; + } } } 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 8e86ffb4d641..94793f0ab791 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 @@ -35,6 +35,8 @@ import org.elasticsearch.client.ml.GetBucketsResponse; import org.elasticsearch.client.ml.GetJobRequest; import org.elasticsearch.client.ml.GetJobResponse; +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.PutJobRequest; @@ -43,6 +45,7 @@ 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.job.results.AnomalyRecord; import org.elasticsearch.client.ml.job.results.Bucket; import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.unit.TimeValue; @@ -454,4 +457,94 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } + + public void testGetRecords() throws IOException, InterruptedException { + RestHighLevelClient client = highLevelClient(); + + String jobId = "test-get-records"; + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + // Let us index a record + IndexRequest indexRequest = new IndexRequest(".ml-anomalies-shared", "doc"); + indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + indexRequest.source("{\"job_id\":\"test-get-records\", \"result_type\":\"record\", \"timestamp\": 1533081600000," + + "\"bucket_span\": 600,\"is_interim\": false, \"record_score\": 80.0}", XContentType.JSON); + client.index(indexRequest, RequestOptions.DEFAULT); + + { + // tag::x-pack-ml-get-records-request + GetRecordsRequest request = new GetRecordsRequest(jobId); // <1> + // end::x-pack-ml-get-records-request + + // tag::x-pack-ml-get-records-desc + request.setDescending(true); // <1> + // end::x-pack-ml-get-records-desc + + // tag::x-pack-ml-get-records-end + request.setEnd("2018-08-21T00:00:00Z"); // <1> + // end::x-pack-ml-get-records-end + + // tag::x-pack-ml-get-records-exclude-interim + request.setExcludeInterim(true); // <1> + // end::x-pack-ml-get-records-exclude-interim + + // tag::x-pack-ml-get-records-page + request.setPageParams(new PageParams(100, 200)); // <1> + // end::x-pack-ml-get-records-page + + // Set page params back to null so the response contains the record we indexed + request.setPageParams(null); + + // tag::x-pack-ml-get-records-record-score + request.setRecordScore(75.0); // <1> + // end::x-pack-ml-get-records-record-score + + // tag::x-pack-ml-get-records-sort + request.setSort("probability"); // <1> + // end::x-pack-ml-get-records-sort + + // tag::x-pack-ml-get-records-start + request.setStart("2018-08-01T00:00:00Z"); // <1> + // end::x-pack-ml-get-records-start + + // tag::x-pack-ml-get-records-execute + GetRecordsResponse response = client.machineLearning().getRecords(request, RequestOptions.DEFAULT); + // end::x-pack-ml-get-records-execute + + // tag::x-pack-ml-get-records-response + long count = response.count(); // <1> + List records = response.records(); // <2> + // end::x-pack-ml-get-records-response + assertEquals(1, records.size()); + } + { + GetRecordsRequest request = new GetRecordsRequest(jobId); + + // tag::x-pack-ml-get-records-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(GetRecordsResponse getRecordsResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::x-pack-ml-get-records-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-records-execute-async + client.machineLearning().getRecordsAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::x-pack-ml-get-records-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsRequestTests.java new file mode 100644 index 000000000000..226ffe75b01e --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsRequestTests.java @@ -0,0 +1,72 @@ +/* + * 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 org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +public class GetRecordsRequestTests extends AbstractXContentTestCase { + + @Override + protected GetRecordsRequest createTestInstance() { + GetRecordsRequest request = new GetRecordsRequest(ESTestCase.randomAlphaOfLengthBetween(1, 20)); + + if (ESTestCase.randomBoolean()) { + request.setStart(String.valueOf(ESTestCase.randomLong())); + } + if (ESTestCase.randomBoolean()) { + request.setEnd(String.valueOf(ESTestCase.randomLong())); + } + if (ESTestCase.randomBoolean()) { + request.setExcludeInterim(ESTestCase.randomBoolean()); + } + if (ESTestCase.randomBoolean()) { + request.setRecordScore(ESTestCase.randomDouble()); + } + if (ESTestCase.randomBoolean()) { + int from = ESTestCase.randomInt(10000); + int size = ESTestCase.randomInt(10000); + request.setPageParams(new PageParams(from, size)); + } + if (ESTestCase.randomBoolean()) { + request.setSort("anomaly_score"); + } + if (ESTestCase.randomBoolean()) { + request.setDescending(ESTestCase.randomBoolean()); + } + if (ESTestCase.randomBoolean()) { + request.setExcludeInterim(ESTestCase.randomBoolean()); + } + return request; + } + + @Override + protected GetRecordsRequest doParseInstance(XContentParser parser) throws IOException { + return GetRecordsRequest.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsResponseTests.java new file mode 100644 index 000000000000..be455f716540 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetRecordsResponseTests.java @@ -0,0 +1,54 @@ +/* + * 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.AnomalyRecord; +import org.elasticsearch.client.ml.job.results.AnomalyRecordTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GetRecordsResponseTests extends AbstractXContentTestCase { + + @Override + protected GetRecordsResponse createTestInstance() { + String jobId = ESTestCase.randomAlphaOfLength(20); + int listSize = ESTestCase.randomInt(10); + List records = new ArrayList<>(listSize); + for (int j = 0; j < listSize; j++) { + AnomalyRecord record = AnomalyRecordTests.createTestInstance(jobId); + records.add(record); + } + return new GetRecordsResponse(records, listSize); + } + + @Override + protected GetRecordsResponse doParseInstance(XContentParser parser) throws IOException { + return GetRecordsResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java index 88abcea86377..a857cd3d9b10 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java @@ -33,7 +33,7 @@ protected AnomalyRecord createTestInstance() { return createTestInstance("foo"); } - public AnomalyRecord createTestInstance(String jobId) { + public static AnomalyRecord createTestInstance(String jobId) { AnomalyRecord anomalyRecord = new AnomalyRecord(jobId, new Date(randomNonNegativeLong()), randomNonNegativeLong()); anomalyRecord.setActual(Collections.singletonList(randomDouble())); anomalyRecord.setTypical(Collections.singletonList(randomDouble())); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketTests.java index 6a0a5d3c6444..b9fac88faccc 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/BucketTests.java @@ -70,7 +70,7 @@ public static Bucket createTestInstance(String jobId) { int size = randomInt(10); List records = new ArrayList<>(size); for (int i = 0; i < size; i++) { - AnomalyRecord anomalyRecord = new AnomalyRecordTests().createTestInstance(jobId); + AnomalyRecord anomalyRecord = AnomalyRecordTests.createTestInstance(jobId); records.add(anomalyRecord); } bucket.setRecords(records); diff --git a/docs/java-rest/high-level/ml/get-records.asciidoc b/docs/java-rest/high-level/ml/get-records.asciidoc new file mode 100644 index 000000000000..40cc185225ee --- /dev/null +++ b/docs/java-rest/high-level/ml/get-records.asciidoc @@ -0,0 +1,113 @@ +[[java-rest-high-x-pack-ml-get-records]] +=== Get Records API + +The Get Records API retrieves one or more record results. +It accepts a `GetRecordsRequest` object and responds +with a `GetRecordsResponse` object. + +[[java-rest-high-x-pack-ml-get-records-request]] +==== Get Records Request + +A `GetRecordsRequest` 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-records-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-records-desc] +-------------------------------------------------- +<1> If `true`, the records are sorted in descending order. Defaults to `false`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-end] +-------------------------------------------------- +<1> Records with timestamps earlier than this time will be returned. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-exclude-interim] +-------------------------------------------------- +<1> If `true`, interim results will be excluded. Defaults to `false`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-page] +-------------------------------------------------- +<1> The page parameters `from` and `size`. `from` specifies the number of records to skip. +`size` specifies the maximum number of records to get. Defaults to `0` and `100` respectively. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-record-score] +-------------------------------------------------- +<1> Records with record_score greater or equal than this value will be returned. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-sort] +-------------------------------------------------- +<1> The field to sort records on. Defaults to `influencer_score`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-end] +-------------------------------------------------- +<1> Records with timestamps on or after this time will be returned. + +[[java-rest-high-x-pack-ml-get-records-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-records-execute] +-------------------------------------------------- + + +[[java-rest-high-x-pack-ml-get-records-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-records-execute-async] +-------------------------------------------------- +<1> The `GetRecordsRequest` 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 `GetRecordsResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-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-records-response]] +==== Get Records Response + +The returned `GetRecordsResponse` contains the requested records: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-response] +-------------------------------------------------- +<1> The count of records that were matched +<2> The records 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 64c95912b5ea..2b72ca74f6aa 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -212,6 +212,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> include::ml/put-job.asciidoc[] include::ml/get-job.asciidoc[] @@ -219,6 +220,7 @@ include::ml/delete-job.asciidoc[] include::ml/open-job.asciidoc[] include::ml/close-job.asciidoc[] include::ml/get-buckets.asciidoc[] +include::ml/get-records.asciidoc[] == Migration APIs From e95c2afe3c20da62a71c6deefbf9ecbac5f9be6a Mon Sep 17 00:00:00 2001 From: markharwood Date: Wed, 29 Aug 2018 15:19:26 +0100 Subject: [PATCH 215/283] Test fix - Graph HLRC tests needed another field adding to randomisation exception list Related to #33231 --- .../protocol/xpack/graph/GraphExploreResponseTests.java | 2 +- .../protocol/xpack/graph/GraphExploreResponseTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index cfdd23aee74f..9f0f41456954 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -76,7 +76,7 @@ protected boolean supportsUnknownFields() { @Override protected String[] getShuffleFieldsExceptions() { - return new String[]{"vertices"}; + return new String[]{"vertices", "connections"}; } protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index a0efb7b3433a..9c4a76fdcecf 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -89,7 +89,7 @@ protected boolean supportsUnknownFields() { @Override protected String[] getShuffleFieldsExceptions() { - return new String[]{"vertices"}; + return new String[]{"vertices", "connections"}; } protected Predicate getRandomFieldsExcludeFilterWhenResultHasErrors() { From 6a0d4b4a773d2c3d463b9807f400f911932fe51e Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Wed, 29 Aug 2018 16:43:13 +0200 Subject: [PATCH 216/283] Remote 6.x transport BWC Layer for `_shrink` (#33236) The shrink action was renamed to `_resize` with the addition or split. This bwc layer is unnecessary on 7.x since 6.latest will always use the resize action. --- .../elasticsearch/action/ActionModule.java | 3 -- .../indices/shrink/TransportShrinkAction.java | 46 ------------------- 2 files changed, 49 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportShrinkAction.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 58efce77c9fd..b3ec72d52709 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -131,9 +131,7 @@ import org.elasticsearch.action.admin.indices.shards.IndicesShardStoresAction; import org.elasticsearch.action.admin.indices.shards.TransportIndicesShardStoresAction; import org.elasticsearch.action.admin.indices.shrink.ResizeAction; -import org.elasticsearch.action.admin.indices.shrink.ShrinkAction; import org.elasticsearch.action.admin.indices.shrink.TransportResizeAction; -import org.elasticsearch.action.admin.indices.shrink.TransportShrinkAction; import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; import org.elasticsearch.action.admin.indices.stats.TransportIndicesStatsAction; import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateAction; @@ -446,7 +444,6 @@ public void reg actions.register(IndicesSegmentsAction.INSTANCE, TransportIndicesSegmentsAction.class); actions.register(IndicesShardStoresAction.INSTANCE, TransportIndicesShardStoresAction.class); actions.register(CreateIndexAction.INSTANCE, TransportCreateIndexAction.class); - actions.register(ShrinkAction.INSTANCE, TransportShrinkAction.class); actions.register(ResizeAction.INSTANCE, TransportResizeAction.class); actions.register(RolloverAction.INSTANCE, TransportRolloverAction.class); actions.register(DeleteIndexAction.INSTANCE, TransportDeleteIndexAction.class); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportShrinkAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportShrinkAction.java deleted file mode 100644 index acc88251970f..000000000000 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportShrinkAction.java +++ /dev/null @@ -1,46 +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.action.admin.indices.shrink; - -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.client.Client; -import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; -import org.elasticsearch.cluster.metadata.MetaDataCreateIndexService; -import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TransportService; - -/** - * Main class to initiate shrinking an index into a new index - * This class is only here for backwards compatibility. It will be replaced by - * TransportResizeAction in 7.x once this is backported - */ -public class TransportShrinkAction extends TransportResizeAction { - - @Inject - public TransportShrinkAction(Settings settings, TransportService transportService, ClusterService clusterService, - ThreadPool threadPool, MetaDataCreateIndexService createIndexService, - ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, Client client) { - super(settings, ShrinkAction.NAME, transportService, clusterService, threadPool, createIndexService, actionFilters, - indexNameExpressionResolver, client); - } -} From 3828ec60f5c358cb9f968488ccdbbde697d66514 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Wed, 29 Aug 2018 17:43:40 +0300 Subject: [PATCH 217/283] Fix forbidden apis on FIPS (#33202) - third party audit detects jar hell with JDK so we disable it - jdk non portable in forbiddenapis detects classes being used from the JDK ( for fips ) that are not portable, this is intended so we don't scan for it on fips. - different exclusion rules for third party audit on fips Closes #33179 --- distribution/tools/plugin-cli/build.gradle | 6 ++++++ modules/transport-netty4/build.gradle | 9 ++++++++- plugins/ingest-attachment/build.gradle | 6 ++++++ plugins/transport-nio/build.gradle | 10 ++++++++-- x-pack/plugin/security/cli/build.gradle | 14 ++++++++++++-- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index c47786299bc2..38be8db42ff6 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -39,3 +39,9 @@ test { // TODO: find a way to add permissions for the tests in this module systemProperty 'tests.security.manager', 'false' } + +if (project.inFipsJvm) { + // FIPS JVM includes manny classes from bouncycastle which count as jar hell for the third party audit, + // rather than provide a long list of exclusions, disable the check on FIPS. + thirdPartyAudit.enabled = false +} diff --git a/modules/transport-netty4/build.gradle b/modules/transport-netty4/build.gradle index 12ce5ce7d4a8..e7c36ff506ed 100644 --- a/modules/transport-netty4/build.gradle +++ b/modules/transport-netty4/build.gradle @@ -83,7 +83,6 @@ thirdPartyAudit.excludes = [ 'io.netty.internal.tcnative.SSLContext', // from io.netty.handler.ssl.util.BouncyCastleSelfSignedCertGenerator (netty) - 'org.bouncycastle.asn1.x500.X500Name', 'org.bouncycastle.cert.X509v3CertificateBuilder', 'org.bouncycastle.cert.jcajce.JcaX509CertificateConverter', 'org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder', @@ -163,3 +162,11 @@ thirdPartyAudit.excludes = [ 'org.conscrypt.Conscrypt', 'org.conscrypt.HandshakeListener' ] + +if (project.inFipsJvm == false) { + // BouncyCastleFIPS provides this class, so the exclusion is invalid when running CI in + // a FIPS JVM with BouncyCastleFIPS Provider + thirdPartyAudit.excludes += [ + 'org.bouncycastle.asn1.x500.X500Name' + ] +} diff --git a/plugins/ingest-attachment/build.gradle b/plugins/ingest-attachment/build.gradle index 6cd55f682c8b..f55104f2a96f 100644 --- a/plugins/ingest-attachment/build.gradle +++ b/plugins/ingest-attachment/build.gradle @@ -2141,3 +2141,9 @@ if (project.runtimeJavaVersion > JavaVersion.VERSION_1_8) { 'javax.xml.bind.Unmarshaller' ] } + +if (project.inFipsJvm) { + // FIPS JVM includes manny classes from bouncycastle which count as jar hell for the third party audit, + // rather than provide a long list of exclusions, disable the check on FIPS. + thirdPartyAudit.enabled = false +} diff --git a/plugins/transport-nio/build.gradle b/plugins/transport-nio/build.gradle index 07605bfee29b..cb8916b857c2 100644 --- a/plugins/transport-nio/build.gradle +++ b/plugins/transport-nio/build.gradle @@ -62,7 +62,6 @@ thirdPartyAudit.excludes = [ 'io.netty.internal.tcnative.SSLContext', // from io.netty.handler.ssl.util.BouncyCastleSelfSignedCertGenerator (netty) - 'org.bouncycastle.asn1.x500.X500Name', 'org.bouncycastle.cert.X509v3CertificateBuilder', 'org.bouncycastle.cert.jcajce.JcaX509CertificateConverter', 'org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder', @@ -141,4 +140,11 @@ thirdPartyAudit.excludes = [ 'org.conscrypt.BufferAllocator', 'org.conscrypt.Conscrypt', 'org.conscrypt.HandshakeListener' -] \ No newline at end of file +] +if (project.inFipsJvm == false) { + // BouncyCastleFIPS provides this class, so the exclusion is invalid when running CI in + // a FIPS JVM with BouncyCastleFIPS Provider + thirdPartyAudit.excludes += [ + 'org.bouncycastle.asn1.x500.X500Name' + ] +} \ No newline at end of file diff --git a/x-pack/plugin/security/cli/build.gradle b/x-pack/plugin/security/cli/build.gradle index 426c48aac80a..377d10ec7f20 100644 --- a/x-pack/plugin/security/cli/build.gradle +++ b/x-pack/plugin/security/cli/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.precommit.ForbiddenApisCliTask + apply plugin: 'elasticsearch.build' archivesBaseName = 'elasticsearch-security-cli' @@ -6,8 +8,8 @@ dependencies { compileOnly "org.elasticsearch:elasticsearch:${version}" // "org.elasticsearch.plugin:x-pack-core:${version}" doesn't work with idea because the testArtifacts are also here compileOnly project(path: xpackModule('core'), configuration: 'default') - compile 'org.bouncycastle:bcprov-jdk15on:1.59' compile 'org.bouncycastle:bcpkix-jdk15on:1.59' + compile 'org.bouncycastle:bcprov-jdk15on:1.59' testImplementation 'com.google.jimfs:jimfs:1.1' testCompile "junit:junit:${versions.junit}" testCompile "org.hamcrest:hamcrest-all:${versions.hamcrest}" @@ -20,6 +22,14 @@ dependencyLicenses { mapping from: /bc.*/, to: 'bouncycastle' } -if (inFipsJvm) { +if (project.inFipsJvm) { test.enabled = false + // Forbiden APIs non-portable checks fail because bouncy castle classes being used from the FIPS JDK since those are + // not part of the Java specification - all of this is as designed, so we have to relax this check for FIPS. + tasks.withType(ForbiddenApisCliTask) { + bundledSignatures -= "jdk-non-portable" + } + // FIPS JVM includes manny classes from bouncycastle which count as jar hell for the third party audit, + // rather than provide a long list of exclusions, disable the check on FIPS. + thirdPartyAudit.enabled = false } From 6daf8115d6b0b7a4b6e11230fe25dcce11bffa4a Mon Sep 17 00:00:00 2001 From: jaymode Date: Wed, 29 Aug 2018 09:02:32 -0600 Subject: [PATCH 218/283] Update version after client credentials backport This commit changes the serialization version from V_7_0_0_alpha1 to V_6_5_0 for the create token request and response with a client credentials grant type. The client credentials work has now been backported to 6.x. Relates #33106 --- .../xpack/core/security/action/token/CreateTokenRequest.java | 2 +- .../xpack/core/security/action/token/CreateTokenResponse.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java index 4d57da06b921..ed31f0cc020c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java @@ -186,7 +186,7 @@ public String getRefreshToken() { @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - if (out.getVersion().before(Version.V_7_0_0_alpha1) && GrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) { + if (out.getVersion().before(Version.V_6_5_0) && GrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) { throw new IllegalArgumentException("a request with the client_credentials grant_type cannot be sent to version [" + out.getVersion() + "]"); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java index 439247356789..30111a92431d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java @@ -59,7 +59,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(tokenString); out.writeTimeValue(expiresIn); out.writeOptionalString(scope); - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { // TODO change to V_6_5_0 after backport + if (out.getVersion().onOrAfter(Version.V_6_5_0)) { out.writeOptionalString(refreshToken); } else if (out.getVersion().onOrAfter(Version.V_6_2_0)) { if (refreshToken == null) { @@ -76,7 +76,7 @@ public void readFrom(StreamInput in) throws IOException { tokenString = in.readString(); expiresIn = in.readTimeValue(); scope = in.readOptionalString(); - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { // TODO change to V_6_5_0 after backport + if (in.getVersion().onOrAfter(Version.V_6_5_0)) { refreshToken = in.readOptionalString(); } else if (in.getVersion().onOrAfter(Version.V_6_2_0)) { refreshToken = in.readString(); From b52818ec6f0dbc39090554d51317bff31ea3dec1 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Wed, 29 Aug 2018 09:24:56 -0700 Subject: [PATCH 219/283] Painless: Add Bindings (#33042) Add bindings that allow some specialized methods to store permanent state between script executions. --- .../elasticsearch/painless/spi/Whitelist.java | 5 +- .../painless/spi/WhitelistBinding.java | 67 +++++ .../painless/spi/WhitelistClass.java | 5 +- .../painless/spi/WhitelistLoader.java | 161 +++++++--- .../painless/spi/WhitelistMethod.java | 3 +- .../elasticsearch/painless/BindingTest.java | 32 ++ .../org/elasticsearch/painless/Globals.java | 18 +- .../painless/lookup/PainlessBinding.java | 41 +++ .../painless/lookup/PainlessClass.java | 1 + .../painless/lookup/PainlessClassBuilder.java | 1 + .../painless/lookup/PainlessConstructor.java | 1 + .../painless/lookup/PainlessField.java | 1 + .../painless/lookup/PainlessLookup.java | 15 +- .../lookup/PainlessLookupBuilder.java | 282 ++++++++++++++++-- .../painless/lookup/PainlessMethod.java | 1 + .../painless/node/ECallLocal.java | 58 +++- .../elasticsearch/painless/node/SSource.java | 7 + .../painless/spi/org.elasticsearch.txt | 40 +-- .../elasticsearch/painless/BindingsTests.java | 64 ++++ 19 files changed, 712 insertions(+), 91 deletions(-) create mode 100644 modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistBinding.java create mode 100644 modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java create mode 100644 modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessBinding.java create mode 100644 modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java index c38325edd142..7acbff6cb0b9 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java @@ -61,9 +61,12 @@ public final class Whitelist { /** The {@link List} of all the whitelisted Painless classes. */ public final List whitelistClasses; + public final List whitelistBindings; + /** Standard constructor. All values must be not {@code null}. */ - public Whitelist(ClassLoader classLoader, List whitelistClasses) { + public Whitelist(ClassLoader classLoader, List whitelistClasses, List whitelistBindings) { this.classLoader = Objects.requireNonNull(classLoader); this.whitelistClasses = Collections.unmodifiableList(Objects.requireNonNull(whitelistClasses)); + this.whitelistBindings = Collections.unmodifiableList(Objects.requireNonNull(whitelistBindings)); } } diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistBinding.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistBinding.java new file mode 100644 index 000000000000..364dbbb09ca9 --- /dev/null +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistBinding.java @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.painless.spi; + +import java.util.List; +import java.util.Objects; + +/** + * A binding represents a method call that stores state. Each binding class must have exactly one + * public constructor and one public method excluding those inherited directly from {@link Object}. + * The canonical type name parameters provided must match those of the constructor and method combined. + * The constructor for a binding class will be called when the binding method is called for the first + * time at which point state may be stored for the arguments passed into the constructor. The method + * for a binding class will be called each time the binding method is called and may use the previously + * stored state. + */ +public class WhitelistBinding { + + /** Information about where this constructor was whitelisted from. */ + public final String origin; + + /** The Java class name this binding represents. */ + public final String targetJavaClassName; + + /** The method name for this binding. */ + public final String methodName; + + /** + * The canonical type name for the return type. + */ + public final String returnCanonicalTypeName; + + /** + * A {@link List} of {@link String}s that are the Painless type names for the parameters of the + * constructor which can be used to look up the Java constructor through reflection. + */ + public final List canonicalTypeNameParameters; + + /** Standard constructor. All values must be not {@code null}. */ + public WhitelistBinding(String origin, String targetJavaClassName, + String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters) { + + this.origin = Objects.requireNonNull(origin); + this.targetJavaClassName = Objects.requireNonNull(targetJavaClassName); + + this.methodName = Objects.requireNonNull(methodName); + this.returnCanonicalTypeName = Objects.requireNonNull(returnCanonicalTypeName); + this.canonicalTypeNameParameters = Objects.requireNonNull(canonicalTypeNameParameters); + } +} diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClass.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClass.java index 0b216ae5c295..7b3eb75aa3ec 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClass.java +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClass.java @@ -62,9 +62,8 @@ public final class WhitelistClass { /** Standard constructor. All values must be not {@code null}. */ public WhitelistClass(String origin, String javaClassName, boolean noImport, - List whitelistConstructors, - List whitelistMethods, - List whitelistFields) { + List whitelistConstructors, List whitelistMethods, List whitelistFields) + { this.origin = Objects.requireNonNull(origin); this.javaClassName = Objects.requireNonNull(javaClassName); diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java index a4a0076626a9..0279c82f1b67 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java @@ -133,6 +133,7 @@ public final class WhitelistLoader { */ public static Whitelist loadFromResourceFiles(Class resource, String... filepaths) { List whitelistClasses = new ArrayList<>(); + List whitelistBindings = new ArrayList<>(); // Execute a single pass through the whitelist text files. This will gather all the // constructors, methods, augmented methods, and fields for each whitelisted class. @@ -141,8 +142,9 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep int number = -1; try (LineNumberReader reader = new LineNumberReader( - new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) { + new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) { + String parseType = null; String whitelistClassOrigin = null; String javaClassName = null; boolean noImport = false; @@ -165,7 +167,11 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep // Ensure the final token of the line is '{'. if (line.endsWith("{") == false) { throw new IllegalArgumentException( - "invalid class definition: failed to parse class opening bracket [" + line + "]"); + "invalid class definition: failed to parse class opening bracket [" + line + "]"); + } + + if (parseType != null) { + throw new IllegalArgumentException("invalid definition: cannot embed class definition [" + line + "]"); } // Parse the Java class name. @@ -178,6 +184,7 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep throw new IllegalArgumentException("invalid class definition: failed to parse class name [" + line + "]"); } + parseType = "class"; whitelistClassOrigin = "[" + filepath + "]:[" + number + "]"; javaClassName = tokens[0]; @@ -185,34 +192,117 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep whitelistConstructors = new ArrayList<>(); whitelistMethods = new ArrayList<>(); whitelistFields = new ArrayList<>(); + } else if (line.startsWith("static ")) { + // Ensure the final token of the line is '{'. + if (line.endsWith("{") == false) { + throw new IllegalArgumentException( + "invalid static definition: failed to parse static opening bracket [" + line + "]"); + } - // Handle the end of a class, by creating a new WhitelistClass with all the previously gathered - // constructors, methods, augmented methods, and fields, and adding it to the list of whitelisted classes. + if (parseType != null) { + throw new IllegalArgumentException("invalid definition: cannot embed static definition [" + line + "]"); + } + + parseType = "static"; + + // Handle the end of a definition and reset all previously gathered values. // Expects the following format: '}' '\n' } else if (line.equals("}")) { - if (javaClassName == null) { - throw new IllegalArgumentException("invalid class definition: extraneous closing bracket"); + if (parseType == null) { + throw new IllegalArgumentException("invalid definition: extraneous closing bracket"); } - whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport, - whitelistConstructors, whitelistMethods, whitelistFields)); + // Create a new WhitelistClass with all the previously gathered constructors, methods, + // augmented methods, and fields, and add it to the list of whitelisted classes. + if ("class".equals(parseType)) { + whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport, + whitelistConstructors, whitelistMethods, whitelistFields)); + + whitelistClassOrigin = null; + javaClassName = null; + noImport = false; + whitelistConstructors = null; + whitelistMethods = null; + whitelistFields = null; + } - // Set all the variables to null to ensure a new class definition is found before other parsable values. - whitelistClassOrigin = null; - javaClassName = null; - noImport = false; - whitelistConstructors = null; - whitelistMethods = null; - whitelistFields = null; + // Reset the parseType. + parseType = null; - // Handle all other valid cases. - } else { + // Handle static definition types. + // Expects the following format: ID ID '(' ( ID ( ',' ID )* )? ')' 'bound_to' ID '\n' + } else if ("static".equals(parseType)) { + // Mark the origin of this parsable object. + String origin = "[" + filepath + "]:[" + number + "]"; + + // Parse the tokens prior to the method parameters. + int parameterStartIndex = line.indexOf('('); + + if (parameterStartIndex == -1) { + throw new IllegalArgumentException( + "illegal static definition: start of method parameters not found [" + line + "]"); + } + + String[] tokens = line.substring(0, parameterStartIndex).trim().split("\\s+"); + + String methodName; + + // Based on the number of tokens, look up the Java method name. + if (tokens.length == 2) { + methodName = tokens[1]; + } else { + throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]"); + } + + String returnCanonicalTypeName = tokens[0]; + + // Parse the method parameters. + int parameterEndIndex = line.indexOf(')'); + + if (parameterEndIndex == -1) { + throw new IllegalArgumentException( + "illegal static definition: end of method parameters not found [" + line + "]"); + } + + String[] canonicalTypeNameParameters = + line.substring(parameterStartIndex + 1, parameterEndIndex).replaceAll("\\s+", "").split(","); + + // Handle the case for a method with no parameters. + if ("".equals(canonicalTypeNameParameters[0])) { + canonicalTypeNameParameters = new String[0]; + } + + // Parse the static type and class. + tokens = line.substring(parameterEndIndex + 1).trim().split("\\s+"); + + String staticType; + String targetJavaClassName; + + // Based on the number of tokens, look up the type and class. + if (tokens.length == 2) { + staticType = tokens[0]; + targetJavaClassName = tokens[1]; + } else { + throw new IllegalArgumentException("invalid static definition: unexpected format [" + line + "]"); + } + + // Check the static type is valid. + if ("bound_to".equals(staticType) == false) { + throw new IllegalArgumentException( + "invalid static definition: unexpected static type [" + staticType + "] [" + line + "]"); + } + + whitelistBindings.add(new WhitelistBinding(origin, targetJavaClassName, + methodName, returnCanonicalTypeName, Arrays.asList(canonicalTypeNameParameters))); + + // Handle class definition types. + } else if ("class".equals(parseType)) { // Mark the origin of this parsable object. String origin = "[" + filepath + "]:[" + number + "]"; // Ensure we have a defined class before adding any constructors, methods, augmented methods, or fields. - if (javaClassName == null) { - throw new IllegalArgumentException("invalid object definition: expected a class name [" + line + "]"); + if (parseType == null) { + throw new IllegalArgumentException("invalid definition: expected one of ['class', 'static'] [" + line + "]"); } // Handle the case for a constructor definition. @@ -221,7 +311,7 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep // Ensure the final token of the line is ')'. if (line.endsWith(")") == false) { throw new IllegalArgumentException( - "invalid constructor definition: expected a closing parenthesis [" + line + "]"); + "invalid constructor definition: expected a closing parenthesis [" + line + "]"); } // Parse the constructor parameters. @@ -234,34 +324,34 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep whitelistConstructors.add(new WhitelistConstructor(origin, Arrays.asList(tokens))); - // Handle the case for a method or augmented method definition. - // Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n' + // Handle the case for a method or augmented method definition. + // Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n' } else if (line.contains("(")) { // Ensure the final token of the line is ')'. if (line.endsWith(")") == false) { throw new IllegalArgumentException( - "invalid method definition: expected a closing parenthesis [" + line + "]"); + "invalid method definition: expected a closing parenthesis [" + line + "]"); } // Parse the tokens prior to the method parameters. int parameterIndex = line.indexOf('('); - String[] tokens = line.trim().substring(0, parameterIndex).split("\\s+"); + String[] tokens = line.substring(0, parameterIndex).trim().split("\\s+"); - String javaMethodName; + String methodName; String javaAugmentedClassName; // Based on the number of tokens, look up the Java method name and if provided the Java augmented class. if (tokens.length == 2) { - javaMethodName = tokens[1]; + methodName = tokens[1]; javaAugmentedClassName = null; } else if (tokens.length == 3) { - javaMethodName = tokens[2]; + methodName = tokens[2]; javaAugmentedClassName = tokens[1]; } else { throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]"); } - String painlessReturnTypeName = tokens[0]; + String returnCanonicalTypeName = tokens[0]; // Parse the method parameters. tokens = line.substring(parameterIndex + 1, line.length() - 1).replaceAll("\\s+", "").split(","); @@ -271,11 +361,11 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep tokens = new String[0]; } - whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, javaMethodName, - painlessReturnTypeName, Arrays.asList(tokens))); + whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, methodName, + returnCanonicalTypeName, Arrays.asList(tokens))); - // Handle the case for a field definition. - // Expects the following format: ID ID '\n' + // Handle the case for a field definition. + // Expects the following format: ID ID '\n' } else { // Parse the field tokens. String[] tokens = line.split("\\s+"); @@ -287,20 +377,23 @@ public static Whitelist loadFromResourceFiles(Class resource, String... filep whitelistFields.add(new WhitelistField(origin, tokens[1], tokens[0])); } + } else { + throw new IllegalArgumentException("invalid definition: unable to parse line [" + line + "]"); } } // Ensure all classes end with a '}' token before the end of the file. if (javaClassName != null) { - throw new IllegalArgumentException("invalid class definition: expected closing bracket"); + throw new IllegalArgumentException("invalid definition: expected closing bracket"); } } catch (Exception exception) { throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception); } } + ClassLoader loader = AccessController.doPrivileged((PrivilegedAction)resource::getClassLoader); - return new Whitelist(loader, whitelistClasses); + return new Whitelist(loader, whitelistClasses, whitelistBindings); } private WhitelistLoader() {} diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistMethod.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistMethod.java index 5cd023a3591a..f450ee0238d1 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistMethod.java +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistMethod.java @@ -67,7 +67,8 @@ public class WhitelistMethod { * is augmented as described in the class documentation. */ public WhitelistMethod(String origin, String augmentedCanonicalClassName, String methodName, - String returnCanonicalTypeName, List canonicalTypeNameParameters) { + String returnCanonicalTypeName, List canonicalTypeNameParameters) { + this.origin = Objects.requireNonNull(origin); this.augmentedCanonicalClassName = augmentedCanonicalClassName; this.methodName = methodName; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java new file mode 100644 index 000000000000..1dcbce037b26 --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.painless; + +public class BindingTest { + public int state; + + public BindingTest(int state0, int state1) { + this.state = state0 + state1; + } + + public int testAddWithState(int stateless) { + return stateless + state; + } +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java index 83eb74d827f8..d18cf2780cf3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java @@ -31,6 +31,7 @@ public class Globals { private final Map syntheticMethods = new HashMap<>(); private final Map constantInitializers = new HashMap<>(); + private final Map> bindings = new HashMap<>(); private final BitSet statements; /** Create a new Globals from the set of statement boundaries */ @@ -54,7 +55,15 @@ public void addConstantInitializer(Constant constant) { throw new IllegalStateException("constant initializer: " + constant.name + " already exists"); } } - + + /** Adds a new binding to be written as a local variable */ + public String addBinding(Class type) { + String name = "$binding$" + bindings.size(); + bindings.put(name, type); + + return name; + } + /** Returns the current synthetic methods */ public Map getSyntheticMethods() { return syntheticMethods; @@ -64,7 +73,12 @@ public Map getSyntheticMethods() { public Map getConstantInitializers() { return constantInitializers; } - + + /** Returns the current bindings */ + public Map> getBindings() { + return bindings; + } + /** Returns the set of statement boundaries */ public BitSet getStatements() { return statements; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessBinding.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessBinding.java new file mode 100644 index 000000000000..41178dd5d750 --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessBinding.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.painless.lookup; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +public class PainlessBinding { + + public final Constructor javaConstructor; + public final Method javaMethod; + + public final Class returnType; + public final List> typeParameters; + + PainlessBinding(Constructor javaConstructor, Method javaMethod, Class returnType, List> typeParameters) { + this.javaConstructor = javaConstructor; + this.javaMethod = javaMethod; + + this.returnType = returnType; + this.typeParameters = typeParameters; + } +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClass.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClass.java index 50bb79dcfbdf..f5d6c97bb2f3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClass.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClass.java @@ -24,6 +24,7 @@ import java.util.Map; public final class PainlessClass { + public final Map constructors; public final Map staticMethods; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBuilder.java index a61215e9ed74..92100d1bda0c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBuilder.java @@ -24,6 +24,7 @@ import java.util.Map; final class PainlessClassBuilder { + final Map constructors; final Map staticMethods; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java index 76597c1a29d6..a3dc6c8122bd 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessConstructor.java @@ -25,6 +25,7 @@ import java.util.List; public class PainlessConstructor { + public final Constructor javaConstructor; public final List> typeParameters; public final MethodHandle methodHandle; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessField.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessField.java index a55d6c3730eb..9567e97331c7 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessField.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessField.java @@ -23,6 +23,7 @@ import java.lang.reflect.Field; public final class PainlessField { + public final Field javaField; public final Class typeParameter; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java index 55855a3cb1ef..2d6ed3e361dc 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java @@ -37,12 +37,17 @@ public final class PainlessLookup { private final Map> canonicalClassNamesToClasses; private final Map, PainlessClass> classesToPainlessClasses; - PainlessLookup(Map> canonicalClassNamesToClasses, Map, PainlessClass> classesToPainlessClasses) { + private final Map painlessMethodKeysToPainlessBindings; + + PainlessLookup(Map> canonicalClassNamesToClasses, Map, PainlessClass> classesToPainlessClasses, + Map painlessMethodKeysToPainlessBindings) { Objects.requireNonNull(canonicalClassNamesToClasses); Objects.requireNonNull(classesToPainlessClasses); this.canonicalClassNamesToClasses = Collections.unmodifiableMap(canonicalClassNamesToClasses); this.classesToPainlessClasses = Collections.unmodifiableMap(classesToPainlessClasses); + + this.painlessMethodKeysToPainlessBindings = Collections.unmodifiableMap(painlessMethodKeysToPainlessBindings); } public boolean isValidCanonicalClassName(String canonicalClassName) { @@ -162,6 +167,14 @@ public PainlessField lookupPainlessField(Class targetClass, boolean isStatic, return painlessField; } + public PainlessBinding lookupPainlessBinding(String methodName, int arity) { + Objects.requireNonNull(methodName); + + String painlessMethodKey = buildPainlessMethodKey(methodName, arity); + + return painlessMethodKeysToPainlessBindings.get(painlessMethodKey); + } + public PainlessMethod lookupFunctionalInterfacePainlessMethod(Class targetClass) { PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetClass); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index c8353b54c9f4..7adc81625205 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -20,6 +20,7 @@ package org.elasticsearch.painless.lookup; import org.elasticsearch.painless.spi.Whitelist; +import org.elasticsearch.painless.spi.WhitelistBinding; import org.elasticsearch.painless.spi.WhitelistClass; import org.elasticsearch.painless.spi.WhitelistConstructor; import org.elasticsearch.painless.spi.WhitelistField; @@ -52,11 +53,11 @@ public final class PainlessLookupBuilder { private static class PainlessConstructorCacheKey { - private final Class targetType; + private final Class targetClass; private final List> typeParameters; - private PainlessConstructorCacheKey(Class targetType, List> typeParameters) { - this.targetType = targetType; + private PainlessConstructorCacheKey(Class targetClass, List> typeParameters) { + this.targetClass = targetClass; this.typeParameters = Collections.unmodifiableList(typeParameters); } @@ -72,25 +73,27 @@ public boolean equals(Object object) { PainlessConstructorCacheKey that = (PainlessConstructorCacheKey)object; - return Objects.equals(targetType, that.targetType) && + return Objects.equals(targetClass, that.targetClass) && Objects.equals(typeParameters, that.typeParameters); } @Override public int hashCode() { - return Objects.hash(targetType, typeParameters); + return Objects.hash(targetClass, typeParameters); } } private static class PainlessMethodCacheKey { - private final Class targetType; + private final Class targetClass; private final String methodName; + private final Class returnType; private final List> typeParameters; - private PainlessMethodCacheKey(Class targetType, String methodName, List> typeParameters) { - this.targetType = targetType; + private PainlessMethodCacheKey(Class targetClass, String methodName, Class returnType, List> typeParameters) { + this.targetClass = targetClass; this.methodName = methodName; + this.returnType = returnType; this.typeParameters = Collections.unmodifiableList(typeParameters); } @@ -106,25 +109,26 @@ public boolean equals(Object object) { PainlessMethodCacheKey that = (PainlessMethodCacheKey)object; - return Objects.equals(targetType, that.targetType) && + return Objects.equals(targetClass, that.targetClass) && Objects.equals(methodName, that.methodName) && + Objects.equals(returnType, that.returnType) && Objects.equals(typeParameters, that.typeParameters); } @Override public int hashCode() { - return Objects.hash(targetType, methodName, typeParameters); + return Objects.hash(targetClass, methodName, returnType, typeParameters); } } private static class PainlessFieldCacheKey { - private final Class targetType; + private final Class targetClass; private final String fieldName; private final Class typeParameter; - private PainlessFieldCacheKey(Class targetType, String fieldName, Class typeParameter) { - this.targetType = targetType; + private PainlessFieldCacheKey(Class targetClass, String fieldName, Class typeParameter) { + this.targetClass = targetClass; this.fieldName = fieldName; this.typeParameter = typeParameter; } @@ -141,20 +145,61 @@ public boolean equals(Object object) { PainlessFieldCacheKey that = (PainlessFieldCacheKey) object; - return Objects.equals(targetType, that.targetType) && + return Objects.equals(targetClass, that.targetClass) && Objects.equals(fieldName, that.fieldName) && Objects.equals(typeParameter, that.typeParameter); } @Override public int hashCode() { - return Objects.hash(targetType, fieldName, typeParameter); + return Objects.hash(targetClass, fieldName, typeParameter); } } - private static final Map painlessConstuctorCache = new HashMap<>(); - private static final Map painlessMethodCache = new HashMap<>(); - private static final Map painlessFieldCache = new HashMap<>(); + private static class PainlessBindingCacheKey { + + private final Class targetClass; + private final String methodName; + private final Class methodReturnType; + private final List> methodTypeParameters; + + private PainlessBindingCacheKey(Class targetClass, + String methodName, Class returnType, List> typeParameters) { + + this.targetClass = targetClass; + this.methodName = methodName; + this.methodReturnType = returnType; + this.methodTypeParameters = Collections.unmodifiableList(typeParameters); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + PainlessBindingCacheKey that = (PainlessBindingCacheKey)object; + + return Objects.equals(targetClass, that.targetClass) && + Objects.equals(methodName, that.methodName) && + Objects.equals(methodReturnType, that.methodReturnType) && + Objects.equals(methodTypeParameters, that.methodTypeParameters); + } + + @Override + public int hashCode() { + return Objects.hash(targetClass, methodName, methodReturnType, methodTypeParameters); + } + } + + private static final Map painlessConstructorCache = new HashMap<>(); + private static final Map painlessMethodCache = new HashMap<>(); + private static final Map painlessFieldCache = new HashMap<>(); + private static final Map painlessBindingCache = new HashMap<>(); private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("^[_a-zA-Z][._a-zA-Z0-9]*$"); private static final Pattern METHOD_NAME_PATTERN = Pattern.compile("^[_a-zA-Z][_a-zA-Z0-9]*$"); @@ -197,6 +242,14 @@ public static PainlessLookup buildFromWhitelists(List whitelists) { targetCanonicalClassName, whitelistField.fieldName, whitelistField.canonicalTypeNameParameter); } } + + for (WhitelistBinding whitelistBinding : whitelist.whitelistBindings) { + origin = whitelistBinding.origin; + painlessLookupBuilder.addPainlessBinding( + whitelist.classLoader, whitelistBinding.targetJavaClassName, + whitelistBinding.methodName, whitelistBinding.returnCanonicalTypeName, + whitelistBinding.canonicalTypeNameParameters); + } } } catch (Exception exception) { throw new IllegalArgumentException("error loading whitelist(s) " + origin, exception); @@ -208,9 +261,13 @@ public static PainlessLookup buildFromWhitelists(List whitelists) { private final Map> canonicalClassNamesToClasses; private final Map, PainlessClassBuilder> classesToPainlessClassBuilders; + private final Map painlessMethodKeysToPainlessBindings; + public PainlessLookupBuilder() { canonicalClassNamesToClasses = new HashMap<>(); classesToPainlessClassBuilders = new HashMap<>(); + + painlessMethodKeysToPainlessBindings = new HashMap<>(); } private Class canonicalTypeNameToType(String canonicalTypeName) { @@ -392,7 +449,7 @@ public void addPainlessConstructor(Class targetClass, List> typePara MethodType methodType = methodHandle.type(); - painlessConstructor = painlessConstuctorCache.computeIfAbsent( + painlessConstructor = painlessConstructorCache.computeIfAbsent( new PainlessConstructorCacheKey(targetClass, typeParameters), key -> new PainlessConstructor(javaConstructor, typeParameters, methodHandle, methodType) ); @@ -439,7 +496,7 @@ public void addPainlessMethod(ClassLoader classLoader, String targetCanonicalCla Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); if (typeParameter == null) { - throw new IllegalArgumentException("parameter type [" + canonicalTypeNameParameter + "] not found for method " + + throw new IllegalArgumentException("type parameter [" + canonicalTypeNameParameter + "] not found for method " + "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]"); } @@ -449,7 +506,7 @@ public void addPainlessMethod(ClassLoader classLoader, String targetCanonicalCla Class returnType = canonicalTypeNameToType(returnCanonicalTypeName); if (returnType == null) { - throw new IllegalArgumentException("parameter type [" + returnCanonicalTypeName + "] not found for method " + + throw new IllegalArgumentException("return type [" + returnCanonicalTypeName + "] not found for method " + "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]"); } @@ -548,7 +605,7 @@ public void addPainlessMethod(Class targetClass, Class augmentedClass, Str MethodType methodType = methodHandle.type(); painlessMethod = painlessMethodCache.computeIfAbsent( - new PainlessMethodCacheKey(targetClass, methodName, typeParameters), + new PainlessMethodCacheKey(targetClass, methodName, returnType, typeParameters), key -> new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType)); painlessClassBuilder.staticMethods.put(painlessMethodKey, painlessMethod); @@ -588,7 +645,7 @@ public void addPainlessMethod(Class targetClass, Class augmentedClass, Str MethodType methodType = methodHandle.type(); painlessMethod = painlessMethodCache.computeIfAbsent( - new PainlessMethodCacheKey(targetClass, methodName, typeParameters), + new PainlessMethodCacheKey(targetClass, methodName, returnType, typeParameters), key -> new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType)); painlessClassBuilder.methods.put(painlessMethodKey, painlessMethod); @@ -731,6 +788,183 @@ public void addPainlessField(Class targetClass, String fieldName, Class ty } } + public void addPainlessBinding(ClassLoader classLoader, String targetJavaClassName, + String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters) { + + Objects.requireNonNull(classLoader); + Objects.requireNonNull(targetJavaClassName); + Objects.requireNonNull(methodName); + Objects.requireNonNull(returnCanonicalTypeName); + Objects.requireNonNull(canonicalTypeNameParameters); + + Class targetClass; + + try { + targetClass = Class.forName(targetJavaClassName, true, classLoader); + } catch (ClassNotFoundException cnfe) { + throw new IllegalArgumentException("class [" + targetJavaClassName + "] not found", cnfe); + } + + String targetCanonicalClassName = typeToCanonicalTypeName(targetClass); + List> typeParameters = new ArrayList<>(canonicalTypeNameParameters.size()); + + for (String canonicalTypeNameParameter : canonicalTypeNameParameters) { + Class typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter); + + if (typeParameter == null) { + throw new IllegalArgumentException("type parameter [" + canonicalTypeNameParameter + "] not found for binding " + + "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]"); + } + + typeParameters.add(typeParameter); + } + + Class returnType = canonicalTypeNameToType(returnCanonicalTypeName); + + if (returnType == null) { + throw new IllegalArgumentException("return type [" + returnCanonicalTypeName + "] not found for binding " + + "[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]"); + } + + addPainlessBinding(targetClass, methodName, returnType, typeParameters); + } + + public void addPainlessBinding(Class targetClass, String methodName, Class returnType, List> typeParameters) { + + Objects.requireNonNull(targetClass); + Objects.requireNonNull(methodName); + Objects.requireNonNull(returnType); + Objects.requireNonNull(typeParameters); + + if (targetClass == def.class) { + throw new IllegalArgumentException("cannot add binding as reserved class [" + DEF_CLASS_NAME + "]"); + } + + String targetCanonicalClassName = typeToCanonicalTypeName(targetClass); + + Constructor[] javaConstructors = targetClass.getConstructors(); + Constructor javaConstructor = null; + + for (Constructor eachJavaConstructor : javaConstructors) { + if (eachJavaConstructor.getDeclaringClass() == targetClass) { + if (javaConstructor != null) { + throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] cannot have multiple constructors"); + } + + javaConstructor = eachJavaConstructor; + } + } + + if (javaConstructor == null) { + throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] must have exactly one constructor"); + } + + int constructorTypeParametersSize = javaConstructor.getParameterCount(); + + for (int typeParameterIndex = 0; typeParameterIndex < constructorTypeParametersSize; ++typeParameterIndex) { + Class typeParameter = typeParameters.get(typeParameterIndex); + + if (isValidType(typeParameter) == false) { + throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " + + "for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]"); + } + + Class javaTypeParameter = javaConstructor.getParameterTypes()[typeParameterIndex]; + + if (isValidType(javaTypeParameter) == false) { + throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " + + "for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]"); + } + + if (javaTypeParameter != typeToJavaType(typeParameter)) { + throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(javaTypeParameter) + "] " + + "does not match the specified type parameter [" + typeToCanonicalTypeName(typeParameter) + "] " + + "for binding [[" + targetClass.getCanonicalName() + "], " + typesToCanonicalTypeNames(typeParameters) + "]"); + } + } + + if (METHOD_NAME_PATTERN.matcher(methodName).matches() == false) { + throw new IllegalArgumentException( + "invalid method name [" + methodName + "] for binding [" + targetCanonicalClassName + "]."); + } + + Method[] javaMethods = targetClass.getMethods(); + Method javaMethod = null; + + for (Method eachJavaMethod : javaMethods) { + if (eachJavaMethod.getDeclaringClass() == targetClass) { + if (javaMethod != null) { + throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] cannot have multiple methods"); + } + + javaMethod = eachJavaMethod; + } + } + + if (javaMethod == null) { + throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] must have exactly one method"); + } + + int methodTypeParametersSize = javaMethod.getParameterCount(); + + for (int typeParameterIndex = 0; typeParameterIndex < methodTypeParametersSize; ++typeParameterIndex) { + Class typeParameter = typeParameters.get(typeParameterIndex); + + if (isValidType(typeParameter) == false) { + throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " + + "for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]"); + } + + Class javaTypeParameter = javaMethod.getParameterTypes()[typeParameterIndex]; + + if (isValidType(javaTypeParameter) == false) { + throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " + + "for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]"); + } + + if (javaTypeParameter != typeToJavaType(typeParameter)) { + throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(javaTypeParameter) + "] " + + "does not match the specified type parameter [" + typeToCanonicalTypeName(typeParameter) + "] " + + "for binding [[" + targetClass.getCanonicalName() + "], " + typesToCanonicalTypeNames(typeParameters) + "]"); + } + } + + if (javaMethod.getReturnType() != typeToJavaType(returnType)) { + throw new IllegalArgumentException("return type [" + typeToCanonicalTypeName(javaMethod.getReturnType()) + "] " + + "does not match the specified returned type [" + typeToCanonicalTypeName(returnType) + "] " + + "for binding [[" + targetClass.getCanonicalName() + "], [" + methodName + "], " + + typesToCanonicalTypeNames(typeParameters) + "]"); + } + + String painlessMethodKey = buildPainlessMethodKey(methodName, constructorTypeParametersSize + methodTypeParametersSize); + PainlessBinding painlessBinding = painlessMethodKeysToPainlessBindings.get(painlessMethodKey); + + if (painlessBinding == null) { + Constructor finalJavaConstructor = javaConstructor; + Method finalJavaMethod = javaMethod; + + painlessBinding = painlessBindingCache.computeIfAbsent( + new PainlessBindingCacheKey(targetClass, methodName, returnType, typeParameters), + key -> new PainlessBinding(finalJavaConstructor, finalJavaMethod, returnType, typeParameters)); + + painlessMethodKeysToPainlessBindings.put(painlessMethodKey, painlessBinding); + } else if (painlessBinding.javaConstructor.equals(javaConstructor) == false || + painlessBinding.javaMethod.equals(javaMethod) == false || + painlessBinding.returnType != returnType || + painlessBinding.typeParameters.equals(typeParameters) == false) { + throw new IllegalArgumentException("cannot have bindings " + + "[[" + targetCanonicalClassName + "], " + + "[" + methodName + "], " + + "[" + typeToCanonicalTypeName(returnType) + "], " + + typesToCanonicalTypeNames(typeParameters) + "] and " + + "[[" + targetCanonicalClassName + "], " + + "[" + methodName + "], " + + "[" + typeToCanonicalTypeName(painlessBinding.returnType) + "], " + + typesToCanonicalTypeNames(painlessBinding.typeParameters) + "] and " + + "with the same name and arity but different constructors or methods"); + } + } + public PainlessLookup build() { copyPainlessClassMembers(); cacheRuntimeHandles(); @@ -742,7 +976,7 @@ public PainlessLookup build() { classesToPainlessClasses.put(painlessClassBuilderEntry.getKey(), painlessClassBuilderEntry.getValue().build()); } - return new PainlessLookup(canonicalClassNamesToClasses, classesToPainlessClasses); + return new PainlessLookup(canonicalClassNamesToClasses, classesToPainlessClasses, painlessMethodKeysToPainlessBindings); } private void copyPainlessClassMembers() { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java index 9dd143a40286..89462170ae5e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessMethod.java @@ -26,6 +26,7 @@ import java.util.List; public class PainlessMethod { + public final Method javaMethod; public final Class targetClass; public final Class returnType; diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java index 1f9973df1922..8ae6ad9723da 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java @@ -24,8 +24,12 @@ import org.elasticsearch.painless.Locals.LocalMethod; import org.elasticsearch.painless.Location; import org.elasticsearch.painless.MethodWriter; +import org.elasticsearch.painless.lookup.PainlessBinding; +import org.objectweb.asm.Label; +import org.objectweb.asm.Type; import org.objectweb.asm.commons.Method; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; @@ -41,6 +45,7 @@ public final class ECallLocal extends AExpression { private final List arguments; private LocalMethod method = null; + private PainlessBinding binding = null; public ECallLocal(Location location, String name, List arguments) { super(location); @@ -60,32 +65,71 @@ void extractVariables(Set variables) { void analyze(Locals locals) { method = locals.getMethod(name, arguments.size()); + if (method == null) { - throw createError(new IllegalArgumentException("Unknown call [" + name + "] with [" + arguments.size() + "] arguments.")); + binding = locals.getPainlessLookup().lookupPainlessBinding(name, arguments.size()); + + if (binding == null) { + throw createError(new IllegalArgumentException("Unknown call [" + name + "] with [" + arguments.size() + "] arguments.")); + } } + List> typeParameters = new ArrayList<>(method == null ? binding.typeParameters : method.typeParameters); + for (int argument = 0; argument < arguments.size(); ++argument) { AExpression expression = arguments.get(argument); - expression.expected = method.typeParameters.get(argument); + expression.expected = typeParameters.get(argument); expression.internal = true; expression.analyze(locals); arguments.set(argument, expression.cast(locals)); } statement = true; - actual = method.returnType; + actual = method == null ? binding.returnType : method.returnType; } @Override void write(MethodWriter writer, Globals globals) { writer.writeDebugInfo(location); - for (AExpression argument : arguments) { - argument.write(writer, globals); - } + if (method == null) { + String name = globals.addBinding(binding.javaConstructor.getDeclaringClass()); + Type type = Type.getType(binding.javaConstructor.getDeclaringClass()); + int javaConstructorParameterCount = binding.javaConstructor.getParameterCount(); + + Label nonNull = new Label(); - writer.invokeStatic(CLASS_TYPE, new Method(method.name, method.methodType.toMethodDescriptorString())); + writer.loadThis(); + writer.getField(CLASS_TYPE, name, type); + writer.ifNonNull(nonNull); + writer.loadThis(); + writer.newInstance(type); + writer.dup(); + + for (int argument = 0; argument < javaConstructorParameterCount; ++argument) { + arguments.get(argument).write(writer, globals); + } + + writer.invokeConstructor(type, Method.getMethod(binding.javaConstructor)); + writer.putField(CLASS_TYPE, name, type); + + writer.mark(nonNull); + writer.loadThis(); + writer.getField(CLASS_TYPE, name, type); + + for (int argument = 0; argument < binding.javaMethod.getParameterCount(); ++argument) { + arguments.get(argument + javaConstructorParameterCount).write(writer, globals); + } + + writer.invokeVirtual(type, Method.getMethod(binding.javaMethod)); + } else { + for (AExpression argument : arguments) { + argument.write(writer, globals); + } + + writer.invokeStatic(CLASS_TYPE, new Method(method.name, method.methodType.toMethodDescriptorString())); + } } @Override diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java index 0f7445a38c44..8abd3c7185d8 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java @@ -359,6 +359,13 @@ public void write() { clinit.endMethod(); } + // Write binding variables + for (Map.Entry> binding : globals.getBindings().entrySet()) { + String name = binding.getKey(); + String descriptor = Type.getType(binding.getValue()).getDescriptor(); + visitor.visitField(Opcodes.ACC_PRIVATE, name, descriptor, null, null).visitEnd(); + } + // Write any needsVarName methods for used variables for (org.objectweb.asm.commons.Method needsMethod : scriptClassInfo.getNeedsMethods()) { String name = needsMethod.getName(); diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt index a3ff479533bd..65f50bbc3834 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt @@ -132,24 +132,6 @@ class org.elasticsearch.index.mapper.IpFieldMapper$IpFieldType$IpScriptDocValues List getValues() } -# for testing. -# currently FeatureTest exposes overloaded constructor, field load store, and overloaded static methods -class org.elasticsearch.painless.FeatureTest no_import { - int z - () - (int,int) - int getX() - int getY() - void setX(int) - void setY(int) - boolean overloadedStatic() - boolean overloadedStatic(boolean) - Object twoFunctionsOfX(Function,Function) - void listInput(List) - int org.elasticsearch.painless.FeatureTestAugmentation getTotal() - int org.elasticsearch.painless.FeatureTestAugmentation addToTotal(int) -} - class org.elasticsearch.search.lookup.FieldLookup { def getValue() List getValues() @@ -174,4 +156,26 @@ class org.elasticsearch.index.similarity.ScriptedSimilarity$Term { class org.elasticsearch.index.similarity.ScriptedSimilarity$Doc { int getLength() float getFreq() +} + +# for testing +class org.elasticsearch.painless.FeatureTest no_import { + int z + () + (int,int) + int getX() + int getY() + void setX(int) + void setY(int) + boolean overloadedStatic() + boolean overloadedStatic(boolean) + Object twoFunctionsOfX(Function,Function) + void listInput(List) + int org.elasticsearch.painless.FeatureTestAugmentation getTotal() + int org.elasticsearch.painless.FeatureTestAugmentation addToTotal(int) +} + +# for testing +static { + int testAddWithState(int, int, int) bound_to org.elasticsearch.painless.BindingTest } \ No newline at end of file diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java new file mode 100644 index 000000000000..c6d4e1974c14 --- /dev/null +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.painless; + +import org.elasticsearch.script.ExecutableScript; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class BindingsTests extends ScriptTestCase { + + public void testBasicBinding() { + assertEquals(15, exec("testAddWithState(4, 5, 6)")); + } + + public void testRepeatedBinding() { + String script = "testAddWithState(4, 5, params.test)"; + Map params = new HashMap<>(); + ExecutableScript.Factory factory = scriptEngine.compile(null, script, ExecutableScript.CONTEXT, Collections.emptyMap()); + ExecutableScript executableScript = factory.newInstance(params); + + executableScript.setNextVar("test", 5); + assertEquals(14, executableScript.run()); + + executableScript.setNextVar("test", 4); + assertEquals(13, executableScript.run()); + + executableScript.setNextVar("test", 7); + assertEquals(16, executableScript.run()); + } + + public void testBoundBinding() { + String script = "testAddWithState(4, params.bound, params.test)"; + Map params = new HashMap<>(); + ExecutableScript.Factory factory = scriptEngine.compile(null, script, ExecutableScript.CONTEXT, Collections.emptyMap()); + ExecutableScript executableScript = factory.newInstance(params); + + executableScript.setNextVar("test", 5); + executableScript.setNextVar("bound", 1); + assertEquals(10, executableScript.run()); + + executableScript.setNextVar("test", 4); + executableScript.setNextVar("bound", 2); + assertEquals(9, executableScript.run()); + } +} From cfc003d4855601b1f1bf2be522f1c6087feeb229 Mon Sep 17 00:00:00 2001 From: Hendrik Muhs Date: Wed, 29 Aug 2018 20:28:21 +0200 Subject: [PATCH 220/283] [Rollup] Re-factor Rollup Indexer into a generic indexer for re-usability (#32743) This extracts a super class out of the rollup indexer called the AsyncTwoPhaseIterator. The implementor of it can define the query, transformation of the response, indexing and the object to persist the position/state of the indexer. The stats object used by the indexer to record progress is also now abstract, allowing the implementation provide custom stats beyond what the indexer provides. It also allows the implementation to decide how the stats are presented (leaves toXContent() up to the implementation). This should allow new projects to reuse the search-then-index persistent task that Rollup uses, but without the restrictions/baggage of how Rollup has to work internally to satisfy time-based rollups. --- .../core/indexing/AsyncTwoPhaseIndexer.java | 385 ++++++++++++++++++ .../xpack/core/indexing/IndexerJobStats.java | 114 ++++++ .../job => indexing}/IndexerState.java | 2 +- .../xpack/core/indexing/IterationResult.java | 62 +++ .../rollup/action/GetRollupJobsAction.java | 25 +- .../rollup/job/RollupIndexerJobStats.java | 70 ++++ .../xpack/core/rollup/job/RollupJobStats.java | 156 ------- .../core/rollup/job/RollupJobStatus.java | 1 + .../indexing/AsyncTwoPhaseIndexerTests.java | 143 +++++++ .../IndexerStateEnumTests.java | 2 +- .../job/JobWrapperSerializingTests.java | 4 +- .../job/RollupIndexerJobStatsTests.java | 34 ++ .../core/rollup/job/RollupJobStatsTests.java | 35 -- .../core/rollup/job/RollupJobStatusTests.java | 1 + .../xpack/rollup/job/IndexerUtils.java | 4 +- .../xpack/rollup/job/RollupIndexer.java | 358 ++-------------- .../xpack/rollup/job/RollupJobTask.java | 6 +- .../rollup/rest/RestGetRollupJobsAction.java | 8 +- .../xpack/rollup/job/IndexerUtilsTests.java | 21 +- .../job/RollupIndexerIndexingTests.java | 2 +- .../rollup/job/RollupIndexerStateTests.java | 12 +- .../xpack/rollup/job/RollupJobTaskTests.java | 2 +- .../rest-api-spec/test/rollup/get_jobs.yml | 1 + 23 files changed, 900 insertions(+), 548 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexer.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IndexerJobStats.java rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/{rollup/job => indexing}/IndexerState.java (97%) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IterationResult.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStats.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStats.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexerTests.java rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/{rollup/job => indexing}/IndexerStateEnumTests.java (98%) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStatsTests.java delete mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatsTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexer.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexer.java new file mode 100644 index 000000000000..ee0c0de97e0a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexer.java @@ -0,0 +1,385 @@ +/* + * 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.indexing; + +import org.apache.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +/** + * An abstract class that builds an index incrementally. A background job can be launched using {@link #maybeTriggerAsyncJob(long)}, + * it will create the index from the source index up to the last complete bucket that is allowed to be built (based on job position). + * Only one background job can run simultaneously and {@link #onFinish()} is called when the job + * finishes. {@link #onFailure(Exception)} is called if the job fails with an exception and {@link #onAbort()} is called if the indexer is + * aborted while a job is running. The indexer must be started ({@link #start()} to allow a background job to run when + * {@link #maybeTriggerAsyncJob(long)} is called. {@link #stop()} can be used to stop the background job without aborting the indexer. + * + * In a nutshell this is a 2 cycle engine: 1st it sends a query, 2nd it indexes documents based on the response, sends the next query, + * indexes, queries, indexes, ... until a condition lets the engine pause until the source provides new input. + * + * @param Type that defines a job position to be defined by the implementation. + */ +public abstract class AsyncTwoPhaseIndexer { + private static final Logger logger = Logger.getLogger(AsyncTwoPhaseIndexer.class.getName()); + + private final JobStats stats; + + private final AtomicReference state; + private final AtomicReference position; + private final Executor executor; + + protected AsyncTwoPhaseIndexer(Executor executor, AtomicReference initialState, + JobPosition initialPosition, JobStats jobStats) { + this.executor = executor; + this.state = initialState; + this.position = new AtomicReference<>(initialPosition); + this.stats = jobStats; + } + + /** + * Get the current state of the indexer. + */ + public IndexerState getState() { + return state.get(); + } + + /** + * Get the current position of the indexer. + */ + public JobPosition getPosition() { + return position.get(); + } + + /** + * Get the stats of this indexer. + */ + public JobStats getStats() { + return stats; + } + + /** + * Sets the internal state to {@link IndexerState#STARTED} if the previous state + * was {@link IndexerState#STOPPED}. Setting the state to STARTED allows a job + * to run in the background when {@link #maybeTriggerAsyncJob(long)} is called. + * + * @return The new state for the indexer (STARTED, INDEXING or ABORTING if the + * job was already aborted). + */ + public synchronized IndexerState start() { + state.compareAndSet(IndexerState.STOPPED, IndexerState.STARTED); + return state.get(); + } + + /** + * Sets the internal state to {@link IndexerState#STOPPING} if an async job is + * running in the background and in such case {@link #onFinish()} will be called + * as soon as the background job detects that the indexer is stopped. If there + * is no job running when this function is called, the state is directly set to + * {@link IndexerState#STOPPED} and {@link #onFinish()} will never be called. + * + * @return The new state for the indexer (STOPPED, STOPPING or ABORTING if the + * job was already aborted). + */ + public synchronized IndexerState stop() { + IndexerState currentState = state.updateAndGet(previousState -> { + if (previousState == IndexerState.INDEXING) { + return IndexerState.STOPPING; + } else if (previousState == IndexerState.STARTED) { + return IndexerState.STOPPED; + } else { + return previousState; + } + }); + return currentState; + } + + /** + * Sets the internal state to {@link IndexerState#ABORTING}. It returns false if + * an async job is running in the background and in such case {@link #onAbort} + * will be called as soon as the background job detects that the indexer is + * aborted. If there is no job running when this function is called, it returns + * true and {@link #onAbort()} will never be called. + * + * @return true if the indexer is aborted, false if a background job is running + * and abort is delayed. + */ + public synchronized boolean abort() { + IndexerState prevState = state.getAndUpdate((prev) -> IndexerState.ABORTING); + return prevState == IndexerState.STOPPED || prevState == IndexerState.STARTED; + } + + /** + * Triggers a background job that builds the index asynchronously iff + * there is no other job that runs and the indexer is started + * ({@link IndexerState#STARTED}. + * + * @param now + * The current time in milliseconds (used to limit the job to + * complete buckets) + * @return true if a job has been triggered, false otherwise + */ + public synchronized boolean maybeTriggerAsyncJob(long now) { + final IndexerState currentState = state.get(); + switch (currentState) { + case INDEXING: + case STOPPING: + case ABORTING: + logger.warn("Schedule was triggered for job [" + getJobId() + "], but prior indexer is still running."); + return false; + + case STOPPED: + logger.debug("Schedule was triggered for job [" + getJobId() + "] but job is stopped. Ignoring trigger."); + return false; + + case STARTED: + logger.debug("Schedule was triggered for job [" + getJobId() + "], state: [" + currentState + "]"); + stats.incrementNumInvocations(1); + onStartJob(now); + + if (state.compareAndSet(IndexerState.STARTED, IndexerState.INDEXING)) { + // fire off the search. Note this is async, the method will return from here + executor.execute(() -> doNextSearch(buildSearchRequest(), + ActionListener.wrap(this::onSearchResponse, exc -> finishWithFailure(exc)))); + logger.debug("Beginning to index [" + getJobId() + "], state: [" + currentState + "]"); + return true; + } else { + logger.debug("Could not move from STARTED to INDEXING state because current state is [" + state.get() + "]"); + return false; + } + + default: + logger.warn("Encountered unexpected state [" + currentState + "] while indexing"); + throw new IllegalStateException("Job encountered an illegal state [" + currentState + "]"); + } + } + + /** + * Called to get the Id of the job, used for logging. + * + * @return a string with the id of the job + */ + protected abstract String getJobId(); + + /** + * Called to process a response from the 1 search request in order to turn it into a {@link IterationResult}. + * + * @param searchResponse response from the search phase. + * @return Iteration object to be passed to indexing phase. + */ + protected abstract IterationResult doProcess(SearchResponse searchResponse); + + /** + * Called to build the next search request. + * + * @return SearchRequest to be passed to the search phase. + */ + protected abstract SearchRequest buildSearchRequest(); + + /** + * Called at startup after job has been triggered using {@link #maybeTriggerAsyncJob(long)} and the + * internal state is {@link IndexerState#STARTED}. + * + * @param now The current time in milliseconds passed through from {@link #maybeTriggerAsyncJob(long)} + */ + protected abstract void onStartJob(long now); + + /** + * Executes the {@link SearchRequest} and calls nextPhase with the + * response or the exception if an error occurs. + * + * @param request + * The search request to execute + * @param nextPhase + * Listener for the next phase + */ + protected abstract void doNextSearch(SearchRequest request, ActionListener nextPhase); + + /** + * Executes the {@link BulkRequest} and calls nextPhase with the + * response or the exception if an error occurs. + * + * @param request + * The bulk request to execute + * @param nextPhase + * Listener for the next phase + */ + protected abstract void doNextBulk(BulkRequest request, ActionListener nextPhase); + + /** + * Called periodically during the execution of a background job. Implementation + * should persists the state somewhere and continue the execution asynchronously + * using next. + * + * @param state + * The current state of the indexer + * @param position + * The current position of the indexer + * @param next + * Runnable for the next phase + */ + protected abstract void doSaveState(IndexerState state, JobPosition position, Runnable next); + + /** + * Called when a failure occurs in an async job causing the execution to stop. + * + * @param exc + * The exception + */ + protected abstract void onFailure(Exception exc); + + /** + * Called when a background job finishes. + */ + protected abstract void onFinish(); + + /** + * Called when a background job detects that the indexer is aborted causing the + * async execution to stop. + */ + protected abstract void onAbort(); + + private void finishWithFailure(Exception exc) { + doSaveState(finishAndSetState(), position.get(), () -> onFailure(exc)); + } + + private IndexerState finishAndSetState() { + return state.updateAndGet(prev -> { + switch (prev) { + case INDEXING: + // ready for another job + return IndexerState.STARTED; + + case STOPPING: + // must be started again + return IndexerState.STOPPED; + + case ABORTING: + // abort and exit + onAbort(); + return IndexerState.ABORTING; // This shouldn't matter, since onAbort() will kill the task first + + case STOPPED: + // No-op. Shouldn't really be possible to get here (should have to go through + // STOPPING + // first which will be handled) but is harmless to no-op and we don't want to + // throw exception here + return IndexerState.STOPPED; + + default: + // any other state is unanticipated at this point + throw new IllegalStateException("Indexer job encountered an illegal state [" + prev + "]"); + } + }); + } + + private void onSearchResponse(SearchResponse searchResponse) { + try { + if (checkState(getState()) == false) { + return; + } + if (searchResponse.getShardFailures().length != 0) { + throw new RuntimeException("Shard failures encountered while running indexer for job [" + getJobId() + "]: " + + Arrays.toString(searchResponse.getShardFailures())); + } + + stats.incrementNumPages(1); + IterationResult iterationResult = doProcess(searchResponse); + + if (iterationResult.isDone()) { + logger.debug("Finished indexing for job [" + getJobId() + "], saving state and shutting down."); + + // Change state first, then try to persist. This prevents in-progress + // STOPPING/ABORTING from + // being persisted as STARTED but then stop the job + doSaveState(finishAndSetState(), position.get(), this::onFinish); + return; + } + + final List docs = iterationResult.getToIndex(); + final BulkRequest bulkRequest = new BulkRequest(); + docs.forEach(bulkRequest::add); + + // TODO this might be a valid case, e.g. if implementation filters + assert bulkRequest.requests().size() > 0; + + doNextBulk(bulkRequest, ActionListener.wrap(bulkResponse -> { + // TODO we should check items in the response and move after accordingly to + // resume the failing buckets ? + if (bulkResponse.hasFailures()) { + logger.warn("Error while attempting to bulk index documents: " + bulkResponse.buildFailureMessage()); + } + stats.incrementNumOutputDocuments(bulkResponse.getItems().length); + if (checkState(getState()) == false) { + return; + } + + JobPosition newPosition = iterationResult.getPosition(); + position.set(newPosition); + + onBulkResponse(bulkResponse, newPosition); + }, exc -> finishWithFailure(exc))); + } catch (Exception e) { + finishWithFailure(e); + } + } + + private void onBulkResponse(BulkResponse response, JobPosition position) { + try { + + ActionListener listener = ActionListener.wrap(this::onSearchResponse, this::finishWithFailure); + // TODO probably something more intelligent than every-50 is needed + if (stats.getNumPages() > 0 && stats.getNumPages() % 50 == 0) { + doSaveState(IndexerState.INDEXING, position, () -> doNextSearch(buildSearchRequest(), listener)); + } else { + doNextSearch(buildSearchRequest(), listener); + } + } catch (Exception e) { + finishWithFailure(e); + } + } + + /** + * Checks the {@link IndexerState} and returns false if the execution should be + * stopped. + */ + private boolean checkState(IndexerState currentState) { + switch (currentState) { + case INDEXING: + // normal state; + return true; + + case STOPPING: + logger.info("Indexer job encountered [" + IndexerState.STOPPING + "] state, halting indexer."); + doSaveState(finishAndSetState(), getPosition(), () -> { + }); + return false; + + case STOPPED: + return false; + + case ABORTING: + logger.info("Requested shutdown of indexer for job [" + getJobId() + "]"); + onAbort(); + return false; + + default: + // Anything other than indexing, aborting or stopping is unanticipated + logger.warn("Encountered unexpected state [" + currentState + "] while indexing"); + throw new IllegalStateException("Indexer job encountered an illegal state [" + currentState + "]"); + } + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IndexerJobStats.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IndexerJobStats.java new file mode 100644 index 000000000000..2453504a5ba7 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IndexerJobStats.java @@ -0,0 +1,114 @@ +/* + * 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.indexing; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContentObject; + +import java.io.IOException; +import java.util.Objects; + +/** + * This class holds the runtime statistics of a job. The stats are not used by any internal process + * and are only for external monitoring/reference. Statistics are not persisted with the job, so if the + * allocated task is shutdown/restarted on a different node all the stats will reset. + */ +public abstract class IndexerJobStats implements ToXContentObject, Writeable { + + public static final ParseField NAME = new ParseField("job_stats"); + + protected long numPages = 0; + protected long numInputDocuments = 0; + protected long numOuputDocuments = 0; + protected long numInvocations = 0; + + public IndexerJobStats() { + } + + public IndexerJobStats(long numPages, long numInputDocuments, long numOuputDocuments, long numInvocations) { + this.numPages = numPages; + this.numInputDocuments = numInputDocuments; + this.numOuputDocuments = numOuputDocuments; + this.numInvocations = numInvocations; + } + + public IndexerJobStats(StreamInput in) throws IOException { + this.numPages = in.readVLong(); + this.numInputDocuments = in.readVLong(); + this.numOuputDocuments = in.readVLong(); + this.numInvocations = in.readVLong(); + } + + public long getNumPages() { + return numPages; + } + + public long getNumDocuments() { + return numInputDocuments; + } + + public long getNumInvocations() { + return numInvocations; + } + + public long getOutputDocuments() { + return numOuputDocuments; + } + + public void incrementNumPages(long n) { + assert(n >= 0); + numPages += n; + } + + public void incrementNumDocuments(long n) { + assert(n >= 0); + numInputDocuments += n; + } + + public void incrementNumInvocations(long n) { + assert(n >= 0); + numInvocations += n; + } + + public void incrementNumOutputDocuments(long n) { + assert(n >= 0); + numOuputDocuments += n; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVLong(numPages); + out.writeVLong(numInputDocuments); + out.writeVLong(numOuputDocuments); + out.writeVLong(numInvocations); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + IndexerJobStats that = (IndexerJobStats) other; + + return Objects.equals(this.numPages, that.numPages) + && Objects.equals(this.numInputDocuments, that.numInputDocuments) + && Objects.equals(this.numOuputDocuments, that.numOuputDocuments) + && Objects.equals(this.numInvocations, that.numInvocations); + } + + @Override + public int hashCode() { + return Objects.hash(numPages, numInputDocuments, numOuputDocuments, numInvocations); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/IndexerState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IndexerState.java similarity index 97% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/IndexerState.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IndexerState.java index 6e211c1df9e3..1b6b9a943cba 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/IndexerState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IndexerState.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.core.rollup.job; +package org.elasticsearch.xpack.core.indexing; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IterationResult.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IterationResult.java new file mode 100644 index 000000000000..1261daf185b4 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexing/IterationResult.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.indexing; + +import org.elasticsearch.action.index.IndexRequest; + +import java.util.List; + +/** + * Result object to hold the result of 1 iteration of iterative indexing. + * Acts as an interface between the implementation and the generic indexer. + */ +public class IterationResult { + + private final boolean isDone; + private final JobPosition position; + private final List toIndex; + + /** + * Constructor for the result of 1 iteration. + * + * @param toIndex the list of requests to be indexed + * @param position the extracted, persistable position of the job required for the search phase + * @param isDone true if source is exhausted and job should go to sleep + * + * Note: toIndex.empty() != isDone due to possible filtering in the specific implementation + */ + public IterationResult(List toIndex, JobPosition position, boolean isDone) { + this.toIndex = toIndex; + this.position = position; + this.isDone = isDone; + } + + /** + * Returns true if this indexing iteration is done and job should go into sleep mode. + */ + public boolean isDone() { + return isDone; + } + + /** + * Return the position of the job, a generic to be passed to the next query construction. + * + * @return the position + */ + public JobPosition getPosition() { + return position; + } + + /** + * List of requests to be passed to bulk indexing. + * + * @return List of index requests. + */ + public List getToIndex() { + return toIndex; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java index 50f793150858..7bbbf07e6dcb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java @@ -26,8 +26,8 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.tasks.Task; import org.elasticsearch.xpack.core.rollup.RollupField; +import org.elasticsearch.xpack.core.rollup.job.RollupIndexerJobStats; import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; -import org.elasticsearch.xpack.core.rollup.job.RollupJobStats; import org.elasticsearch.xpack.core.rollup.job.RollupJobStatus; import java.io.IOException; @@ -174,7 +174,14 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(JOBS.getPreferredName(), jobs); + + // XContentBuilder does not support passing the params object for Iterables + builder.field(JOBS.getPreferredName()); + builder.startArray(); + for (JobWrapper job : jobs) { + job.toXContent(builder, params); + } + builder.endArray(); builder.endObject(); return builder; } @@ -204,20 +211,20 @@ public final String toString() { public static class JobWrapper implements Writeable, ToXContentObject { private final RollupJobConfig job; - private final RollupJobStats stats; + private final RollupIndexerJobStats stats; private final RollupJobStatus status; public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, a -> new JobWrapper((RollupJobConfig) a[0], - (RollupJobStats) a[1], (RollupJobStatus)a[2])); + (RollupIndexerJobStats) a[1], (RollupJobStatus)a[2])); static { PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> RollupJobConfig.fromXContent(p, null), CONFIG); - PARSER.declareObject(ConstructingObjectParser.constructorArg(), RollupJobStats.PARSER::apply, STATS); + PARSER.declareObject(ConstructingObjectParser.constructorArg(), RollupIndexerJobStats.PARSER::apply, STATS); PARSER.declareObject(ConstructingObjectParser.constructorArg(), RollupJobStatus.PARSER::apply, STATUS); } - public JobWrapper(RollupJobConfig job, RollupJobStats stats, RollupJobStatus status) { + public JobWrapper(RollupJobConfig job, RollupIndexerJobStats stats, RollupJobStatus status) { this.job = job; this.stats = stats; this.status = status; @@ -225,7 +232,7 @@ public JobWrapper(RollupJobConfig job, RollupJobStats stats, RollupJobStatus sta public JobWrapper(StreamInput in) throws IOException { this.job = new RollupJobConfig(in); - this.stats = new RollupJobStats(in); + this.stats = new RollupIndexerJobStats(in); this.status = new RollupJobStatus(in); } @@ -240,7 +247,7 @@ public RollupJobConfig getJob() { return job; } - public RollupJobStats getStats() { + public RollupIndexerJobStats getStats() { return stats; } @@ -254,7 +261,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(CONFIG.getPreferredName()); job.toXContent(builder, params); builder.field(STATUS.getPreferredName(), status); - builder.field(STATS.getPreferredName(), stats); + builder.field(STATS.getPreferredName(), stats, params); builder.endObject(); return builder; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStats.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStats.java new file mode 100644 index 000000000000..87915671b79a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStats.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.core.rollup.job; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.xpack.core.indexing.IndexerJobStats; + +import java.io.IOException; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * The Rollup specialization of stats for the AsyncTwoPhaseIndexer. + * Note: instead of `documents_indexed`, this XContent show `rollups_indexed` + */ +public class RollupIndexerJobStats extends IndexerJobStats { + private static ParseField NUM_PAGES = new ParseField("pages_processed"); + private static ParseField NUM_INPUT_DOCUMENTS = new ParseField("documents_processed"); + private static ParseField NUM_OUTPUT_DOCUMENTS = new ParseField("rollups_indexed"); + private static ParseField NUM_INVOCATIONS = new ParseField("trigger_count"); + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>(NAME.getPreferredName(), + args -> new RollupIndexerJobStats((long) args[0], (long) args[1], (long) args[2], (long) args[3])); + + static { + PARSER.declareLong(constructorArg(), NUM_PAGES); + PARSER.declareLong(constructorArg(), NUM_INPUT_DOCUMENTS); + PARSER.declareLong(constructorArg(), NUM_OUTPUT_DOCUMENTS); + PARSER.declareLong(constructorArg(), NUM_INVOCATIONS); + } + + public RollupIndexerJobStats() { + super(); + } + + public RollupIndexerJobStats(long numPages, long numInputDocuments, long numOuputDocuments, long numInvocations) { + super(numPages, numInputDocuments, numOuputDocuments, numInvocations); + } + + public RollupIndexerJobStats(StreamInput in) throws IOException { + super(in); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(NUM_PAGES.getPreferredName(), numPages); + builder.field(NUM_INPUT_DOCUMENTS.getPreferredName(), numInputDocuments); + builder.field(NUM_OUTPUT_DOCUMENTS.getPreferredName(), numOuputDocuments); + builder.field(NUM_INVOCATIONS.getPreferredName(), numInvocations); + builder.endObject(); + return builder; + } + + public static RollupIndexerJobStats fromXContent(XContentParser parser) { + try { + return PARSER.parse(parser, null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStats.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStats.java deleted file mode 100644 index 06cfb520af55..000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStats.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.core.rollup.job; - -import org.elasticsearch.common.ParseField; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.common.xcontent.ConstructingObjectParser; -import org.elasticsearch.common.xcontent.ToXContentObject; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; - -import java.io.IOException; -import java.util.Objects; - -import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; - -/** - * This class holds the runtime statistics of a job. The stats are not used by any internal process - * and are only for external monitoring/reference. Statistics are not persisted with the job, so if the - * allocated task is shutdown/restarted on a different node all the stats will reset. - */ -public class RollupJobStats implements ToXContentObject, Writeable { - - public static final ParseField NAME = new ParseField("job_stats"); - - private static ParseField NUM_PAGES = new ParseField("pages_processed"); - private static ParseField NUM_DOCUMENTS = new ParseField("documents_processed"); - private static ParseField NUM_ROLLUPS = new ParseField("rollups_indexed"); - private static ParseField NUM_INVOCATIONS = new ParseField("trigger_count"); - - private long numPages = 0; - private long numDocuments = 0; - private long numRollups = 0; - private long numInvocations = 0; - - public static final ConstructingObjectParser PARSER = - new ConstructingObjectParser<>(NAME.getPreferredName(), - args -> new RollupJobStats((long) args[0], (long) args[1], (long) args[2], (long) args[3])); - - static { - PARSER.declareLong(constructorArg(), NUM_PAGES); - PARSER.declareLong(constructorArg(), NUM_DOCUMENTS); - PARSER.declareLong(constructorArg(), NUM_ROLLUPS); - PARSER.declareLong(constructorArg(), NUM_INVOCATIONS); - } - - public RollupJobStats() { - } - - public RollupJobStats(long numPages, long numDocuments, long numRollups, long numInvocations) { - this.numPages = numPages; - this.numDocuments = numDocuments; - this.numRollups = numRollups; - this.numInvocations = numInvocations; - } - - public RollupJobStats(StreamInput in) throws IOException { - this.numPages = in.readVLong(); - this.numDocuments = in.readVLong(); - this.numRollups = in.readVLong(); - this.numInvocations = in.readVLong(); - } - - public long getNumPages() { - return numPages; - } - - public long getNumDocuments() { - return numDocuments; - } - - public long getNumInvocations() { - return numInvocations; - } - - public long getNumRollups() { - return numRollups; - } - - public void incrementNumPages(long n) { - assert(n >= 0); - numPages += n; - } - - public void incrementNumDocuments(long n) { - assert(n >= 0); - numDocuments += n; - } - - public void incrementNumInvocations(long n) { - assert(n >= 0); - numInvocations += n; - } - - public void incrementNumRollups(long n) { - assert(n >= 0); - numRollups += n; - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeVLong(numPages); - out.writeVLong(numDocuments); - out.writeVLong(numRollups); - out.writeVLong(numInvocations); - } - - public static RollupJobStats fromXContent(XContentParser parser) { - try { - return PARSER.parse(parser, null); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(NUM_PAGES.getPreferredName(), numPages); - builder.field(NUM_DOCUMENTS.getPreferredName(), numDocuments); - builder.field(NUM_ROLLUPS.getPreferredName(), numRollups); - builder.field(NUM_INVOCATIONS.getPreferredName(), numInvocations); - builder.endObject(); - return builder; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - - if (other == null || getClass() != other.getClass()) { - return false; - } - - RollupJobStats that = (RollupJobStats) other; - - return Objects.equals(this.numPages, that.numPages) - && Objects.equals(this.numDocuments, that.numDocuments) - && Objects.equals(this.numRollups, that.numRollups) - && Objects.equals(this.numInvocations, that.numInvocations); - } - - @Override - public int hashCode() { - return Objects.hash(numPages, numDocuments, numRollups, numInvocations); - } - -} - diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatus.java index 640385c9c80d..0a2f046907c8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatus.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatus.java @@ -17,6 +17,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.persistent.PersistentTaskState; import org.elasticsearch.tasks.Task; +import org.elasticsearch.xpack.core.indexing.IndexerState; import java.io.IOException; import java.util.HashMap; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexerTests.java new file mode 100644 index 000000000000..2662e05570c6 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexing/AsyncTwoPhaseIndexerTests.java @@ -0,0 +1,143 @@ +/* + * 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.indexing; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.SearchResponseSections; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.equalTo; + +public class AsyncTwoPhaseIndexerTests extends ESTestCase { + + AtomicBoolean isFinished = new AtomicBoolean(false); + + private class MockIndexer extends AsyncTwoPhaseIndexer { + + // test the execution order + private int step; + + protected MockIndexer(Executor executor, AtomicReference initialState, Integer initialPosition) { + super(executor, initialState, initialPosition, new MockJobStats()); + } + + @Override + protected String getJobId() { + return "mock"; + } + + @Override + protected IterationResult doProcess(SearchResponse searchResponse) { + assertThat(step, equalTo(3)); + ++step; + return new IterationResult(Collections.emptyList(), 3, true); + } + + @Override + protected SearchRequest buildSearchRequest() { + assertThat(step, equalTo(1)); + ++step; + return null; + } + + @Override + protected void onStartJob(long now) { + assertThat(step, equalTo(0)); + ++step; + } + + @Override + protected void doNextSearch(SearchRequest request, ActionListener nextPhase) { + assertThat(step, equalTo(2)); + ++step; + final SearchResponseSections sections = new SearchResponseSections(new SearchHits(new SearchHit[0], 0, 0), null, null, false, + null, null, 1); + + nextPhase.onResponse(new SearchResponse(sections, null, 1, 1, 0, 0, ShardSearchFailure.EMPTY_ARRAY, null)); + } + + @Override + protected void doNextBulk(BulkRequest request, ActionListener nextPhase) { + fail("should not be called"); + } + + @Override + protected void doSaveState(IndexerState state, Integer position, Runnable next) { + assertThat(step, equalTo(4)); + ++step; + next.run(); + } + + @Override + protected void onFailure(Exception exc) { + fail(exc.getMessage()); + } + + @Override + protected void onFinish() { + assertThat(step, equalTo(5)); + ++step; + isFinished.set(true); + } + + @Override + protected void onAbort() { + } + + public int getStep() { + return step; + } + + } + + private static class MockJobStats extends IndexerJobStats { + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return null; + } + } + + public void testStateMachine() throws InterruptedException { + AtomicReference state = new AtomicReference<>(IndexerState.STOPPED); + final ExecutorService executor = Executors.newFixedThreadPool(1); + + try { + + MockIndexer indexer = new MockIndexer(executor, state, 2); + indexer.start(); + assertThat(indexer.getState(), equalTo(IndexerState.STARTED)); + assertTrue(indexer.maybeTriggerAsyncJob(System.currentTimeMillis())); + assertThat(indexer.getState(), equalTo(IndexerState.INDEXING)); + assertThat(indexer.getPosition(), equalTo(2)); + ESTestCase.awaitBusy(() -> isFinished.get()); + assertThat(indexer.getStep(), equalTo(6)); + assertThat(indexer.getStats().getNumInvocations(), equalTo(1L)); + assertThat(indexer.getStats().getNumPages(), equalTo(1L)); + assertThat(indexer.getStats().getOutputDocuments(), equalTo(0L)); + assertTrue(indexer.abort()); + } finally { + executor.shutdownNow(); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/IndexerStateEnumTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexing/IndexerStateEnumTests.java similarity index 98% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/IndexerStateEnumTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexing/IndexerStateEnumTests.java index ec17a37e23b2..329800c2f1a2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/IndexerStateEnumTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexing/IndexerStateEnumTests.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.core.rollup.job; +package org.elasticsearch.xpack.core.indexing; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/JobWrapperSerializingTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/JobWrapperSerializingTests.java index a0df63bc38dd..1ab6e6a55d49 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/JobWrapperSerializingTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/JobWrapperSerializingTests.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.indexing.IndexerState; import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers; import org.elasticsearch.xpack.core.rollup.action.GetRollupJobsAction; @@ -40,7 +41,8 @@ protected GetRollupJobsAction.JobWrapper createTestInstance() { } return new GetRollupJobsAction.JobWrapper(ConfigTestHelpers.randomRollupJobConfig(random()), - new RollupJobStats(randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeLong()), + new RollupIndexerJobStats(randomNonNegativeLong(), randomNonNegativeLong(), + randomNonNegativeLong(), randomNonNegativeLong()), new RollupJobStatus(state, Collections.emptyMap(), randomBoolean())); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStatsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStatsTests.java new file mode 100644 index 000000000000..81f31e2e5c4e --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupIndexerJobStatsTests.java @@ -0,0 +1,34 @@ +/* + * 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.rollup.job; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractSerializingTestCase; + +public class RollupIndexerJobStatsTests extends AbstractSerializingTestCase { + + @Override + protected RollupIndexerJobStats createTestInstance() { + return randomStats(); + } + + @Override + protected Writeable.Reader instanceReader() { + return RollupIndexerJobStats::new; + } + + @Override + protected RollupIndexerJobStats doParseInstance(XContentParser parser) { + return RollupIndexerJobStats.fromXContent(parser); + } + + public static RollupIndexerJobStats randomStats() { + return new RollupIndexerJobStats(randomNonNegativeLong(), randomNonNegativeLong(), + randomNonNegativeLong(), randomNonNegativeLong()); + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatsTests.java deleted file mode 100644 index 0091b21dc40d..000000000000 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatsTests.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.core.rollup.job; - -import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.test.AbstractSerializingTestCase; -import org.elasticsearch.xpack.core.rollup.job.RollupJobStats; - -public class RollupJobStatsTests extends AbstractSerializingTestCase { - - @Override - protected RollupJobStats createTestInstance() { - return randomStats(); - } - - @Override - protected Writeable.Reader instanceReader() { - return RollupJobStats::new; - } - - @Override - protected RollupJobStats doParseInstance(XContentParser parser) { - return RollupJobStats.fromXContent(parser); - } - - public static RollupJobStats randomStats() { - return new RollupJobStats(randomNonNegativeLong(), randomNonNegativeLong(), - randomNonNegativeLong(), randomNonNegativeLong()); - } -} - diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatusTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatusTests.java index 2c802a7e41dc..f46bda788bf5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatusTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/rollup/job/RollupJobStatusTests.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.indexing.IndexerState; import java.util.HashMap; import java.util.Map; diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java index 9119a5445d42..94d64b17de8f 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java @@ -17,7 +17,7 @@ import org.elasticsearch.xpack.core.rollup.RollupField; import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; import org.elasticsearch.xpack.core.rollup.job.GroupConfig; -import org.elasticsearch.xpack.core.rollup.job.RollupJobStats; +import org.elasticsearch.xpack.core.rollup.job.RollupIndexerJobStats; import org.elasticsearch.xpack.rollup.Rollup; import java.util.ArrayList; @@ -46,7 +46,7 @@ class IndexerUtils { * @param isUpgradedDocID `true` if this job is using the new ID scheme * @return A list of rolled documents derived from the response */ - static List processBuckets(CompositeAggregation agg, String rollupIndex, RollupJobStats stats, + static List processBuckets(CompositeAggregation agg, String rollupIndex, RollupIndexerJobStats stats, GroupConfig groupConfig, String jobId, boolean isUpgradedDocID) { logger.debug("Buckets: [" + agg.getBuckets().size() + "][" + jobId + "]"); diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java index 6abb7ffa5675..b1b052a3659d 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/RollupIndexer.java @@ -5,11 +5,6 @@ */ package org.elasticsearch.xpack.rollup.job; -import org.apache.log4j.Logger; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.bulk.BulkRequest; -import org.elasticsearch.action.bulk.BulkResponse; -import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.unit.TimeValue; @@ -33,20 +28,22 @@ import org.elasticsearch.search.aggregations.support.ValueType; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.xpack.core.indexing.IndexerState; +import org.elasticsearch.xpack.core.indexing.IterationResult; +import org.elasticsearch.xpack.core.indexing.AsyncTwoPhaseIndexer; import org.elasticsearch.xpack.core.rollup.RollupField; import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; import org.elasticsearch.xpack.core.rollup.job.GroupConfig; import org.elasticsearch.xpack.core.rollup.job.HistogramGroupConfig; -import org.elasticsearch.xpack.core.rollup.job.IndexerState; import org.elasticsearch.xpack.core.rollup.job.MetricConfig; +import org.elasticsearch.xpack.core.rollup.job.RollupIndexerJobStats; import org.elasticsearch.xpack.core.rollup.job.RollupJob; import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; -import org.elasticsearch.xpack.core.rollup.job.RollupJobStats; import org.elasticsearch.xpack.core.rollup.job.TermsGroupConfig; import org.joda.time.DateTimeZone; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -54,30 +51,16 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import static java.util.Collections.singletonList; -import static java.util.Collections.unmodifiableList; import static org.elasticsearch.xpack.core.rollup.RollupField.formatFieldName; /** - * An abstract class that builds a rollup index incrementally. A background job can be launched using {@link #maybeTriggerAsyncJob(long)}, - * it will create the rollup index from the source index up to the last complete bucket that is allowed to be built (based on the current - * time and the delay set on the rollup job). Only one background job can run simultaneously and {@link #onFinish()} is called when the job - * finishes. {@link #onFailure(Exception)} is called if the job fails with an exception and {@link #onAbort()} is called if the indexer is - * aborted while a job is running. The indexer must be started ({@link #start()} to allow a background job to run when - * {@link #maybeTriggerAsyncJob(long)} is called. {@link #stop()} can be used to stop the background job without aborting the indexer. + * An abstract implementation of {@link AsyncTwoPhaseIndexer} that builds a rollup index incrementally. */ -public abstract class RollupIndexer { - private static final Logger logger = Logger.getLogger(RollupIndexer.class.getName()); - +public abstract class RollupIndexer extends AsyncTwoPhaseIndexer, RollupIndexerJobStats> { static final String AGGREGATION_NAME = RollupField.NAME; private final RollupJob job; - private final RollupJobStats stats; - private final AtomicReference state; - private final AtomicReference> position; - private final Executor executor; protected final AtomicBoolean upgradedDocumentID; - private final CompositeAggregationBuilder compositeBuilder; private long maxBoundary; @@ -87,84 +70,16 @@ public abstract class RollupIndexer { * @param job The rollup job * @param initialState Initial state for the indexer * @param initialPosition The last indexed bucket of the task + * @param upgradedDocumentID whether job has updated IDs (for BWC) */ - RollupIndexer(Executor executor, RollupJob job, AtomicReference initialState, - Map initialPosition, AtomicBoolean upgradedDocumentID) { - this.executor = executor; + RollupIndexer(Executor executor, RollupJob job, AtomicReference initialState, Map initialPosition, + AtomicBoolean upgradedDocumentID) { + super(executor, initialState, initialPosition, new RollupIndexerJobStats()); this.job = job; - this.stats = new RollupJobStats(); - this.state = initialState; - this.position = new AtomicReference<>(initialPosition); this.compositeBuilder = createCompositeBuilder(job.getConfig()); this.upgradedDocumentID = upgradedDocumentID; } - /** - * Executes the {@link SearchRequest} and calls nextPhase with the response - * or the exception if an error occurs. - * - * @param request The search request to execute - * @param nextPhase Listener for the next phase - */ - protected abstract void doNextSearch(SearchRequest request, ActionListener nextPhase); - - /** - * Executes the {@link BulkRequest} and calls nextPhase with the response - * or the exception if an error occurs. - * - * @param request The bulk request to execute - * @param nextPhase Listener for the next phase - */ - protected abstract void doNextBulk(BulkRequest request, ActionListener nextPhase); - - /** - * Called periodically during the execution of a background job. Implementation should - * persists the state somewhere and continue the execution asynchronously using next. - * - * @param state The current state of the indexer - * @param position The current position of the indexer - * @param next Runnable for the next phase - */ - protected abstract void doSaveState(IndexerState state, Map position, Runnable next); - - /** - * Called when a failure occurs in an async job causing the execution to stop. - * @param exc The exception - */ - protected abstract void onFailure(Exception exc); - - /** - * Called when a background job finishes. - */ - protected abstract void onFinish(); - - /** - * Called when a background job detects that the indexer is aborted causing the async execution - * to stop. - */ - protected abstract void onAbort(); - - /** - * Get the current state of the indexer. - */ - public IndexerState getState() { - return state.get(); - } - - /** - * Get the current position of the indexer. - */ - public Map getPosition() { - return position.get(); - } - - /** - * Get the stats of this indexer. - */ - public RollupJobStats getStats() { - return stats; - } - /** * Returns if this job has upgraded it's ID scheme yet or not */ @@ -172,229 +87,28 @@ public boolean isUpgradedDocumentID() { return upgradedDocumentID.get(); } - /** - * Sets the internal state to {@link IndexerState#STARTED} if the previous state was {@link IndexerState#STOPPED}. Setting the state to - * STARTED allows a job to run in the background when {@link #maybeTriggerAsyncJob(long)} is called. - * @return The new state for the indexer (STARTED, INDEXING or ABORTING if the job was already aborted). - */ - public synchronized IndexerState start() { - state.compareAndSet(IndexerState.STOPPED, IndexerState.STARTED); - return state.get(); + @Override + protected String getJobId() { + return job.getConfig().getId(); } - /** - * Sets the internal state to {@link IndexerState#STOPPING} if an async job is running in the background and in such case - * {@link #onFinish()} will be called as soon as the background job detects that the indexer is stopped. If there is no job running when - * this function is called, the state is directly set to {@link IndexerState#STOPPED} and {@link #onFinish()} will never be called. - * @return The new state for the indexer (STOPPED, STOPPING or ABORTING if the job was already aborted). - */ - public synchronized IndexerState stop() { - IndexerState currentState = state.updateAndGet(previousState -> { - if (previousState == IndexerState.INDEXING) { - return IndexerState.STOPPING; - } else if (previousState == IndexerState.STARTED) { - return IndexerState.STOPPED; - } else { - return previousState; - } - }); - return currentState; - } - - /** - * Sets the internal state to {@link IndexerState#ABORTING}. It returns false if an async job is running in the background and in such - * case {@link #onAbort} will be called as soon as the background job detects that the indexer is aborted. If there is no job running - * when this function is called, it returns true and {@link #onAbort()} will never be called. - * @return true if the indexer is aborted, false if a background job is running and abort is delayed. - */ - public synchronized boolean abort() { - IndexerState prevState = state.getAndUpdate((prev) -> IndexerState.ABORTING); - return prevState == IndexerState.STOPPED || prevState == IndexerState.STARTED; - } - - /** - * Triggers a background job that builds the rollup index asynchronously iff there is no other job that runs - * and the indexer is started ({@link IndexerState#STARTED}. - * - * @param now The current time in milliseconds (used to limit the job to complete buckets) - * @return true if a job has been triggered, false otherwise - */ - public synchronized boolean maybeTriggerAsyncJob(long now) { - final IndexerState currentState = state.get(); - switch (currentState) { - case INDEXING: - case STOPPING: - case ABORTING: - logger.warn("Schedule was triggered for rollup job [" + job.getConfig().getId() + "], but prior indexer is still running."); - return false; - - case STOPPED: - logger.debug("Schedule was triggered for rollup job [" + job.getConfig().getId() - + "] but job is stopped. Ignoring trigger."); - return false; - - case STARTED: - logger.debug("Schedule was triggered for rollup job [" + job.getConfig().getId() + "], state: [" + currentState + "]"); - // Only valid time to start indexing is when we are STARTED but not currently INDEXING. - stats.incrementNumInvocations(1); - - // rounds the current time to its current bucket based on the date histogram interval. - // this is needed to exclude buckets that can still receive new documents. - DateHistogramGroupConfig dateHisto = job.getConfig().getGroupConfig().getDateHistogram(); - long rounded = dateHisto.createRounding().round(now); - if (dateHisto.getDelay() != null) { - // if the job has a delay we filter all documents that appear before it. - maxBoundary = rounded - TimeValue.parseTimeValue(dateHisto.getDelay().toString(), "").millis(); - } else { - maxBoundary = rounded; - } - - if (state.compareAndSet(IndexerState.STARTED, IndexerState.INDEXING)) { - // fire off the search. Note this is async, the method will return from here - executor.execute(() -> doNextSearch(buildSearchRequest(), - ActionListener.wrap(this::onSearchResponse, exc -> finishWithFailure(exc)))); - logger.debug("Beginning to rollup [" + job.getConfig().getId() + "], state: [" + currentState + "]"); - return true; - } else { - logger.debug("Could not move from STARTED to INDEXING state because current state is [" + state.get() + "]"); - return false; - } - - default: - logger.warn("Encountered unexpected state [" + currentState + "] while indexing"); - throw new IllegalStateException("Rollup job encountered an illegal state [" + currentState + "]"); - } - } - - /** - * Checks the {@link IndexerState} and returns false if the execution - * should be stopped. - */ - private boolean checkState(IndexerState currentState) { - switch (currentState) { - case INDEXING: - // normal state; - return true; - - case STOPPING: - logger.info("Rollup job encountered [" + IndexerState.STOPPING + "] state, halting indexer."); - doSaveState(finishAndSetState(), getPosition(), () -> {}); - return false; - - case STOPPED: - return false; - - case ABORTING: - logger.info("Requested shutdown of indexer for job [" + job.getConfig().getId() + "]"); - onAbort(); - return false; - - default: - // Anything other than indexing, aborting or stopping is unanticipated - logger.warn("Encountered unexpected state [" + currentState + "] while indexing"); - throw new IllegalStateException("Rollup job encountered an illegal state [" + currentState + "]"); - } - } - - private void onBulkResponse(BulkResponse response, Map after) { - // TODO we should check items in the response and move after accordingly to resume the failing buckets ? - stats.incrementNumRollups(response.getItems().length); - if (response.hasFailures()) { - logger.warn("Error while attempting to bulk index rollup documents: " + response.buildFailureMessage()); - } - try { - if (checkState(getState()) == false) { - return ; - } - position.set(after); - ActionListener listener = ActionListener.wrap(this::onSearchResponse, this::finishWithFailure); - // TODO probably something more intelligent than every-50 is needed - if (stats.getNumPages() > 0 && stats.getNumPages() % 50 == 0) { - doSaveState(IndexerState.INDEXING, after, () -> doNextSearch(buildSearchRequest(), listener)); - } else { - doNextSearch(buildSearchRequest(), listener); - } - } catch (Exception e) { - finishWithFailure(e); + @Override + protected void onStartJob(long now) { + // this is needed to exclude buckets that can still receive new documents. + DateHistogramGroupConfig dateHisto = job.getConfig().getGroupConfig().getDateHistogram(); + long rounded = dateHisto.createRounding().round(now); + if (dateHisto.getDelay() != null) { + // if the job has a delay we filter all documents that appear before it. + maxBoundary = rounded - TimeValue.parseTimeValue(dateHisto.getDelay().toString(), "").millis(); + } else { + maxBoundary = rounded; } } - private void onSearchResponse(SearchResponse searchResponse) { - try { - if (checkState(getState()) == false) { - return ; - } - if (searchResponse.getShardFailures().length != 0) { - throw new RuntimeException("Shard failures encountered while running indexer for rollup job [" - + job.getConfig().getId() + "]: " + Arrays.toString(searchResponse.getShardFailures())); - } - final CompositeAggregation response = searchResponse.getAggregations().get(AGGREGATION_NAME); - if (response == null) { - throw new IllegalStateException("Missing composite response for query: " + compositeBuilder.toString()); - } - stats.incrementNumPages(1); - if (response.getBuckets().isEmpty()) { - // this is the end... - logger.debug("Finished indexing for job [" + job.getConfig().getId() + "], saving state and shutting down."); - - // Change state first, then try to persist. This prevents in-progress STOPPING/ABORTING from - // being persisted as STARTED but then stop the job - doSaveState(finishAndSetState(), position.get(), this::onFinish); - return; - } - - final BulkRequest bulkRequest = new BulkRequest(); + @Override + protected SearchRequest buildSearchRequest() { // Indexer is single-threaded, and only place that the ID scheme can get upgraded is doSaveState(), so // we can pass down the boolean value rather than the atomic here - final List docs = IndexerUtils.processBuckets(response, job.getConfig().getRollupIndex(), - stats, job.getConfig().getGroupConfig(), job.getConfig().getId(), upgradedDocumentID.get()); - docs.forEach(bulkRequest::add); - assert bulkRequest.requests().size() > 0; - doNextBulk(bulkRequest, - ActionListener.wrap( - bulkResponse -> onBulkResponse(bulkResponse, response.afterKey()), - exc -> finishWithFailure(exc) - ) - ); - } catch(Exception e) { - finishWithFailure(e); - } - } - - private void finishWithFailure(Exception exc) { - doSaveState(finishAndSetState(), position.get(), () -> onFailure(exc)); - } - - private IndexerState finishAndSetState() { - return state.updateAndGet( - prev -> { - switch (prev) { - case INDEXING: - // ready for another job - return IndexerState.STARTED; - - case STOPPING: - // must be started again - return IndexerState.STOPPED; - - case ABORTING: - // abort and exit - onAbort(); - return IndexerState.ABORTING; // This shouldn't matter, since onAbort() will kill the task first - - case STOPPED: - // No-op. Shouldn't really be possible to get here (should have to go through STOPPING - // first which will be handled) but is harmless to no-op and we don't want to throw exception here - return IndexerState.STOPPED; - - default: - // any other state is unanticipated at this point - throw new IllegalStateException("Rollup job encountered an illegal state [" + prev + "]"); - } - }); - } - - private SearchRequest buildSearchRequest() { final Map position = getPosition(); SearchSourceBuilder searchSource = new SearchSourceBuilder() .size(0) @@ -405,6 +119,16 @@ private SearchRequest buildSearchRequest() { return new SearchRequest(job.getConfig().getIndexPattern()).source(searchSource); } + @Override + protected IterationResult> doProcess(SearchResponse searchResponse) { + final CompositeAggregation response = searchResponse.getAggregations().get(AGGREGATION_NAME); + + return new IterationResult<>( + IndexerUtils.processBuckets(response, job.getConfig().getRollupIndex(), getStats(), + job.getConfig().getGroupConfig(), job.getConfig().getId(), upgradedDocumentID.get()), + response.afterKey(), response.getBuckets().isEmpty()); + } + /** * Creates a skeleton {@link CompositeAggregationBuilder} from the provided job config. * @param config The config for the job. @@ -481,7 +205,7 @@ public static List> createValueSourceBuilders(fi final TermsGroupConfig terms = groupConfig.getTerms(); builders.addAll(createValueSourceBuilders(terms)); } - return unmodifiableList(builders); + return Collections.unmodifiableList(builders); } public static List> createValueSourceBuilders(final DateHistogramGroupConfig dateHistogram) { @@ -491,7 +215,7 @@ public static List> createValueSourceBuilders(fi dateHistogramBuilder.dateHistogramInterval(dateHistogram.getInterval()); dateHistogramBuilder.field(dateHistogramField); dateHistogramBuilder.timeZone(toDateTimeZone(dateHistogram.getTimeZone())); - return singletonList(dateHistogramBuilder); + return Collections.singletonList(dateHistogramBuilder); } public static List> createValueSourceBuilders(final HistogramGroupConfig histogram) { @@ -506,7 +230,7 @@ public static List> createValueSourceBuilders(fi builders.add(histogramBuilder); } } - return unmodifiableList(builders); + return Collections.unmodifiableList(builders); } public static List> createValueSourceBuilders(final TermsGroupConfig terms) { @@ -520,7 +244,7 @@ public static List> createValueSourceBuilders(fi builders.add(termsBuilder); } } - return unmodifiableList(builders); + return Collections.unmodifiableList(builders); } /** @@ -564,7 +288,7 @@ static List createAggregationBuilders(final List docs = IndexerUtils.processBuckets(composite, "foo", new RollupJobStats(), groupConfig, "foo", false); + List docs = IndexerUtils.processBuckets(composite, "foo", new RollupIndexerJobStats(), groupConfig, "foo", false); assertThat(docs.size(), equalTo(1)); assertThat(docs.get(0).id(), equalTo("1237859798")); } @@ -406,7 +406,7 @@ public void testKeyOrderingNewID() { }); GroupConfig groupConfig = new GroupConfig(randomDateHistogramGroupConfig(random()), new HistogramGroupConfig(1L, "abc"), null); - List docs = IndexerUtils.processBuckets(composite, "foo", new RollupJobStats(), groupConfig, "foo", true); + List docs = IndexerUtils.processBuckets(composite, "foo", new RollupIndexerJobStats(), groupConfig, "foo", true); assertThat(docs.size(), equalTo(1)); assertThat(docs.get(0).id(), equalTo("foo$c9LcrFqeFW92uN_Z7sv1hA")); } @@ -456,7 +456,7 @@ public void testKeyOrderingNewIDLong() { }); GroupConfig groupConfig = new GroupConfig(randomDateHistogramGroupConfig(random()), new HistogramGroupConfig(1, "abc"), null); - List docs = IndexerUtils.processBuckets(composite, "foo", new RollupJobStats(), groupConfig, "foo", true); + List docs = IndexerUtils.processBuckets(composite, "foo", new RollupIndexerJobStats(), groupConfig, "foo", true); assertThat(docs.size(), equalTo(1)); assertThat(docs.get(0).id(), equalTo("foo$VAFKZpyaEqYRPLyic57_qw")); } @@ -483,14 +483,15 @@ public void testNullKeys() { }); GroupConfig groupConfig = new GroupConfig(randomDateHistogramGroupConfig(random()), randomHistogramGroupConfig(random()), null); - List docs = IndexerUtils.processBuckets(composite, "foo", new RollupJobStats(), groupConfig, "foo", randomBoolean()); + List docs = IndexerUtils.processBuckets(composite, "foo", new RollupIndexerJobStats(), + groupConfig, "foo", randomBoolean()); assertThat(docs.size(), equalTo(1)); assertFalse(Strings.isNullOrEmpty(docs.get(0).id())); } public void testMissingBuckets() throws IOException { String indexName = randomAlphaOfLengthBetween(1, 10); - RollupJobStats stats= new RollupJobStats(0, 0, 0, 0); + RollupIndexerJobStats stats = new RollupIndexerJobStats(0, 0, 0, 0); String metricField = "metric_field"; String valueField = "value_field"; diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java index 6d29ee9f9ba6..55f1cfbdbb29 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java @@ -50,10 +50,10 @@ import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers; import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig; import org.elasticsearch.xpack.core.rollup.job.GroupConfig; -import org.elasticsearch.xpack.core.rollup.job.IndexerState; import org.elasticsearch.xpack.core.rollup.job.MetricConfig; import org.elasticsearch.xpack.core.rollup.job.RollupJob; import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; +import org.elasticsearch.xpack.core.indexing.IndexerState; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.Before; diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerStateTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerStateTests.java index 955dcbc2beb4..c74ecbadf4fb 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerStateTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerStateTests.java @@ -23,8 +23,8 @@ import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers; import org.elasticsearch.xpack.core.rollup.RollupField; import org.elasticsearch.xpack.core.rollup.job.GroupConfig; -import org.elasticsearch.xpack.core.rollup.job.IndexerState; import org.elasticsearch.xpack.core.rollup.job.RollupJob; +import org.elasticsearch.xpack.core.indexing.IndexerState; import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig; import org.mockito.stubbing.Answer; @@ -639,7 +639,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws assertThat(indexer.getStats().getNumPages(), equalTo(1L)); // Note: no docs were indexed - assertThat(indexer.getStats().getNumRollups(), equalTo(0L)); + assertThat(indexer.getStats().getOutputDocuments(), equalTo(0L)); assertTrue(indexer.abort()); } finally { executor.shutdownNow(); @@ -743,7 +743,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws assertThat(indexer.getStats().getNumPages(), equalTo(1L)); // Note: no docs were indexed - assertThat(indexer.getStats().getNumRollups(), equalTo(0L)); + assertThat(indexer.getStats().getOutputDocuments(), equalTo(0L)); assertTrue(indexer.abort()); } finally { executor.shutdownNow(); @@ -763,7 +763,7 @@ public void testSearchShardFailure() throws Exception { Function bulkFunction = bulkRequest -> new BulkResponse(new BulkItemResponse[0], 100); Consumer failureConsumer = e -> { - assertThat(e.getMessage(), startsWith("Shard failures encountered while running indexer for rollup job")); + assertThat(e.getMessage(), startsWith("Shard failures encountered while running indexer for job")); isFinished.set(true); }; @@ -786,7 +786,7 @@ public void testSearchShardFailure() throws Exception { // Note: no pages processed, no docs were indexed assertThat(indexer.getStats().getNumPages(), equalTo(0L)); - assertThat(indexer.getStats().getNumRollups(), equalTo(0L)); + assertThat(indexer.getStats().getOutputDocuments(), equalTo(0L)); assertTrue(indexer.abort()); } finally { executor.shutdownNow(); @@ -896,7 +896,7 @@ protected void doNextBulk(BulkRequest request, ActionListener next assertThat(indexer.getStats().getNumPages(), equalTo(1L)); // Note: no docs were indexed - assertThat(indexer.getStats().getNumRollups(), equalTo(0L)); + assertThat(indexer.getStats().getOutputDocuments(), equalTo(0L)); assertTrue(indexer.abort()); } finally { executor.shutdownNow(); diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java index 9a75d6fc6759..a47d057b5d5b 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupJobTaskTests.java @@ -20,11 +20,11 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.indexing.IndexerState; import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers; import org.elasticsearch.xpack.core.rollup.RollupField; import org.elasticsearch.xpack.core.rollup.action.StartRollupJobAction; import org.elasticsearch.xpack.core.rollup.action.StopRollupJobAction; -import org.elasticsearch.xpack.core.rollup.job.IndexerState; import org.elasticsearch.xpack.core.rollup.job.RollupJob; import org.elasticsearch.xpack.core.rollup.job.RollupJobStatus; import org.elasticsearch.xpack.core.scheduler.SchedulerEngine; diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/get_jobs.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/get_jobs.yml index f3fa8114ddbd..759ddbad2b46 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/get_jobs.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/rollup/get_jobs.yml @@ -210,3 +210,4 @@ setup: job_state: "stopped" upgraded_doc_id: true + From 92bd7242a36be6511f463722bedbe7a85f176f2e Mon Sep 17 00:00:00 2001 From: Matt Weber Date: Wed, 29 Aug 2018 12:19:58 -0700 Subject: [PATCH 221/283] Fix classpath security checks for external tests. (#33066) This commit checks that when we manually add a class to the codebase map, that it does in-fact not exist on the classpath in a jar. This will only be true if we are using the test framework externally such as when a user develops a plugin. --- .../org/elasticsearch/bootstrap/BootstrapForTesting.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java b/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java index 35dac2e99e00..c50e7cf066b8 100644 --- a/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java +++ b/test/framework/src/main/java/org/elasticsearch/bootstrap/BootstrapForTesting.java @@ -177,8 +177,11 @@ public boolean implies(ProtectionDomain domain, Permission permission) { private static void addClassCodebase(Map codebases, String name, String classname) { try { Class clazz = BootstrapForTesting.class.getClassLoader().loadClass(classname); - if (codebases.put(name, clazz.getProtectionDomain().getCodeSource().getLocation()) != null) { - throw new IllegalStateException("Already added " + name + " codebase for testing"); + URL location = clazz.getProtectionDomain().getCodeSource().getLocation(); + if (location.toString().endsWith(".jar") == false) { + if (codebases.put(name, location) != null) { + throw new IllegalStateException("Already added " + name + " codebase for testing"); + } } } catch (ClassNotFoundException e) { // no class, fall through to not add. this can happen for any tests that do not include From 0f22dbb1cce49e7a1dbca76e4f684140c57fdb09 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 29 Aug 2018 15:56:13 -0400 Subject: [PATCH 222/283] Apply settings filter to get cluster settings API (#33247) Some settings have filters applied to them and we use this in logs and the get nodes info API. For consistency, we should apply this in the get cluster settings API too. --- .../cluster/RestClusterGetSettingsAction.java | 22 +++--- .../RestClusterGetSettingsActionTests.java | 70 +++++++++++++++++++ 2 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsActionTests.java diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsAction.java index b452b62eb5e9..746bb643bf62 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsAction.java @@ -87,13 +87,19 @@ public boolean canTripCircuitBreaker() { private XContentBuilder renderResponse(ClusterState state, boolean renderDefaults, XContentBuilder builder, ToXContent.Params params) throws IOException { - return - new ClusterGetSettingsResponse( - state.metaData().persistentSettings(), - state.metaData().transientSettings(), - renderDefaults ? - settingsFilter.filter(clusterSettings.diff(state.metaData().settings(), this.settings)) : - Settings.EMPTY - ).toXContent(builder, params); + return response(state, renderDefaults, settingsFilter, clusterSettings, settings).toXContent(builder, params); } + + static ClusterGetSettingsResponse response( + final ClusterState state, + final boolean renderDefaults, + final SettingsFilter settingsFilter, + final ClusterSettings clusterSettings, + final Settings settings) { + return new ClusterGetSettingsResponse( + settingsFilter.filter(state.metaData().persistentSettings()), + settingsFilter.filter(state.metaData().transientSettings()), + renderDefaults ? settingsFilter.filter(clusterSettings.diff(state.metaData().settings(), settings)) : Settings.EMPTY); + } + } diff --git a/server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsActionTests.java new file mode 100644 index 000000000000..29b19739e758 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/rest/action/admin/cluster/RestClusterGetSettingsActionTests.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.rest.action.admin.cluster; + +import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.test.ESTestCase; + +import java.util.Collections; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public class RestClusterGetSettingsActionTests extends ESTestCase { + + public void testFilterPersistentSettings() { + runTestFilterSettingsTest(MetaData.Builder::persistentSettings, ClusterGetSettingsResponse::getPersistentSettings); + } + + public void testFilterTransientSettings() { + runTestFilterSettingsTest(MetaData.Builder::transientSettings, ClusterGetSettingsResponse::getTransientSettings); + } + + private void runTestFilterSettingsTest( + final BiConsumer md, final Function s) { + final MetaData.Builder mdBuilder = new MetaData.Builder(); + final Settings settings = Settings.builder().put("foo.filtered", "bar").put("foo.non_filtered", "baz").build(); + md.accept(mdBuilder, settings); + final ClusterState.Builder builder = new ClusterState.Builder(ClusterState.EMPTY_STATE).metaData(mdBuilder); + final SettingsFilter filter = new SettingsFilter(Settings.EMPTY, Collections.singleton("foo.filtered")); + final Setting.Property[] properties = {Setting.Property.Dynamic, Setting.Property.Filtered, Setting.Property.NodeScope}; + final Set> settingsSet = Stream.concat( + ClusterSettings.BUILT_IN_CLUSTER_SETTINGS.stream(), + Stream.concat( + Stream.of(Setting.simpleString("foo.filtered", properties)), + Stream.of(Setting.simpleString("foo.non_filtered", properties)))) + .collect(Collectors.toSet()); + final ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, settingsSet); + final ClusterGetSettingsResponse response = + RestClusterGetSettingsAction.response(builder.build(), randomBoolean(), filter, clusterSettings, Settings.EMPTY); + assertFalse(s.apply(response).hasValue("foo.filtered")); + assertTrue(s.apply(response).hasValue("foo.non_filtered")); + } + +} From 13880bd8c1ecac163a80e46a8fea943e6be8516c Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Wed, 29 Aug 2018 22:42:08 +0200 Subject: [PATCH 223/283] Watcher: Reload properly on remote shard change (#33167) When a node dies that carries a watcher shard or a shard is relocated to another node, then watcher needs not only trigger a reload on the node where the shard relocation happened, but also on other nodes where copies of this shard, as different watches may need to be loaded. This commit takes the change of remote nodes into account by not only storing the local shard allocation ids in the WatcherLifeCycleService, but storing a list of ShardRoutings based on the local active shards. This also fixes some tests, which had a wrong assumption. Using `TestShardRouting.newShardRouting` in our tests for cluster state creation led to the issue of always creating new allocation ids which implicitely lead to a reload. --- .../watcher/WatcherLifeCycleService.java | 31 ++++--- .../watcher/WatcherLifeCycleServiceTests.java | 88 +++++++++++++++++-- 2 files changed, 98 insertions(+), 21 deletions(-) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java index 620d575fc802..fd46ce67bbe6 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java @@ -11,7 +11,6 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.cluster.routing.AllocationId; import org.elasticsearch.cluster.routing.RoutingNode; import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.service.ClusterService; @@ -22,13 +21,16 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.gateway.GatewayService; +import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.xpack.core.watcher.WatcherMetaData; import org.elasticsearch.xpack.core.watcher.WatcherState; import org.elasticsearch.xpack.core.watcher.watch.Watch; import org.elasticsearch.xpack.watcher.watch.WatchStoreUtils; import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -45,7 +47,7 @@ public class WatcherLifeCycleService extends AbstractComponent implements Cluste Setting.boolSetting("xpack.watcher.require_manual_start", false, Property.NodeScope); private final AtomicReference state = new AtomicReference<>(WatcherState.STARTED); - private final AtomicReference> previousAllocationIds = new AtomicReference<>(Collections.emptyList()); + private final AtomicReference> previousShardRoutings = new AtomicReference<>(Collections.emptyList()); private final boolean requireManualStart; private volatile boolean shutDown = false; // indicates that the node has been shutdown and we should never start watcher after this. private volatile WatcherService watcherService; @@ -144,15 +146,20 @@ public void clusterChanged(ClusterChangedEvent event) { return; } - List currentAllocationIds = localShards.stream() - .map(ShardRouting::allocationId) - .map(AllocationId::getId) - .sorted() + // also check if non local shards have changed, as loosing a shard on a + // remote node or adding a replica on a remote node needs to trigger a reload too + Set localShardIds = localShards.stream().map(ShardRouting::shardId).collect(Collectors.toSet()); + List allShards = event.state().routingTable().index(watchIndex).shardsWithState(STARTED); + allShards.addAll(event.state().routingTable().index(watchIndex).shardsWithState(RELOCATING)); + List localAffectedShardRoutings = allShards.stream() + .filter(shardRouting -> localShardIds.contains(shardRouting.shardId())) + // shardrouting is not comparable, so we need some order mechanism + .sorted(Comparator.comparing(ShardRouting::hashCode)) .collect(Collectors.toList()); - if (previousAllocationIds.get().equals(currentAllocationIds) == false) { + if (previousShardRoutings.get().equals(localAffectedShardRoutings) == false) { if (watcherService.validate(event.state())) { - previousAllocationIds.set(Collections.unmodifiableList(currentAllocationIds)); + previousShardRoutings.set(localAffectedShardRoutings); if (state.get() == WatcherState.STARTED) { watcherService.reload(event.state(), "new local watcher shard allocation ids"); } else if (state.get() == WatcherState.STOPPED) { @@ -187,13 +194,13 @@ private boolean isWatcherStoppedManually(ClusterState state) { * @return true, if existing allocation ids were cleaned out, false otherwise */ private boolean clearAllocationIds() { - List previousIds = previousAllocationIds.getAndSet(Collections.emptyList()); - return previousIds.equals(Collections.emptyList()) == false; + List previousIds = previousShardRoutings.getAndSet(Collections.emptyList()); + return previousIds.isEmpty() == false; } // for testing purposes only - List allocationIds() { - return previousAllocationIds.get(); + List shardRoutings() { + return previousShardRoutings.get(); } public WatcherState getState() { diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java index 700901753d4a..384338af5a28 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleServiceTests.java @@ -254,9 +254,12 @@ public void testReplicaWasAddedOrRemoved() { .add(newNode("node_2")) .build(); + ShardRouting firstShardOnSecondNode = TestShardRouting.newShardRouting(shardId, "node_2", true, STARTED); + ShardRouting secondShardOnFirstNode = TestShardRouting.newShardRouting(secondShardId, "node_1", true, STARTED); + IndexRoutingTable previousWatchRoutingTable = IndexRoutingTable.builder(watchIndex) - .addShard(TestShardRouting.newShardRouting(secondShardId, "node_1", true, STARTED)) - .addShard(TestShardRouting.newShardRouting(shardId, "node_2", true, STARTED)) + .addShard(secondShardOnFirstNode) + .addShard(firstShardOnSecondNode) .build(); IndexMetaData indexMetaData = IndexMetaData.builder(Watch.INDEX) @@ -273,10 +276,19 @@ public void testReplicaWasAddedOrRemoved() { .metaData(MetaData.builder().put(indexMetaData, false)) .build(); + // add a replica in the local node + boolean addShardOnLocalNode = randomBoolean(); + final ShardRouting addedShardRouting; + if (addShardOnLocalNode) { + addedShardRouting = TestShardRouting.newShardRouting(shardId, "node_1", false, STARTED); + } else { + addedShardRouting = TestShardRouting.newShardRouting(secondShardId, "node_2", false, STARTED); + } + IndexRoutingTable currentWatchRoutingTable = IndexRoutingTable.builder(watchIndex) - .addShard(TestShardRouting.newShardRouting(shardId, "node_1", false, STARTED)) - .addShard(TestShardRouting.newShardRouting(secondShardId, "node_1", true, STARTED)) - .addShard(TestShardRouting.newShardRouting(shardId, "node_2", true, STARTED)) + .addShard(secondShardOnFirstNode) + .addShard(firstShardOnSecondNode) + .addShard(addedShardRouting) .build(); ClusterState stateWithReplicaAdded = ClusterState.builder(new ClusterName("my-cluster")) @@ -477,7 +489,67 @@ public void testDataNodeWithoutDataCanStart() { assertThat(lifeCycleService.getState(), is(WatcherState.STARTED)); } - private ClusterState startWatcher() { + // this emulates a node outage somewhere in the cluster that carried a watcher shard + // the number of shards remains the same, but we need to ensure that watcher properly reloads + // previously we only checked the local shard allocations, but we also need to check if shards in the cluster have changed + public void testWatcherReloadsOnNodeOutageWithWatcherShard() { + Index watchIndex = new Index(Watch.INDEX, "foo"); + ShardId shardId = new ShardId(watchIndex, 0); + String localNodeId = randomFrom("node_1", "node_2"); + String outageNodeId = localNodeId.equals("node_1") ? "node_2" : "node_1"; + DiscoveryNodes previousDiscoveryNodes = new DiscoveryNodes.Builder().masterNodeId(localNodeId).localNodeId(localNodeId) + .add(newNode(localNodeId)) + .add(newNode(outageNodeId)) + .build(); + + ShardRouting replicaShardRouting = TestShardRouting.newShardRouting(shardId, localNodeId, false, STARTED); + ShardRouting primartShardRouting = TestShardRouting.newShardRouting(shardId, outageNodeId, true, STARTED); + IndexRoutingTable previousWatchRoutingTable = IndexRoutingTable.builder(watchIndex) + .addShard(replicaShardRouting) + .addShard(primartShardRouting) + .build(); + + IndexMetaData indexMetaData = IndexMetaData.builder(Watch.INDEX) + .settings(Settings.builder() + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.INDEX_FORMAT_SETTING.getKey(), 6) + ).build(); + + ClusterState previousState = ClusterState.builder(new ClusterName("my-cluster")) + .nodes(previousDiscoveryNodes) + .routingTable(RoutingTable.builder().add(previousWatchRoutingTable).build()) + .metaData(MetaData.builder().put(indexMetaData, false)) + .build(); + + ShardRouting nowPrimaryShardRouting = replicaShardRouting.moveActiveReplicaToPrimary(); + IndexRoutingTable currentWatchRoutingTable = IndexRoutingTable.builder(watchIndex) + .addShard(nowPrimaryShardRouting) + .build(); + + DiscoveryNodes currentDiscoveryNodes = new DiscoveryNodes.Builder().masterNodeId(localNodeId).localNodeId(localNodeId) + .add(newNode(localNodeId)) + .build(); + + ClusterState currentState = ClusterState.builder(new ClusterName("my-cluster")) + .nodes(currentDiscoveryNodes) + .routingTable(RoutingTable.builder().add(currentWatchRoutingTable).build()) + .metaData(MetaData.builder().put(indexMetaData, false)) + .build(); + + // initialize the previous state, so all the allocation ids are loaded + when(watcherService.validate(anyObject())).thenReturn(true); + lifeCycleService.clusterChanged(new ClusterChangedEvent("whatever", previousState, currentState)); + + reset(watcherService); + when(watcherService.validate(anyObject())).thenReturn(true); + ClusterChangedEvent event = new ClusterChangedEvent("whatever", currentState, previousState); + lifeCycleService.clusterChanged(event); + verify(watcherService).reload(eq(event.state()), anyString()); + } + + private void startWatcher() { Index index = new Index(Watch.INDEX, "uuid"); IndexRoutingTable.Builder indexRoutingTableBuilder = IndexRoutingTable.builder(index); indexRoutingTableBuilder.addShard( @@ -506,12 +578,10 @@ private ClusterState startWatcher() { lifeCycleService.clusterChanged(new ClusterChangedEvent("foo", state, emptyState)); assertThat(lifeCycleService.getState(), is(WatcherState.STARTED)); verify(watcherService, times(1)).reload(eq(state), anyString()); - assertThat(lifeCycleService.allocationIds(), hasSize(1)); + assertThat(lifeCycleService.shardRoutings(), hasSize(1)); // reset the mock, the user has to mock everything themselves again reset(watcherService); - - return state; } private List randomIndexPatterns() { From d93b2a2e9a68ae4defd5949fcc5eb945a26395bf Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 29 Aug 2018 17:10:00 -0400 Subject: [PATCH 224/283] [Rollup] Only allow aggregating on multiples of configured interval (#32052) We need to limit the search request aggregations to whole multiples of the configured interval for both histogram and date_histogram. Otherwise, agg buckets won't overlap with the rolled up buckets and the results will be incorrect. For histogram, the validation is very simple: request must be >= the config, and modulo evenly. Dates are more tricky. - If both request and config are fixed dates, we can convert to millis and treat them just like the histo - If both are calendar, we make sure the request is >= the config with a static lookup map that ranks the calendar values relatively. All calendar units are "singles", so they are evenly divisible already - We disallow any other combination (one fixed, one calendar, etc) --- x-pack/docs/build.gradle | 3 +- .../docs/en/rest-api/rollup/put-job.asciidoc | 2 + .../rollup/rollup-job-config.asciidoc | 50 +++++- .../en/rollup/rollup-getting-started.asciidoc | 124 +++++++------- .../rollup/rollup-search-limitations.asciidoc | 22 ++- .../rollup/RollupJobIdentifierUtils.java | 101 +++++++++-- .../rollup/RollupJobIdentifierUtilTests.java | 160 ++++++++++++++++++ .../elasticsearch/multi_node/RollupIT.java | 2 +- 8 files changed, 380 insertions(+), 84 deletions(-) diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index 6cca05c4a0ef..99e62532e2dc 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -685,9 +685,8 @@ setups['sensor_prefab_data'] = ''' page_size: 1000 groups: date_histogram: - delay: "7d" field: "timestamp" - interval: "1h" + interval: "7d" time_zone: "UTC" terms: fields: diff --git a/x-pack/docs/en/rest-api/rollup/put-job.asciidoc b/x-pack/docs/en/rest-api/rollup/put-job.asciidoc index 1449acadc636..27889d985b8c 100644 --- a/x-pack/docs/en/rest-api/rollup/put-job.asciidoc +++ b/x-pack/docs/en/rest-api/rollup/put-job.asciidoc @@ -43,6 +43,8 @@ started with the <>. `metrics`:: (object) Defines the metrics that should be collected for each grouping tuple. See <>. +For more details about the job configuration, see <>. + ==== Authorization You must have `manage` or `manage_rollup` cluster privileges to use this API. diff --git a/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc b/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc index 2ba92b6b59ea..f937f28601a2 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc +++ b/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc @@ -23,7 +23,7 @@ PUT _xpack/rollup/job/sensor "groups" : { "date_histogram": { "field": "timestamp", - "interval": "1h", + "interval": "60m", "delay": "7d" }, "terms": { @@ -99,7 +99,7 @@ fields will then be available later for aggregating into buckets. For example, "groups" : { "date_histogram": { "field": "timestamp", - "interval": "1h", + "interval": "60m", "delay": "7d" }, "terms": { @@ -133,9 +133,9 @@ The `date_histogram` group has several parameters: The date field that is to be rolled up. `interval` (required):: - The interval of time buckets to be generated when rolling up. E.g. `"1h"` will produce hourly rollups. This follows standard time formatting - syntax as used elsewhere in Elasticsearch. The `interval` defines the _minimum_ interval that can be aggregated only. If hourly (`"1h"`) - intervals are configured, <> can execute aggregations with 1hr or greater (weekly, monthly, etc) intervals. + The interval of time buckets to be generated when rolling up. E.g. `"60m"` will produce 60 minute (hourly) rollups. This follows standard time formatting + syntax as used elsewhere in Elasticsearch. The `interval` defines the _minimum_ interval that can be aggregated only. If hourly (`"60m"`) + intervals are configured, <> can execute aggregations with 60m or greater (weekly, monthly, etc) intervals. So define the interval as the smallest unit that you wish to later query. Note: smaller, more granular intervals take up proportionally more space. @@ -154,6 +154,46 @@ The `date_histogram` group has several parameters: to be stored with a specific timezone. By default, rollup documents are stored in `UTC`, but this can be changed with the `time_zone` parameter. +.Calendar vs Fixed time intervals +********************************** +Elasticsearch understands both "calendar" and "fixed" time intervals. Fixed time intervals are fairly easy to understand; +`"60s"` means sixty seconds. But what does `"1M` mean? One month of time depends on which month we are talking about, +some months are longer or shorter than others. This is an example of "calendar" time, and the duration of that unit +depends on context. Calendar units are also affected by leap-seconds, leap-years, etc. + +This is important because the buckets generated by Rollup will be in either calendar or fixed intervals, and will limit +how you can query them later (see <>. + +We recommend sticking with "fixed" time intervals, since they are easier to understand and are more flexible at query +time. It will introduce some drift in your data during leap-events, and you will have to think about months in a fixed +quantity (30 days) instead of the actual calendar length... but it is often easier than dealing with calendar units +at query time. + +Multiples of units are always "fixed" (e.g. `"2h"` is always the fixed quantity `7200` seconds. Single units can be +fixed or calendar depending on the unit: + +[options="header"] +|======= +|Unit |Calendar |Fixed +|millisecond |NA |`1ms`, `10ms`, etc +|second |NA |`1s`, `10s`, etc +|minute |`1m` |`2m`, `10m`, etc +|hour |`1h` |`2h`, `10h`, etc +|day |`1d` |`2d`, `10d`, etc +|week |`1w` |NA +|month |`1M` |NA +|quarter |`1q` |NA +|year |`1y` |NA +|======= + +For some units where there are both fixed and calendar, you may need to express the quantity in terms of the next +smaller unit. For example, if you want a fixed day (not a calendar day), you should specify `24h` instead of `1d`. +Similarly, if you want fixed hours, specify `60m` instead of `1h`. This is because the single quantity entails +calendar time, and limits you to querying by calendar time in the future. + + +********************************** + ===== Terms The `terms` group can be used on `keyword` or numeric fields, to allow bucketing via the `terms` aggregation at a later point. The `terms` diff --git a/x-pack/docs/en/rollup/rollup-getting-started.asciidoc b/x-pack/docs/en/rollup/rollup-getting-started.asciidoc index 24f68dddd810..b6c913d7d34a 100644 --- a/x-pack/docs/en/rollup/rollup-getting-started.asciidoc +++ b/x-pack/docs/en/rollup/rollup-getting-started.asciidoc @@ -37,8 +37,7 @@ PUT _xpack/rollup/job/sensor "groups" : { "date_histogram": { "field": "timestamp", - "interval": "1h", - "delay": "7d" + "interval": "60m" }, "terms": { "fields": ["node"] @@ -66,7 +65,7 @@ The `cron` parameter controls when and how often the job activates. When a roll from where it left off after the last activation. So if you configure the cron to run every 30 seconds, the job will process the last 30 seconds worth of data that was indexed into the `sensor-*` indices. -If instead the cron was configured to run once a day at midnight, the job would process the last 24hours worth of data. The choice is largely +If instead the cron was configured to run once a day at midnight, the job would process the last 24 hours worth of data. The choice is largely preference, based on how "realtime" you want the rollups, and if you wish to process continuously or move it to off-peak hours. Next, we define a set of `groups` and `metrics`. The metrics are fairly straightforward: we want to save the min/max/sum of the `temperature` @@ -79,7 +78,7 @@ It also allows us to run terms aggregations on the `node` field. .Date histogram interval vs cron schedule ********************************** You'll note that the job's cron is configured to run every 30 seconds, but the date_histogram is configured to -rollup at hourly intervals. How do these relate? +rollup at 60 minute intervals. How do these relate? The date_histogram controls the granularity of the saved data. Data will be rolled up into hourly intervals, and you will be unable to query with finer granularity. The cron simply controls when the process looks for new data to rollup. Every 30 seconds it will see @@ -223,70 +222,71 @@ Which returns a corresponding response: [source,js] ---- { - "took" : 93, - "timed_out" : false, - "terminated_early" : false, - "_shards" : ... , - "hits" : { - "total" : 0, - "max_score" : 0.0, - "hits" : [ ] - }, - "aggregations" : { - "timeline" : { - "meta" : { }, - "buckets" : [ - { - "key_as_string" : "2018-01-18T00:00:00.000Z", - "key" : 1516233600000, - "doc_count" : 6, - "nodes" : { - "doc_count_error_upper_bound" : 0, - "sum_other_doc_count" : 0, - "buckets" : [ - { - "key" : "a", - "doc_count" : 2, - "max_temperature" : { - "value" : 202.0 - }, - "avg_voltage" : { - "value" : 5.1499998569488525 - } - }, - { - "key" : "b", - "doc_count" : 2, - "max_temperature" : { - "value" : 201.0 - }, - "avg_voltage" : { - "value" : 5.700000047683716 - } - }, - { - "key" : "c", - "doc_count" : 2, - "max_temperature" : { - "value" : 202.0 - }, - "avg_voltage" : { - "value" : 4.099999904632568 - } - } - ] - } - } - ] - } - } + "took" : 93, + "timed_out" : false, + "terminated_early" : false, + "_shards" : ... , + "hits" : { + "total" : 0, + "max_score" : 0.0, + "hits" : [ ] + }, + "aggregations" : { + "timeline" : { + "meta" : { }, + "buckets" : [ + { + "key_as_string" : "2018-01-18T00:00:00.000Z", + "key" : 1516233600000, + "doc_count" : 6, + "nodes" : { + "doc_count_error_upper_bound" : 0, + "sum_other_doc_count" : 0, + "buckets" : [ + { + "key" : "a", + "doc_count" : 2, + "max_temperature" : { + "value" : 202.0 + }, + "avg_voltage" : { + "value" : 5.1499998569488525 + } + }, + { + "key" : "b", + "doc_count" : 2, + "max_temperature" : { + "value" : 201.0 + }, + "avg_voltage" : { + "value" : 5.700000047683716 + } + }, + { + "key" : "c", + "doc_count" : 2, + "max_temperature" : { + "value" : 202.0 + }, + "avg_voltage" : { + "value" : 4.099999904632568 + } + } + ] + } + } + ] + } + } } + ---- // TESTRESPONSE[s/"took" : 93/"took" : $body.$_path/] // TESTRESPONSE[s/"_shards" : \.\.\. /"_shards" : $body.$_path/] In addition to being more complicated (date histogram and a terms aggregation, plus an additional average metric), you'll notice -the date_histogram uses a `7d` interval instead of `1h`. +the date_histogram uses a `7d` interval instead of `60m`. [float] === Conclusion diff --git a/x-pack/docs/en/rollup/rollup-search-limitations.asciidoc b/x-pack/docs/en/rollup/rollup-search-limitations.asciidoc index 57ba23eebccb..99f19a179ede 100644 --- a/x-pack/docs/en/rollup/rollup-search-limitations.asciidoc +++ b/x-pack/docs/en/rollup/rollup-search-limitations.asciidoc @@ -80,9 +80,25 @@ The response will tell you that the field and aggregation were not possible, bec [float] === Interval Granularity -Rollups are stored at a certain granularity, as defined by the `date_histogram` group in the configuration. If data is rolled up at hourly -intervals, the <> API can aggregate on any time interval hourly or greater. Intervals that are less than an hour will throw -an exception, since the data simply doesn't exist for finer granularities. +Rollups are stored at a certain granularity, as defined by the `date_histogram` group in the configuration. This means you +can only search/aggregate the rollup data with an interval that is greater-than or equal to the configured rollup interval. + +For example, if data is rolled up at hourly intervals, the <> API can aggregate on any time interval +hourly or greater. Intervals that are less than an hour will throw an exception, since the data simply doesn't +exist for finer granularities. + +[[rollup-search-limitations-intervals]] +.Requests must be multiples of the config +********************************** +Perhaps not immediately apparent, but the interval specified in an aggregation request must be a whole +multiple of the configured interval. If the job was configured to rollup on `3d` intervals, you can only +query and aggregate on multiples of three (`3d`, `6d`, `9d`, etc). + +A non-multiple wouldn't work, since the rolled up data wouldn't cleanly "overlap" with the buckets generated +by the aggregation, leading to incorrect results. + +For that reason, an error is thrown if a whole multiple of the configured interval isn't found. +********************************** Because the RollupSearch endpoint can "upsample" intervals, there is no need to configure jobs with multiple intervals (hourly, daily, etc). It's recommended to just configure a single job with the smallest granularity that is needed, and allow the search endpoint to upsample diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupJobIdentifierUtils.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupJobIdentifierUtils.java index d1706fd708e9..8537f2b6a38b 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupJobIdentifierUtils.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/RollupJobIdentifierUtils.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; @@ -17,7 +18,9 @@ import org.joda.time.DateTimeZone; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -32,6 +35,29 @@ public class RollupJobIdentifierUtils { private static final Comparator COMPARATOR = RollupJobIdentifierUtils.getComparator(); + public static final Map CALENDAR_ORDERING; + + static { + Map dateFieldUnits = new HashMap<>(16); + dateFieldUnits.put("year", 8); + dateFieldUnits.put("1y", 8); + dateFieldUnits.put("quarter", 7); + dateFieldUnits.put("1q", 7); + dateFieldUnits.put("month", 6); + dateFieldUnits.put("1M", 6); + dateFieldUnits.put("week", 5); + dateFieldUnits.put("1w", 5); + dateFieldUnits.put("day", 4); + dateFieldUnits.put("1d", 4); + dateFieldUnits.put("hour", 3); + dateFieldUnits.put("1h", 3); + dateFieldUnits.put("minute", 2); + dateFieldUnits.put("1m", 2); + dateFieldUnits.put("second", 1); + dateFieldUnits.put("1s", 1); + CALENDAR_ORDERING = Collections.unmodifiableMap(dateFieldUnits); + } + /** * Given the aggregation tree and a list of available job capabilities, this method will return a set * of the "best" jobs that should be searched. @@ -93,8 +119,9 @@ private static void checkDateHisto(DateHistogramAggregationBuilder source, List< if (fieldCaps != null) { for (Map agg : fieldCaps.getAggs()) { if (agg.get(RollupField.AGG).equals(DateHistogramAggregationBuilder.NAME)) { - TimeValue interval = TimeValue.parseTimeValue((String)agg.get(RollupField.INTERVAL), "date_histogram.interval"); - String thisTimezone = (String) agg.get(DateHistogramGroupConfig.TIME_ZONE); + DateHistogramInterval interval = new DateHistogramInterval((String)agg.get(RollupField.INTERVAL)); + + String thisTimezone = (String)agg.get(DateHistogramGroupConfig.TIME_ZONE); String sourceTimeZone = source.timeZone() == null ? DateTimeZone.UTC.toString() : source.timeZone().toString(); // Ensure we are working on the same timezone @@ -102,17 +129,20 @@ private static void checkDateHisto(DateHistogramAggregationBuilder source, List< continue; } if (source.dateHistogramInterval() != null) { - TimeValue sourceInterval = TimeValue.parseTimeValue(source.dateHistogramInterval().toString(), - "source.date_histogram.interval"); - //TODO should be divisor of interval - if (interval.compareTo(sourceInterval) <= 0) { + // Check if both are calendar and validate if they are. + // If not, check if both are fixed and validate + if (validateCalendarInterval(source.dateHistogramInterval(), interval)) { + localCaps.add(cap); + } else if (validateFixedInterval(source.dateHistogramInterval(), interval)) { localCaps.add(cap); } } else { - if (interval.getMillis() <= source.interval()) { + // check if config is fixed and validate if it is + if (validateFixedInterval(source.interval(), interval)) { localCaps.add(cap); } } + // not a candidate if we get here break; } } @@ -133,6 +163,55 @@ private static void checkDateHisto(DateHistogramAggregationBuilder source, List< } } + private static boolean isCalendarInterval(DateHistogramInterval interval) { + return DateHistogramAggregationBuilder.DATE_FIELD_UNITS.containsKey(interval.toString()); + } + + static boolean validateCalendarInterval(DateHistogramInterval requestInterval, + DateHistogramInterval configInterval) { + // Both must be calendar intervals + if (isCalendarInterval(requestInterval) == false || isCalendarInterval(configInterval) == false) { + return false; + } + + // The request must be gte the config. The CALENDAR_ORDERING map values are integers representing + // relative orders between the calendar units + int requestOrder = CALENDAR_ORDERING.getOrDefault(requestInterval.toString(), Integer.MAX_VALUE); + int configOrder = CALENDAR_ORDERING.getOrDefault(configInterval.toString(), Integer.MAX_VALUE); + + // All calendar units are multiples naturally, so we just care about gte + return requestOrder >= configOrder; + } + + static boolean validateFixedInterval(DateHistogramInterval requestInterval, + DateHistogramInterval configInterval) { + // Neither can be calendar intervals + if (isCalendarInterval(requestInterval) || isCalendarInterval(configInterval)) { + return false; + } + + // Both are fixed, good to conver to millis now + long configIntervalMillis = TimeValue.parseTimeValue(configInterval.toString(), + "date_histo.config.interval").getMillis(); + long requestIntervalMillis = TimeValue.parseTimeValue(requestInterval.toString(), + "date_histo.request.interval").getMillis(); + + // Must be a multiple and gte the config + return requestIntervalMillis >= configIntervalMillis && requestIntervalMillis % configIntervalMillis == 0; + } + + static boolean validateFixedInterval(long requestInterval, DateHistogramInterval configInterval) { + // config must not be a calendar interval + if (isCalendarInterval(configInterval)) { + return false; + } + long configIntervalMillis = TimeValue.parseTimeValue(configInterval.toString(), + "date_histo.config.interval").getMillis(); + + // Must be a multiple and gte the config + return requestInterval >= configIntervalMillis && requestInterval % configIntervalMillis == 0; + } + /** * Find the set of histo's with the largest interval */ @@ -144,8 +223,8 @@ private static void checkHisto(HistogramAggregationBuilder source, List agg : fieldCaps.getAggs()) { if (agg.get(RollupField.AGG).equals(HistogramAggregationBuilder.NAME)) { Long interval = (long)agg.get(RollupField.INTERVAL); - // TODO should be divisor of interval - if (interval <= source.interval()) { + // query interval must be gte the configured interval, and a whole multiple + if (interval <= source.interval() && source.interval() % interval == 0) { localCaps.add(cap); } break; @@ -155,8 +234,8 @@ private static void checkHisto(HistogramAggregationBuilder source, List caps = singletonSet(cap); + + DateHistogramAggregationBuilder builder = new DateHistogramAggregationBuilder("foo").field("foo") + .dateHistogramInterval(new DateHistogramInterval("1000s")); + + Set bestCaps = RollupJobIdentifierUtils.findBestJobs(builder, caps); + assertThat(bestCaps.size(), equalTo(1)); + } + + public void testBiggerButCompatibleFixedMillisInterval() { + final GroupConfig group = new GroupConfig(new DateHistogramGroupConfig("foo", new DateHistogramInterval("100ms"))); + final RollupJobConfig job = new RollupJobConfig("foo", "index", "rollup", "*/5 * * * * ?", 10, group, emptyList(), null); + RollupJobCaps cap = new RollupJobCaps(job); + Set caps = singletonSet(cap); + + DateHistogramAggregationBuilder builder = new DateHistogramAggregationBuilder("foo").field("foo") + .interval(1000); + + Set bestCaps = RollupJobIdentifierUtils.findBestJobs(builder, caps); + assertThat(bestCaps.size(), equalTo(1)); + } + public void testIncompatibleInterval() { final GroupConfig group = new GroupConfig(new DateHistogramGroupConfig("foo", new DateHistogramInterval("1d"))); final RollupJobConfig job = new RollupJobConfig("foo", "index", "rollup", "*/5 * * * * ?", 10, group, emptyList(), null); @@ -75,6 +101,20 @@ public void testIncompatibleInterval() { "[foo] which also satisfies all requirements of query.")); } + public void testIncompatibleFixedCalendarInterval() { + final GroupConfig group = new GroupConfig(new DateHistogramGroupConfig("foo", new DateHistogramInterval("5d"))); + final RollupJobConfig job = new RollupJobConfig("foo", "index", "rollup", "*/5 * * * * ?", 10, group, emptyList(), null); + RollupJobCaps cap = new RollupJobCaps(job); + Set caps = singletonSet(cap); + + DateHistogramAggregationBuilder builder = new DateHistogramAggregationBuilder("foo").field("foo") + .dateHistogramInterval(new DateHistogramInterval("day")); + + RuntimeException e = expectThrows(RuntimeException.class, () -> RollupJobIdentifierUtils.findBestJobs(builder, caps)); + assertThat(e.getMessage(), equalTo("There is not a rollup job that has a [date_histogram] agg on field " + + "[foo] which also satisfies all requirements of query.")); + } + public void testBadTimeZone() { final GroupConfig group = new GroupConfig(new DateHistogramGroupConfig("foo", new DateHistogramInterval("1h"), null, "EST")); final RollupJobConfig job = new RollupJobConfig("foo", "index", "rollup", "*/5 * * * * ?", 10, group, emptyList(), null); @@ -385,6 +425,27 @@ public void testNoMatchingHistoInterval() { "[bar] which also satisfies all requirements of query.")); } + public void testHistoIntervalNotMultiple() { + HistogramAggregationBuilder histo = new HistogramAggregationBuilder("test_histo"); + histo.interval(10) // <--- interval is not a multiple of 3 + .field("bar") + .subAggregation(new MaxAggregationBuilder("the_max").field("max_field")) + .subAggregation(new AvgAggregationBuilder("the_avg").field("avg_field")); + + final GroupConfig group = new GroupConfig(new DateHistogramGroupConfig("foo", + new DateHistogramInterval("1d"), null, "UTC"), + new HistogramGroupConfig(3L, "bar"), + null); + final RollupJobConfig job = new RollupJobConfig("foo", "index", "rollup", "*/5 * * * * ?", 10, group, emptyList(), null); + RollupJobCaps cap = new RollupJobCaps(job); + Set caps = singletonSet(cap); + + Exception e = expectThrows(RuntimeException.class, + () -> RollupJobIdentifierUtils.findBestJobs(histo, caps)); + assertThat(e.getMessage(), equalTo("There is not a rollup job that has a [histogram] agg on field " + + "[bar] which also satisfies all requirements of query.")); + } + public void testMissingMetric() { int i = ESTestCase.randomIntBetween(0, 3); @@ -417,6 +478,105 @@ public void testMissingMetric() { } + public void testValidateFixedInterval() { + boolean valid = RollupJobIdentifierUtils.validateFixedInterval(100, new DateHistogramInterval("100ms")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(200, new DateHistogramInterval("100ms")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(1000, new DateHistogramInterval("200ms")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(5*60*1000, new DateHistogramInterval("5m")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(10*5*60*1000, new DateHistogramInterval("5m")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(100, new DateHistogramInterval("500ms")); + assertFalse(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(100, new DateHistogramInterval("5m")); + assertFalse(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(100, new DateHistogramInterval("minute")); + assertFalse(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(100, new DateHistogramInterval("second")); + assertFalse(valid); + + // ----------- + // Same tests, with both being DateHistoIntervals + // ----------- + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("100ms"), + new DateHistogramInterval("100ms")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("200ms"), + new DateHistogramInterval("100ms")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("1000ms"), + new DateHistogramInterval("200ms")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("5m"), + new DateHistogramInterval("5m")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("20m"), + new DateHistogramInterval("5m")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("100ms"), + new DateHistogramInterval("500ms")); + assertFalse(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("100ms"), + new DateHistogramInterval("5m")); + assertFalse(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("100ms"), + new DateHistogramInterval("minute")); + assertFalse(valid); + + valid = RollupJobIdentifierUtils.validateFixedInterval(new DateHistogramInterval("100ms"), + new DateHistogramInterval("second")); + assertFalse(valid); + } + + public void testValidateCalendarInterval() { + boolean valid = RollupJobIdentifierUtils.validateCalendarInterval(new DateHistogramInterval("second"), + new DateHistogramInterval("second")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateCalendarInterval(new DateHistogramInterval("minute"), + new DateHistogramInterval("second")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateCalendarInterval(new DateHistogramInterval("month"), + new DateHistogramInterval("day")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateCalendarInterval(new DateHistogramInterval("1d"), + new DateHistogramInterval("1s")); + assertTrue(valid); + + valid = RollupJobIdentifierUtils.validateCalendarInterval(new DateHistogramInterval("second"), + new DateHistogramInterval("minute")); + assertFalse(valid); + + valid = RollupJobIdentifierUtils.validateCalendarInterval(new DateHistogramInterval("second"), + new DateHistogramInterval("1m")); + assertFalse(valid); + + // Fails because both are actually fixed + valid = RollupJobIdentifierUtils.validateCalendarInterval(new DateHistogramInterval("100ms"), + new DateHistogramInterval("100ms")); + assertFalse(valid); + } + private Set singletonSet(RollupJobCaps cap) { Set caps = new HashSet<>(); caps.add(cap); diff --git a/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/RollupIT.java b/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/RollupIT.java index 43ad4dc0a45a..fb9c665b2bf1 100644 --- a/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/RollupIT.java +++ b/x-pack/qa/multi-node/src/test/java/org/elasticsearch/multi_node/RollupIT.java @@ -173,7 +173,7 @@ public void testBigRollup() throws Exception { " \"date_histo\": {\n" + " \"date_histogram\": {\n" + " \"field\": \"timestamp\",\n" + - " \"interval\": \"1h\",\n" + + " \"interval\": \"60m\",\n" + " \"format\": \"date_time\"\n" + " },\n" + " \"aggs\": {\n" + From cc4d7059bfda42042e4e320c7c2bb228c0fe4691 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 30 Aug 2018 03:46:39 +0200 Subject: [PATCH 225/283] Ingest: Add conditional per processor (#32398) * Ingest: Add conditional per processor * closes #21248 --- .../ingest/common/ForEachProcessor.java | 11 +- .../ingest/common/IngestCommonPlugin.java | 2 +- .../common/ForEachProcessorFactoryTests.java | 16 +- .../test/ingest/210_conditional_processor.yml | 81 ++++ .../ingest/SimulatePipelineRequest.java | 6 +- .../ingest/ConditionalProcessor.java | 381 ++++++++++++++++++ .../ingest/ConfigurationUtils.java | 55 ++- .../elasticsearch/ingest/IngestService.java | 16 +- .../org/elasticsearch/ingest/Pipeline.java | 8 +- .../script/IngestConditionalScript.java | 51 +++ .../elasticsearch/script/ScriptModule.java | 1 + .../ingest/ConditionalProcessorTests.java | 141 +++++++ .../ingest/ConfigurationUtilsTests.java | 19 +- .../ingest/PipelineFactoryTests.java | 30 +- .../script/MockScriptEngine.java | 8 + 15 files changed, 788 insertions(+), 38 deletions(-) create mode 100644 modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_conditional_processor.yml create mode 100644 server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java create mode 100644 server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java create mode 100644 server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/ForEachProcessor.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/ForEachProcessor.java index f5bf9cc95910..31c0ae8cc3dc 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/ForEachProcessor.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/ForEachProcessor.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.elasticsearch.script.ScriptService; import static org.elasticsearch.ingest.ConfigurationUtils.newConfigurationException; import static org.elasticsearch.ingest.ConfigurationUtils.readBooleanProperty; @@ -96,6 +97,13 @@ Processor getProcessor() { } public static final class Factory implements Processor.Factory { + + private final ScriptService scriptService; + + Factory(ScriptService scriptService) { + this.scriptService = scriptService; + } + @Override public ForEachProcessor create(Map factories, String tag, Map config) throws Exception { @@ -107,7 +115,8 @@ public ForEachProcessor create(Map factories, String throw newConfigurationException(TYPE, tag, "processor", "Must specify exactly one processor type"); } Map.Entry> entry = entries.iterator().next(); - Processor processor = ConfigurationUtils.readProcessor(factories, entry.getKey(), entry.getValue()); + Processor processor = + ConfigurationUtils.readProcessor(factories, scriptService, entry.getKey(), entry.getValue()); return new ForEachProcessor(tag, field, processor, ignoreMissing); } } diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java index 1ed8b6058e6c..8b048282814e 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/IngestCommonPlugin.java @@ -72,7 +72,7 @@ public Map getProcessors(Processor.Parameters paramet processors.put(ConvertProcessor.TYPE, new ConvertProcessor.Factory()); processors.put(GsubProcessor.TYPE, new GsubProcessor.Factory()); processors.put(FailProcessor.TYPE, new FailProcessor.Factory(parameters.scriptService)); - processors.put(ForEachProcessor.TYPE, new ForEachProcessor.Factory()); + processors.put(ForEachProcessor.TYPE, new ForEachProcessor.Factory(parameters.scriptService)); processors.put(DateIndexNameProcessor.TYPE, new DateIndexNameProcessor.Factory(parameters.scriptService)); processors.put(SortProcessor.TYPE, new SortProcessor.Factory()); processors.put(GrokProcessor.TYPE, new GrokProcessor.Factory(GROK_PATTERNS, createGrokThreadWatchdog(parameters))); diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java index f382ad8dcfb6..7ab19c4147ea 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/ForEachProcessorFactoryTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ingest.Processor; import org.elasticsearch.ingest.TestProcessor; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; @@ -30,14 +31,17 @@ import java.util.Map; import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; public class ForEachProcessorFactoryTests extends ESTestCase { + private final ScriptService scriptService = mock(ScriptService.class); + public void testCreate() throws Exception { Processor processor = new TestProcessor(ingestDocument -> { }); Map registry = new HashMap<>(); registry.put("_name", (r, t, c) -> processor); - ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(); + ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("field", "_field"); @@ -53,7 +57,7 @@ public void testSetIgnoreMissing() throws Exception { Processor processor = new TestProcessor(ingestDocument -> { }); Map registry = new HashMap<>(); registry.put("_name", (r, t, c) -> processor); - ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(); + ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("field", "_field"); @@ -71,7 +75,7 @@ public void testCreateWithTooManyProcessorTypes() throws Exception { Map registry = new HashMap<>(); registry.put("_first", (r, t, c) -> processor); registry.put("_second", (r, t, c) -> processor); - ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(); + ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("field", "_field"); @@ -84,7 +88,7 @@ public void testCreateWithTooManyProcessorTypes() throws Exception { } public void testCreateWithNonExistingProcessorType() throws Exception { - ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(); + ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("field", "_field"); config.put("processor", Collections.singletonMap("_name", Collections.emptyMap())); @@ -97,7 +101,7 @@ public void testCreateWithMissingField() throws Exception { Processor processor = new TestProcessor(ingestDocument -> { }); Map registry = new HashMap<>(); registry.put("_name", (r, t, c) -> processor); - ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(); + ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("processor", Collections.singletonList(Collections.singletonMap("_name", Collections.emptyMap()))); Exception exception = expectThrows(Exception.class, () -> forEachFactory.create(registry, null, config)); @@ -105,7 +109,7 @@ public void testCreateWithMissingField() throws Exception { } public void testCreateWithMissingProcessor() { - ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(); + ForEachProcessor.Factory forEachFactory = new ForEachProcessor.Factory(scriptService); Map config = new HashMap<>(); config.put("field", "_field"); Exception exception = expectThrows(Exception.class, () -> forEachFactory.create(Collections.emptyMap(), null, config)); diff --git a/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_conditional_processor.yml b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_conditional_processor.yml new file mode 100644 index 000000000000..532519c4ca07 --- /dev/null +++ b/modules/ingest-common/src/test/resources/rest-api-spec/test/ingest/210_conditional_processor.yml @@ -0,0 +1,81 @@ +--- +teardown: + - do: + ingest.delete_pipeline: + id: "my_pipeline" + ignore: 404 + +--- +"Test conditional processor fulfilled condition": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "bytes" : { + "if" : "ctx.conditional_field == 'bar'", + "field" : "bytes_source_field", + "target_field" : "bytes_target_field" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + type: test + id: 1 + pipeline: "my_pipeline" + body: {bytes_source_field: "1kb", conditional_field: "bar"} + + - do: + get: + index: test + type: test + id: 1 + - match: { _source.bytes_source_field: "1kb" } + - match: { _source.conditional_field: "bar" } + - match: { _source.bytes_target_field: 1024 } + +--- +"Test conditional processor unfulfilled condition": + - do: + ingest.put_pipeline: + id: "my_pipeline" + body: > + { + "description": "_description", + "processors": [ + { + "bytes" : { + "if" : "ctx.conditional_field == 'foo'", + "field" : "bytes_source_field", + "target_field" : "bytes_target_field" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + type: test + id: 1 + pipeline: "my_pipeline" + body: {bytes_source_field: "1kb", conditional_field: "bar"} + + - do: + get: + index: test + type: test + id: 1 + - match: { _source.bytes_source_field: "1kb" } + - match: { _source.conditional_field: "bar" } + - is_false: _source.bytes_target_field + diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java index fecee5f265fe..7514a41f5756 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java @@ -171,9 +171,11 @@ static Parsed parseWithPipelineId(String pipelineId, Map config, return new Parsed(pipeline, ingestDocumentList, verbose); } - static Parsed parse(Map config, boolean verbose, IngestService pipelineStore) throws Exception { + static Parsed parse(Map config, boolean verbose, IngestService ingestService) throws Exception { Map pipelineConfig = ConfigurationUtils.readMap(null, null, config, Fields.PIPELINE); - Pipeline pipeline = Pipeline.create(SIMULATED_PIPELINE_ID, pipelineConfig, pipelineStore.getProcessorFactories()); + Pipeline pipeline = Pipeline.create( + SIMULATED_PIPELINE_ID, pipelineConfig, ingestService.getProcessorFactories(), ingestService.getScriptService() + ); List ingestDocumentList = parseDocs(config); return new Parsed(pipeline, ingestDocumentList, verbose); } diff --git a/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java new file mode 100644 index 000000000000..d1eb651acae0 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java @@ -0,0 +1,381 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.ingest; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.elasticsearch.script.IngestConditionalScript; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptService; + +public class ConditionalProcessor extends AbstractProcessor { + + static final String TYPE = "conditional"; + + private final Script condition; + + private final ScriptService scriptService; + + private final Processor processor; + + ConditionalProcessor(String tag, Script script, ScriptService scriptService, Processor processor) { + super(tag); + this.condition = script; + this.scriptService = scriptService; + this.processor = processor; + } + + @Override + public void execute(IngestDocument ingestDocument) throws Exception { + IngestConditionalScript script = + scriptService.compile(condition, IngestConditionalScript.CONTEXT).newInstance(condition.getParams()); + if (script.execute(new UnmodifiableIngestData(ingestDocument.getSourceAndMetadata()))) { + processor.execute(ingestDocument); + } + } + + @Override + public String getType() { + return TYPE; + } + + private static Object wrapUnmodifiable(Object raw) { + // Wraps all mutable types that the JSON parser can create by immutable wrappers. + // Any inputs not wrapped are assumed to be immutable + if (raw instanceof Map) { + return new UnmodifiableIngestData((Map) raw); + } else if (raw instanceof List) { + return new UnmodifiableIngestList((List) raw); + } else if (raw instanceof byte[]) { + return ((byte[]) raw).clone(); + } + return raw; + } + + private static UnsupportedOperationException unmodifiableException() { + return new UnsupportedOperationException("Mutating ingest documents in conditionals is not supported"); + } + + private static final class UnmodifiableIngestData implements Map { + + private final Map data; + + UnmodifiableIngestData(Map data) { + this.data = data; + } + + @Override + public int size() { + return data.size(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + return data.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + return data.containsValue(value); + } + + @Override + public Object get(final Object key) { + return wrapUnmodifiable(data.get(key)); + } + + @Override + public Object put(final String key, final Object value) { + throw unmodifiableException(); + } + + @Override + public Object remove(final Object key) { + throw unmodifiableException(); + } + + @Override + public void putAll(final Map m) { + throw unmodifiableException(); + } + + @Override + public void clear() { + throw unmodifiableException(); + } + + @Override + public Set keySet() { + return Collections.unmodifiableSet(data.keySet()); + } + + @Override + public Collection values() { + return new UnmodifiableIngestList(new ArrayList<>(data.values())); + } + + @Override + public Set> entrySet() { + return data.entrySet().stream().map(entry -> + new Entry() { + @Override + public String getKey() { + return entry.getKey(); + } + + @Override + public Object getValue() { + return wrapUnmodifiable(entry.getValue()); + } + + @Override + public Object setValue(final Object value) { + throw unmodifiableException(); + } + + @Override + public boolean equals(final Object o) { + return entry.equals(o); + } + + @Override + public int hashCode() { + return entry.hashCode(); + } + }).collect(Collectors.toSet()); + } + } + + private static final class UnmodifiableIngestList implements List { + + private final List data; + + UnmodifiableIngestList(List data) { + this.data = data; + } + + @Override + public int size() { + return data.size(); + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean contains(final Object o) { + return data.contains(o); + } + + @Override + public Iterator iterator() { + Iterator wrapped = data.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return wrapped.hasNext(); + } + + @Override + public Object next() { + return wrapped.next(); + } + + @Override + public void remove() { + throw unmodifiableException(); + } + }; + } + + @Override + public Object[] toArray() { + Object[] wrapped = data.toArray(new Object[0]); + for (int i = 0; i < wrapped.length; i++) { + wrapped[i] = wrapUnmodifiable(wrapped[i]); + } + return wrapped; + } + + @Override + public T[] toArray(final T[] a) { + Object[] raw = data.toArray(new Object[0]); + T[] wrapped = (T[]) Arrays.copyOf(raw, a.length, a.getClass()); + for (int i = 0; i < wrapped.length; i++) { + wrapped[i] = (T) wrapUnmodifiable(wrapped[i]); + } + return wrapped; + } + + @Override + public boolean add(final Object o) { + throw unmodifiableException(); + } + + @Override + public boolean remove(final Object o) { + throw unmodifiableException(); + } + + @Override + public boolean containsAll(final Collection c) { + return data.contains(c); + } + + @Override + public boolean addAll(final Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean addAll(final int index, final Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean removeAll(final Collection c) { + throw unmodifiableException(); + } + + @Override + public boolean retainAll(final Collection c) { + throw unmodifiableException(); + } + + @Override + public void clear() { + throw unmodifiableException(); + } + + @Override + public Object get(final int index) { + return wrapUnmodifiable(data.get(index)); + } + + @Override + public Object set(final int index, final Object element) { + throw unmodifiableException(); + } + + @Override + public void add(final int index, final Object element) { + throw unmodifiableException(); + } + + @Override + public Object remove(final int index) { + throw unmodifiableException(); + } + + @Override + public int indexOf(final Object o) { + return data.indexOf(o); + } + + @Override + public int lastIndexOf(final Object o) { + return data.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return new UnmodifiableListIterator(data.listIterator()); + } + + @Override + public ListIterator listIterator(final int index) { + return new UnmodifiableListIterator(data.listIterator(index)); + } + + @Override + public List subList(final int fromIndex, final int toIndex) { + return new UnmodifiableIngestList(data.subList(fromIndex, toIndex)); + } + + private static final class UnmodifiableListIterator implements ListIterator { + + private final ListIterator data; + + UnmodifiableListIterator(ListIterator data) { + this.data = data; + } + + @Override + public boolean hasNext() { + return data.hasNext(); + } + + @Override + public Object next() { + return wrapUnmodifiable(data.next()); + } + + @Override + public boolean hasPrevious() { + return data.hasPrevious(); + } + + @Override + public Object previous() { + return wrapUnmodifiable(data.previous()); + } + + @Override + public int nextIndex() { + return data.nextIndex(); + } + + @Override + public int previousIndex() { + return data.previousIndex(); + } + + @Override + public void remove() { + throw unmodifiableException(); + } + + @Override + public void set(final Object o) { + throw unmodifiableException(); + } + + @Override + public void add(final Object o) { + throw unmodifiableException(); + } + } + } +} diff --git a/server/src/main/java/org/elasticsearch/ingest/ConfigurationUtils.java b/server/src/main/java/org/elasticsearch/ingest/ConfigurationUtils.java index 54d06d116552..d4f27f47eb8f 100644 --- a/server/src/main/java/org/elasticsearch/ingest/ConfigurationUtils.java +++ b/server/src/main/java/org/elasticsearch/ingest/ConfigurationUtils.java @@ -19,9 +19,18 @@ package org.elasticsearch.ingest; +import java.io.IOException; +import java.io.InputStream; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; @@ -296,6 +305,7 @@ public static ElasticsearchException newConfigurationException(String processorT } public static List readProcessorConfigs(List> processorConfigs, + ScriptService scriptService, Map processorFactories) throws Exception { Exception exception = null; List processors = new ArrayList<>(); @@ -303,7 +313,7 @@ public static List readProcessorConfigs(List> pro for (Map processorConfigWithKey : processorConfigs) { for (Map.Entry entry : processorConfigWithKey.entrySet()) { try { - processors.add(readProcessor(processorFactories, entry.getKey(), entry.getValue())); + processors.add(readProcessor(processorFactories, scriptService, entry.getKey(), entry.getValue())); } catch (Exception e) { exception = ExceptionsHelper.useOrSuppress(exception, e); } @@ -356,13 +366,14 @@ private static void addMetadataToException(ElasticsearchException exception, Str @SuppressWarnings("unchecked") public static Processor readProcessor(Map processorFactories, + ScriptService scriptService, String type, Object config) throws Exception { if (config instanceof Map) { - return readProcessor(processorFactories, type, (Map) config); + return readProcessor(processorFactories, scriptService, type, (Map) config); } else if (config instanceof String && "script".equals(type)) { Map normalizedScript = new HashMap<>(1); normalizedScript.put(ScriptType.INLINE.getParseField().getPreferredName(), config); - return readProcessor(processorFactories, type, normalizedScript); + return readProcessor(processorFactories, scriptService, type, normalizedScript); } else { throw newConfigurationException(type, null, null, "property isn't a map, but of type [" + config.getClass().getName() + "]"); @@ -370,15 +381,17 @@ public static Processor readProcessor(Map processorFa } public static Processor readProcessor(Map processorFactories, + ScriptService scriptService, String type, Map config) throws Exception { String tag = ConfigurationUtils.readOptionalStringProperty(null, null, config, TAG_KEY); + Script conditionalScript = extractConditional(config); Processor.Factory factory = processorFactories.get(type); if (factory != null) { boolean ignoreFailure = ConfigurationUtils.readBooleanProperty(null, null, config, "ignore_failure", false); List> onFailureProcessorConfigs = ConfigurationUtils.readOptionalList(null, null, config, Pipeline.ON_FAILURE_KEY); - List onFailureProcessors = readProcessorConfigs(onFailureProcessorConfigs, processorFactories); + List onFailureProcessors = readProcessorConfigs(onFailureProcessorConfigs, scriptService, processorFactories); if (onFailureProcessorConfigs != null && onFailureProcessors.isEmpty()) { throw newConfigurationException(type, tag, Pipeline.ON_FAILURE_KEY, @@ -392,14 +405,42 @@ public static Processor readProcessor(Map processorFa type, Arrays.toString(config.keySet().toArray())); } if (onFailureProcessors.size() > 0 || ignoreFailure) { - return new CompoundProcessor(ignoreFailure, Collections.singletonList(processor), onFailureProcessors); - } else { - return processor; + processor = new CompoundProcessor(ignoreFailure, Collections.singletonList(processor), onFailureProcessors); } + if (conditionalScript != null) { + processor = new ConditionalProcessor(tag, conditionalScript, scriptService, processor); + } + return processor; } catch (Exception e) { throw newConfigurationException(type, tag, null, e); } } throw newConfigurationException(type, tag, null, "No processor type exists with name [" + type + "]"); } + + private static Script extractConditional(Map config) throws IOException { + Object scriptSource = config.remove("if"); + if (scriptSource != null) { + try (XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent) + .map(normalizeScript(scriptSource)); + InputStream stream = BytesReference.bytes(builder).streamInput(); + XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, stream)) { + return Script.parse(parser); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private static Map normalizeScript(Object scriptConfig) { + if (scriptConfig instanceof Map) { + return (Map) scriptConfig; + } else if (scriptConfig instanceof String) { + return Collections.singletonMap("source", scriptConfig); + } else { + throw newConfigurationException("conditional", null, "script", + "property isn't a map or string, but of type [" + scriptConfig.getClass().getName() + "]"); + } + } } diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestService.java b/server/src/main/java/org/elasticsearch/ingest/IngestService.java index eee14e958699..f0f5d76caaba 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestService.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestService.java @@ -71,6 +71,7 @@ public class IngestService implements ClusterStateApplier { public static final String NOOP_PIPELINE_NAME = "_none"; private final ClusterService clusterService; + private final ScriptService scriptService; private final Map processorFactories; // Ideally this should be in IngestMetadata class, but we don't have the processor factories around there. // We know of all the processor factories when a node with all its plugin have been initialized. Also some @@ -85,6 +86,7 @@ public IngestService(ClusterService clusterService, ThreadPool threadPool, Environment env, ScriptService scriptService, AnalysisRegistry analysisRegistry, List ingestPlugins) { this.clusterService = clusterService; + this.scriptService = scriptService; this.processorFactories = processorFactories( ingestPlugins, new Processor.Parameters( @@ -116,6 +118,10 @@ public ClusterService getClusterService() { return clusterService; } + public ScriptService getScriptService() { + return scriptService; + } + /** * Deletes the pipeline specified by id in the request. */ @@ -300,11 +306,12 @@ void validatePipeline(Map ingestInfos, PutPipelineReq } Map pipelineConfig = XContentHelper.convertToMap(request.getSource(), false, request.getXContentType()).v2(); - Pipeline pipeline = Pipeline.create(request.getId(), pipelineConfig, processorFactories); + Pipeline pipeline = Pipeline.create(request.getId(), pipelineConfig, processorFactories, scriptService); List exceptions = new ArrayList<>(); for (Processor processor : pipeline.flattenAllProcessors()) { for (Map.Entry entry : ingestInfos.entrySet()) { - if (entry.getValue().containsProcessor(processor.getType()) == false) { + String type = processor.getType(); + if (entry.getValue().containsProcessor(type) == false && ConditionalProcessor.TYPE.equals(type) == false) { String message = "Processor type [" + processor.getType() + "] is not installed on node [" + entry.getKey() + "]"; exceptions.add( ConfigurationUtils.newConfigurationException(processor.getType(), processor.getTag(), null, message) @@ -452,7 +459,10 @@ private void innerUpdatePipelines(ClusterState previousState, ClusterState state List exceptions = new ArrayList<>(); for (PipelineConfiguration pipeline : ingestMetadata.getPipelines().values()) { try { - pipelines.put(pipeline.getId(), Pipeline.create(pipeline.getId(), pipeline.getConfigAsMap(), processorFactories)); + pipelines.put( + pipeline.getId(), + Pipeline.create(pipeline.getId(), pipeline.getConfigAsMap(), processorFactories, scriptService) + ); } catch (ElasticsearchParseException e) { pipelines.put(pipeline.getId(), substitutePipeline(pipeline.getId(), e)); exceptions.add(e); diff --git a/server/src/main/java/org/elasticsearch/ingest/Pipeline.java b/server/src/main/java/org/elasticsearch/ingest/Pipeline.java index 37dd3f52cb7d..0a8f9fbc0d89 100644 --- a/server/src/main/java/org/elasticsearch/ingest/Pipeline.java +++ b/server/src/main/java/org/elasticsearch/ingest/Pipeline.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.elasticsearch.script.ScriptService; /** * A pipeline is a list of {@link Processor} instances grouped under a unique id. @@ -52,14 +53,15 @@ public Pipeline(String id, @Nullable String description, @Nullable Integer versi } public static Pipeline create(String id, Map config, - Map processorFactories) throws Exception { + Map processorFactories, ScriptService scriptService) throws Exception { String description = ConfigurationUtils.readOptionalStringProperty(null, null, config, DESCRIPTION_KEY); Integer version = ConfigurationUtils.readIntProperty(null, null, config, VERSION_KEY, null); List> processorConfigs = ConfigurationUtils.readList(null, null, config, PROCESSORS_KEY); - List processors = ConfigurationUtils.readProcessorConfigs(processorConfigs, processorFactories); + List processors = ConfigurationUtils.readProcessorConfigs(processorConfigs, scriptService, processorFactories); List> onFailureProcessorConfigs = ConfigurationUtils.readOptionalList(null, null, config, ON_FAILURE_KEY); - List onFailureProcessors = ConfigurationUtils.readProcessorConfigs(onFailureProcessorConfigs, processorFactories); + List onFailureProcessors = + ConfigurationUtils.readProcessorConfigs(onFailureProcessorConfigs, scriptService, processorFactories); if (config.isEmpty() == false) { throw new ElasticsearchParseException("pipeline [" + id + "] doesn't support one or more provided configuration parameters " + Arrays.toString(config.keySet().toArray())); diff --git a/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java new file mode 100644 index 000000000000..27ce29b95dc5 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.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.script; + +import java.util.Map; + +/** + * A script used by {@link org.elasticsearch.ingest.ConditionalProcessor}. + */ +public abstract class IngestConditionalScript { + + public static final String[] PARAMETERS = { "ctx" }; + + /** The context used to compile {@link IngestConditionalScript} factories. */ + public static final ScriptContext CONTEXT = new ScriptContext<>("processor_conditional", Factory.class); + + /** The generic runtime parameters for the script. */ + private final Map params; + + public IngestConditionalScript(Map params) { + this.params = params; + } + + /** Return the parameters for this script. */ + public Map getParams() { + return params; + } + + public abstract boolean execute(Map ctx); + + public interface Factory { + IngestConditionalScript newInstance(Map params); + } +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptModule.java b/server/src/main/java/org/elasticsearch/script/ScriptModule.java index f04e690fa425..1788d8c792bf 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptModule.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptModule.java @@ -51,6 +51,7 @@ public class ScriptModule { BucketAggregationSelectorScript.CONTEXT, SignificantTermsHeuristicScoreScript.CONTEXT, IngestScript.CONTEXT, + IngestConditionalScript.CONTEXT, FilterScript.CONTEXT, SimilarityScript.CONTEXT, SimilarityWeightScript.CONTEXT, diff --git a/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java b/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java new file mode 100644 index 000000000000..2cb13af7a280 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java @@ -0,0 +1,141 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.ingest; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.script.MockScriptEngine; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptModule; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.Is.is; + +public class ConditionalProcessorTests extends ESTestCase { + + public void testChecksCondition() throws Exception { + String conditionalField = "field1"; + String scriptName = "conditionalScript"; + String trueValue = "truthy"; + ScriptService scriptService = new ScriptService(Settings.builder().build(), + Collections.singletonMap( + Script.DEFAULT_SCRIPT_LANG, + new MockScriptEngine( + Script.DEFAULT_SCRIPT_LANG, + Collections.singletonMap( + scriptName, ctx -> trueValue.equals(ctx.get(conditionalField)) + ) + ) + ), + new HashMap<>(ScriptModule.CORE_CONTEXTS) + ); + Map document = new HashMap<>(); + ConditionalProcessor processor = new ConditionalProcessor( + randomAlphaOfLength(10), + new Script( + ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, + scriptName, Collections.emptyMap()), scriptService, + new Processor() { + @Override + public void execute(final IngestDocument ingestDocument) throws Exception { + ingestDocument.setFieldValue("foo", "bar"); + } + + @Override + public String getType() { + return null; + } + + @Override + public String getTag() { + return null; + } + }); + + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + ingestDocument.setFieldValue(conditionalField, trueValue); + processor.execute(ingestDocument); + assertThat(ingestDocument.getSourceAndMetadata().get(conditionalField), is(trueValue)); + assertThat(ingestDocument.getSourceAndMetadata().get("foo"), is("bar")); + + String falseValue = "falsy"; + ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + ingestDocument.setFieldValue(conditionalField, falseValue); + processor.execute(ingestDocument); + assertThat(ingestDocument.getSourceAndMetadata().get(conditionalField), is(falseValue)); + assertThat(ingestDocument.getSourceAndMetadata(), not(hasKey("foo"))); + } + + @SuppressWarnings("unchecked") + public void testActsOnImmutableData() throws Exception { + assertMutatingCtxThrows(ctx -> ctx.remove("foo")); + assertMutatingCtxThrows(ctx -> ctx.put("foo", "bar")); + assertMutatingCtxThrows(ctx -> ((List)ctx.get("listField")).add("bar")); + assertMutatingCtxThrows(ctx -> ((List)ctx.get("listField")).remove("bar")); + } + + private static void assertMutatingCtxThrows(Consumer> mutation) throws Exception { + String scriptName = "conditionalScript"; + CompletableFuture expectedException = new CompletableFuture<>(); + ScriptService scriptService = new ScriptService(Settings.builder().build(), + Collections.singletonMap( + Script.DEFAULT_SCRIPT_LANG, + new MockScriptEngine( + Script.DEFAULT_SCRIPT_LANG, + Collections.singletonMap( + scriptName, ctx -> { + try { + mutation.accept(ctx); + } catch (Exception e) { + expectedException.complete(e); + } + return false; + } + ) + ) + ), + new HashMap<>(ScriptModule.CORE_CONTEXTS) + ); + Map document = new HashMap<>(); + ConditionalProcessor processor = new ConditionalProcessor( + randomAlphaOfLength(10), + new Script( + ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, + scriptName, Collections.emptyMap()), scriptService, null + ); + IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document); + ingestDocument.setFieldValue("listField", new ArrayList<>()); + processor.execute(ingestDocument); + Exception e = expectedException.get(); + assertThat(e, instanceOf(UnsupportedOperationException.class)); + assertEquals("Mutating ingest documents in conditionals is not supported", e.getMessage()); + } +} diff --git a/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java b/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java index 61afd9ce2a47..f3a11a86e54e 100644 --- a/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/ConfigurationUtilsTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.ingest; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; import org.junit.Before; @@ -38,6 +39,9 @@ import static org.mockito.Mockito.mock; public class ConfigurationUtilsTests extends ESTestCase { + + private final ScriptService scriptService = mock(ScriptService.class); + private Map config; @Before @@ -120,7 +124,7 @@ public void testReadProcessors() throws Exception { config.add(Collections.singletonMap("test_processor", emptyConfig)); config.add(Collections.singletonMap("test_processor", emptyConfig)); - List result = ConfigurationUtils.readProcessorConfigs(config, registry); + List result = ConfigurationUtils.readProcessorConfigs(config, scriptService, registry); assertThat(result.size(), equalTo(2)); assertThat(result.get(0), sameInstance(processor)); assertThat(result.get(1), sameInstance(processor)); @@ -129,7 +133,7 @@ public void testReadProcessors() throws Exception { unknownTaggedConfig.put("tag", "my_unknown"); config.add(Collections.singletonMap("unknown_processor", unknownTaggedConfig)); ElasticsearchParseException e = expectThrows(ElasticsearchParseException.class, - () -> ConfigurationUtils.readProcessorConfigs(config, registry)); + () -> ConfigurationUtils.readProcessorConfigs(config, scriptService, registry)); assertThat(e.getMessage(), equalTo("No processor type exists with name [unknown_processor]")); assertThat(e.getMetadata("es.processor_tag"), equalTo(Collections.singletonList("my_unknown"))); assertThat(e.getMetadata("es.processor_type"), equalTo(Collections.singletonList("unknown_processor"))); @@ -142,7 +146,10 @@ public void testReadProcessors() throws Exception { Map secondUnknonwTaggedConfig = new HashMap<>(); secondUnknonwTaggedConfig.put("tag", "my_second_unknown"); config2.add(Collections.singletonMap("second_unknown_processor", secondUnknonwTaggedConfig)); - e = expectThrows(ElasticsearchParseException.class, () -> ConfigurationUtils.readProcessorConfigs(config2, registry)); + e = expectThrows( + ElasticsearchParseException.class, + () -> ConfigurationUtils.readProcessorConfigs(config2, scriptService, registry) + ); assertThat(e.getMessage(), equalTo("No processor type exists with name [unknown_processor]")); assertThat(e.getMetadata("es.processor_tag"), equalTo(Collections.singletonList("my_unknown"))); assertThat(e.getMetadata("es.processor_type"), equalTo(Collections.singletonList("unknown_processor"))); @@ -166,17 +173,17 @@ public void testReadProcessorFromObjectOrMap() throws Exception { }); Object emptyConfig = Collections.emptyMap(); - Processor processor1 = ConfigurationUtils.readProcessor(registry, "script", emptyConfig); + Processor processor1 = ConfigurationUtils.readProcessor(registry, scriptService, "script", emptyConfig); assertThat(processor1, sameInstance(processor)); Object inlineScript = "test_script"; - Processor processor2 = ConfigurationUtils.readProcessor(registry, "script", inlineScript); + Processor processor2 = ConfigurationUtils.readProcessor(registry, scriptService, "script", inlineScript); assertThat(processor2, sameInstance(processor)); Object invalidConfig = 12L; ElasticsearchParseException ex = expectThrows(ElasticsearchParseException.class, - () -> ConfigurationUtils.readProcessor(registry, "unknown_processor", invalidConfig)); + () -> ConfigurationUtils.readProcessor(registry, scriptService, "unknown_processor", invalidConfig)); assertThat(ex.getMessage(), equalTo("property isn't a map, but of type [" + invalidConfig.getClass().getName() + "]")); } diff --git a/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java b/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java index cafdbcfb4469..d6d7b4ffa816 100644 --- a/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/PipelineFactoryTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.ingest; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; import java.util.Arrays; @@ -32,11 +33,13 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; public class PipelineFactoryTests extends ESTestCase { private final Integer version = randomBoolean() ? randomInt() : null; private final String versionString = version != null ? Integer.toString(version) : null; + private final ScriptService scriptService = mock(ScriptService.class); public void testCreate() throws Exception { Map processorConfig0 = new HashMap<>(); @@ -48,7 +51,7 @@ public void testCreate() throws Exception { pipelineConfig.put(Pipeline.PROCESSORS_KEY, Arrays.asList(Collections.singletonMap("test", processorConfig0), Collections.singletonMap("test", processorConfig1))); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -64,7 +67,7 @@ public void testCreateWithNoProcessorsField() throws Exception { pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); try { - Pipeline.create("_id", pipelineConfig, Collections.emptyMap()); + Pipeline.create("_id", pipelineConfig, Collections.emptyMap(), scriptService); fail("should fail, missing required [processors] field"); } catch (ElasticsearchParseException e) { assertThat(e.getMessage(), equalTo("[processors] required property is missing")); @@ -76,7 +79,7 @@ public void testCreateWithEmptyProcessorsField() throws Exception { pipelineConfig.put(Pipeline.DESCRIPTION_KEY, "_description"); pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.emptyList()); - Pipeline pipeline = Pipeline.create("_id", pipelineConfig, null); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, null, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -91,7 +94,7 @@ public void testCreateWithPipelineOnFailure() throws Exception { pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); pipelineConfig.put(Pipeline.ON_FAILURE_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -109,7 +112,10 @@ public void testCreateWithPipelineEmptyOnFailure() throws Exception { pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); pipelineConfig.put(Pipeline.ON_FAILURE_KEY, Collections.emptyList()); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Exception e = expectThrows(ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry)); + Exception e = expectThrows( + ElasticsearchParseException.class, + () -> Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService) + ); assertThat(e.getMessage(), equalTo("pipeline [_id] cannot have an empty on_failure option defined")); } @@ -121,7 +127,10 @@ public void testCreateWithPipelineEmptyOnFailureInProcessor() throws Exception { pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Exception e = expectThrows(ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry)); + Exception e = expectThrows( + ElasticsearchParseException.class, + () -> Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService) + ); assertThat(e.getMessage(), equalTo("[on_failure] processors list cannot be empty")); } @@ -136,7 +145,7 @@ public void testCreateWithPipelineIgnoreFailure() throws Exception { pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); - Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); @@ -156,7 +165,10 @@ public void testCreateUnusedProcessorOptions() throws Exception { pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Exception e = expectThrows(ElasticsearchParseException.class, () -> Pipeline.create("_id", pipelineConfig, processorRegistry)); + Exception e = expectThrows( + ElasticsearchParseException.class, + () -> Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService) + ); assertThat(e.getMessage(), equalTo("processor [test] doesn't support one or more provided configuration parameters [unused]")); } @@ -169,7 +181,7 @@ public void testCreateProcessorsWithOnFailureProperties() throws Exception { pipelineConfig.put(Pipeline.VERSION_KEY, versionString); pipelineConfig.put(Pipeline.PROCESSORS_KEY, Collections.singletonList(Collections.singletonMap("test", processorConfig))); Map processorRegistry = Collections.singletonMap("test", new TestProcessor.Factory()); - Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry); + Pipeline pipeline = Pipeline.create("_id", pipelineConfig, processorRegistry, scriptService); assertThat(pipeline.getId(), equalTo("_id")); assertThat(pipeline.getDescription(), equalTo("_description")); assertThat(pipeline.getVersion(), equalTo(version)); 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 4e2b8259e6fb..0ee5798efb30 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -96,6 +96,14 @@ public void execute(Map ctx) { } }; return context.factoryClazz.cast(factory); + } else if (context.instanceClazz.equals(IngestConditionalScript.class)) { + IngestConditionalScript.Factory factory = parameters -> new IngestConditionalScript(parameters) { + @Override + public boolean execute(Map ctx) { + return (boolean) script.apply(ctx); + } + }; + return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(UpdateScript.class)) { UpdateScript.Factory factory = parameters -> new UpdateScript(parameters) { @Override From 6fd971040e6bacde60ca3b617951f4e316c32ed9 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Thu, 30 Aug 2018 12:08:29 +1000 Subject: [PATCH 226/283] [Kerberos] Add unsupported languages for tests (#33253) Ran for all locales in system to find locales which caused problems in tests due to incorrect generalized time handling in simple kdc ldap server. Closes#33228 --- .../xpack/security/authc/kerberos/KerberosTestCase.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java index 2bd1bdf906ad..f97afc1d52c2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java @@ -74,6 +74,13 @@ public abstract class KerberosTestCase extends ESTestCase { unsupportedLocaleLanguages.add("ne"); unsupportedLocaleLanguages.add("dz"); unsupportedLocaleLanguages.add("mzn"); + unsupportedLocaleLanguages.add("mr"); + unsupportedLocaleLanguages.add("as"); + unsupportedLocaleLanguages.add("bn"); + unsupportedLocaleLanguages.add("lrc"); + unsupportedLocaleLanguages.add("my"); + unsupportedLocaleLanguages.add("ps"); + unsupportedLocaleLanguages.add("ur"); } @BeforeClass From f0635870832dbe1d06ef346f700cdda0feb4f045 Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Wed, 29 Aug 2018 21:44:33 -0600 Subject: [PATCH 227/283] HLRC: add client side RefreshPolicy (#33209) With the switch to client side request and response objects, we need a client side version of RefreshPolicy. This change adds a client side version of RefreshPolicy along with a method to add it to the parameters of a request. The existing method to add WriteRequest.RefreshPolicy to the parameters of a request is now deprecated. --- .../client/RequestConverters.java | 15 ++++- .../client/security/RefreshPolicy.java | 59 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/RefreshPolicy.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 15bbde6b5bfa..c45a8e1005e4 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -88,6 +88,7 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.client.security.RefreshPolicy; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Priority; @@ -1353,11 +1354,16 @@ Params withRealtime(boolean realtime) { Params withRefresh(boolean refresh) { if (refresh) { - return withRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + return withRefreshPolicy(RefreshPolicy.IMMEDIATE); } return this; } + /** + * @deprecated If creating a new HLRC ReST API call, use {@link RefreshPolicy} + * instead of {@link WriteRequest.RefreshPolicy} from the server project + */ + @Deprecated Params withRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { if (refreshPolicy != WriteRequest.RefreshPolicy.NONE) { return putParam("refresh", refreshPolicy.getValue()); @@ -1365,6 +1371,13 @@ Params withRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { return this; } + Params withRefreshPolicy(RefreshPolicy refreshPolicy) { + if (refreshPolicy != RefreshPolicy.NONE) { + return putParam("refresh", refreshPolicy.getValue()); + } + return this; + } + Params withRetryOnConflict(int retryOnConflict) { if (retryOnConflict > 0) { return putParam("retry_on_conflict", String.valueOf(retryOnConflict)); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/RefreshPolicy.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/RefreshPolicy.java new file mode 100644 index 000000000000..8b72f704edff --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/RefreshPolicy.java @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security; + +/** + * Enumeration of values that control the refresh policy for a request that + * supports specifying a refresh policy. + */ +public enum RefreshPolicy { + + /** + * Don't refresh after this request. The default. + */ + NONE("false"), + /** + * Force a refresh as part of this request. This refresh policy does not scale for high indexing or search throughput but is useful + * to present a consistent view to for indices with very low traffic. And it is wonderful for tests! + */ + IMMEDIATE("true"), + /** + * Leave this request open until a refresh has made the contents of this request visible to search. This refresh policy is + * compatible with high indexing and search throughput but it causes the request to wait to reply until a refresh occurs. + */ + WAIT_UNTIL("wait_for"); + + private final String value; + + RefreshPolicy(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + /** + * Get the default refresh policy, which is NONE + */ + public static RefreshPolicy getDefault() { + return RefreshPolicy.NONE; + } +} From 47859e56ac31f2c453a9eed6f5217171e0bda7f5 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 30 Aug 2018 06:43:04 +0100 Subject: [PATCH 228/283] Move file-based discovery to core (#33241) Today we support a static list of seed hosts in core Elasticsearch, and allow a dynamic list of seed hosts to be provided via a file using the `discovery-file` plugin. In fact the ability to provide a dynamic list of seed hosts is increasingly useful, so this change moves this functionality to core Elasticsearch to avoid the need for a plugin. Furthermore, in order to start up nodes in integration tests we currently assign a known port to each node before startup, which unfortunately sometimes fails if another process grabs the selected port in the meantime. By moving the `discovery-file` functionality into the core product we can use it to avoid this race. This change also moves the expected path to the file from `$ES_PATH_CONF/discovery-file/unicast_hosts.txt` to `$ES_PATH_CONF/unicast_hosts.txt`. An example of this file is not included in distributions. For BWC purposes the plugin still exists, but does nothing more than create the example file in the old location, and issue a warning when it is used. We also continue to support the old location for the file, but warn about its deprecation. Relates #29244 Closes #33030 --- docs/plugins/discovery-file.asciidoc | 73 +----- docs/reference/modules/discovery/zen.asciidoc | 242 ++++++++++++------ .../file/FileBasedDiscoveryPlugin.java | 24 +- .../file/FileBasedUnicastHostsProvider.java | 83 ------ ...eBasedDiscoveryPluginDeprecationTests.java | 32 +++ .../discovery/DiscoveryModule.java | 5 +- .../zen/FileBasedUnicastHostsProvider.java | 92 +++++++ .../java/org/elasticsearch/node/Node.java | 2 +- .../discovery/DiscoveryModuleTests.java | 4 +- .../FileBasedUnicastHostsProviderTests.java | 103 +++++--- 10 files changed, 370 insertions(+), 290 deletions(-) delete mode 100644 plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedUnicastHostsProvider.java create mode 100644 plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPluginDeprecationTests.java create mode 100644 server/src/main/java/org/elasticsearch/discovery/zen/FileBasedUnicastHostsProvider.java rename {plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file => server/src/test/java/org/elasticsearch/discovery/zen}/FileBasedUnicastHostsProviderTests.java (63%) diff --git a/docs/plugins/discovery-file.asciidoc b/docs/plugins/discovery-file.asciidoc index ad06cfc0cc5f..4f2182da056a 100644 --- a/docs/plugins/discovery-file.asciidoc +++ b/docs/plugins/discovery-file.asciidoc @@ -1,71 +1,14 @@ [[discovery-file]] === File-Based Discovery Plugin -The file-based discovery plugin uses a list of hosts/ports in a `unicast_hosts.txt` file -in the `config/discovery-file` directory for unicast discovery. +The functionality provided by the `discovery-file` plugin is now available in +Elasticsearch without requiring a plugin. This plugin still exists to ensure +backwards compatibility, but it will be removed in a future version. + +On installation, this plugin creates a file at +`$ES_PATH_CONF/discovery-file/unicast_hosts.txt` that comprises comments that +describe how to use it. It is preferable not to install this plugin and instead +to create this file, and its containing directory, using standard tools. :plugin_name: discovery-file include::install_remove.asciidoc[] - -[[discovery-file-usage]] -[float] -==== Using the file-based discovery plugin - -The file-based discovery plugin provides the ability to specify the -unicast hosts list through a simple `unicast_hosts.txt` file that can -be dynamically updated at any time. To enable, add the following in `elasticsearch.yml`: - -[source,yaml] ----- -discovery.zen.hosts_provider: file ----- - -This plugin simply provides a facility to supply the unicast hosts list for -zen discovery through an external file that can be updated at any time by a side process. - -For example, this gives a convenient mechanism for an Elasticsearch instance -that is run in docker containers to be dynamically supplied a list of IP -addresses to connect to for zen discovery when those IP addresses may not be -known at node startup. - -Note that the file-based discovery plugin is meant to augment the unicast -hosts list in `elasticsearch.yml` (if specified), not replace it. Therefore, -if there are valid unicast host entries in `discovery.zen.ping.unicast.hosts`, -they will be used in addition to those supplied in `unicast_hosts.txt`. - -Anytime a change is made to the `unicast_hosts.txt` file, even as Elasticsearch -continues to run, the new changes will be picked up by the plugin and the -new hosts list will be used for the next pinging round for master election. - -Upon installation of the plugin, a default `unicast_hosts.txt` file will -be found in the `$CONFIG_DIR/discovery-file` directory. This default file -will contain some comments about what the file should contain. All comments -for this file must appear on their lines starting with `#` (i.e. comments -cannot start in the middle of a line). - -[[discovery-file-format]] -[float] -==== unicast_hosts.txt file format - -The format of the file is to specify one unicast host entry per line. -Each unicast host entry consists of the host (host name or IP address) and -an optional transport port number. If the port number is specified, is must -come immediately after the host (on the same line) separated by a `:`. -If the port number is not specified, a default value of 9300 is used. - -For example, this is an example of `unicast_hosts.txt` for a cluster with -four nodes that participate in unicast discovery, some of which are not -running on the default port: - -[source,txt] ----------------------------------------------------------------- -10.10.10.5 -10.10.10.6:9305 -10.10.10.5:10005 -# an IPv6 address -[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9301 ----------------------------------------------------------------- - -Host names are allowed instead of IP addresses (similar to -`discovery.zen.ping.unicast.hosts`), and IPv6 addresses must be -specified in brackets with the port coming after the brackets. diff --git a/docs/reference/modules/discovery/zen.asciidoc b/docs/reference/modules/discovery/zen.asciidoc index f0f26a466596..d90be42d9178 100644 --- a/docs/reference/modules/discovery/zen.asciidoc +++ b/docs/reference/modules/discovery/zen.asciidoc @@ -1,13 +1,12 @@ [[modules-discovery-zen]] === Zen Discovery -The zen discovery is the built in discovery module for Elasticsearch and -the default. It provides unicast discovery, but can be extended to -support cloud environments and other forms of discovery. +Zen discovery is the built-in, default, discovery module for Elasticsearch. It +provides unicast and file-based discovery, and can be extended to support cloud +environments and other forms of discovery via plugins. -The zen discovery is integrated with other modules, for example, all -communication between nodes is done using the -<> module. +Zen discovery is integrated with other modules, for example, all communication +between nodes is done using the <> module. It is separated into several sub modules, which are explained below: @@ -15,86 +14,159 @@ It is separated into several sub modules, which are explained below: [[ping]] ==== Ping -This is the process where a node uses the discovery mechanisms to find -other nodes. +This is the process where a node uses the discovery mechanisms to find other +nodes. + +[float] +[[discovery-seed-nodes]] +==== Seed nodes + +Zen discovery uses a list of _seed_ nodes in order to start off the discovery +process. At startup, or when electing a new master, Elasticsearch tries to +connect to each seed node in its list, and holds a gossip-like conversation with +them to find other nodes and to build a complete picture of the cluster. By +default there are two methods for configuring the list of seed nodes: _unicast_ +and _file-based_. It is recommended that the list of seed nodes comprises the +list of master-eligible nodes in the cluster. [float] [[unicast]] ===== Unicast -Unicast discovery requires a list of hosts to use that will act as gossip -routers. These hosts can be specified as hostnames or IP addresses; hosts -specified as hostnames are resolved to IP addresses during each round of -pinging. Note that if you are in an environment where DNS resolutions vary with -time, you might need to adjust your <>. +Unicast discovery configures a static list of hosts for use as seed nodes. +These hosts can be specified as hostnames or IP addresses; hosts specified as +hostnames are resolved to IP addresses during each round of pinging. Note that +if you are in an environment where DNS resolutions vary with time, you might +need to adjust your <>. -It is recommended that the unicast hosts list be maintained as the list of -master-eligible nodes in the cluster. +The list of hosts is set using the `discovery.zen.ping.unicast.hosts` static +setting. This is either an array of hosts or a comma-delimited string. Each +value should be in the form of `host:port` or `host` (where `port` defaults to +the setting `transport.profiles.default.port` falling back to +`transport.tcp.port` if not set). Note that IPv6 hosts must be bracketed. The +default for this setting is `127.0.0.1, [::1]` -Unicast discovery provides the following settings with the `discovery.zen.ping.unicast` prefix: +Additionally, the `discovery.zen.ping.unicast.resolve_timeout` configures the +amount of time to wait for DNS lookups on each round of pinging. This is +specified as a <> and defaults to 5s. -[cols="<,<",options="header",] -|======================================================================= -|Setting |Description -|`hosts` |Either an array setting or a comma delimited setting. Each - value should be in the form of `host:port` or `host` (where `port` defaults to the setting `transport.profiles.default.port` - falling back to `transport.tcp.port` if not set). Note that IPv6 hosts must be bracketed. Defaults to `127.0.0.1, [::1]` -|`hosts.resolve_timeout` |The amount of time to wait for DNS lookups on each round of pinging. Specified as -<>. Defaults to 5s. -|======================================================================= +Unicast discovery uses the <> module to perform the +discovery. -The unicast discovery uses the <> module to perform the discovery. +[float] +[[file-based-hosts-provider]] +===== File-based + +In addition to hosts provided by the static `discovery.zen.ping.unicast.hosts` +setting, it is possible to provide a list of hosts via an external file. +Elasticsearch reloads this file when it changes, so that the list of seed nodes +can change dynamically without needing to restart each node. For example, this +gives a convenient mechanism for an Elasticsearch instance that is run in a +Docker container to be dynamically supplied with a list of IP addresses to +connect to for Zen discovery when those IP addresses may not be known at node +startup. + +To enable file-based discovery, configure the `file` hosts provider as follows: + +``` +discovery.zen.hosts_provider: file +``` + +Then create a file at `$ES_PATH_CONF/unicast_hosts.txt` in +<>. Any time a change is made +to the `unicast_hosts.txt` file the new changes will be picked up by +Elasticsearch and the new hosts list will be used. + +Note that the file-based discovery plugin augments the unicast hosts list in +`elasticsearch.yml`: if there are valid unicast host entries in +`discovery.zen.ping.unicast.hosts` then they will be used in addition to those +supplied in `unicast_hosts.txt`. + +The `discovery.zen.ping.unicast.resolve_timeout` setting also applies to DNS +lookups for nodes specified by address via file-based discovery. This is +specified as a <> and defaults to 5s. + +[[discovery-file-format]] +[float] +====== unicast_hosts.txt file format + +The format of the file is to specify one node entry per line. Each node entry +consists of the host (host name or IP address) and an optional transport port +number. If the port number is specified, is must come immediately after the +host (on the same line) separated by a `:`. If the port number is not +specified, a default value of 9300 is used. + +For example, this is an example of `unicast_hosts.txt` for a cluster with four +nodes that participate in unicast discovery, some of which are not running on +the default port: + +[source,txt] +---------------------------------------------------------------- +10.10.10.5 +10.10.10.6:9305 +10.10.10.5:10005 +# an IPv6 address +[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:9301 +---------------------------------------------------------------- + +Host names are allowed instead of IP addresses (similar to +`discovery.zen.ping.unicast.hosts`), and IPv6 addresses must be specified in +brackets with the port coming after the brackets. + +It is also possible to add comments to this file. All comments must appear on +their lines starting with `#` (i.e. comments cannot start in the middle of a +line). [float] [[master-election]] ==== Master Election -As part of the ping process a master of the cluster is either -elected or joined to. This is done automatically. The -`discovery.zen.ping_timeout` (which defaults to `3s`) determines how long the node -will wait before deciding on starting an election or joining an existing cluster. -Three pings will be sent over this timeout interval. In case where no decision can be -reached after the timeout, the pinging process restarts. -In slow or congested networks, three seconds might not be enough for a node to become -aware of the other nodes in its environment before making an election decision. -Increasing the timeout should be done with care in that case, as it will slow down the -election process. -Once a node decides to join an existing formed cluster, it -will send a join request to the master (`discovery.zen.join_timeout`) -with a timeout defaulting at 20 times the ping timeout. - -When the master node stops or has encountered a problem, the cluster nodes -start pinging again and will elect a new master. This pinging round also -serves as a protection against (partial) network failures where a node may unjustly -think that the master has failed. In this case the node will simply hear from -other nodes about the currently active master. - -If `discovery.zen.master_election.ignore_non_master_pings` is `true`, pings from nodes that are not master -eligible (nodes where `node.master` is `false`) are ignored during master election; the default value is +As part of the ping process a master of the cluster is either elected or joined +to. This is done automatically. The `discovery.zen.ping_timeout` (which defaults +to `3s`) determines how long the node will wait before deciding on starting an +election or joining an existing cluster. Three pings will be sent over this +timeout interval. In case where no decision can be reached after the timeout, +the pinging process restarts. In slow or congested networks, three seconds +might not be enough for a node to become aware of the other nodes in its +environment before making an election decision. Increasing the timeout should +be done with care in that case, as it will slow down the election process. Once +a node decides to join an existing formed cluster, it will send a join request +to the master (`discovery.zen.join_timeout`) with a timeout defaulting at 20 +times the ping timeout. + +When the master node stops or has encountered a problem, the cluster nodes start +pinging again and will elect a new master. This pinging round also serves as a +protection against (partial) network failures where a node may unjustly think +that the master has failed. In this case the node will simply hear from other +nodes about the currently active master. + +If `discovery.zen.master_election.ignore_non_master_pings` is `true`, pings from +nodes that are not master eligible (nodes where `node.master` is `false`) are +ignored during master election; the default value is `false`. + +Nodes can be excluded from becoming a master by setting `node.master` to `false`. -Nodes can be excluded from becoming a master by setting `node.master` to `false`. - -The `discovery.zen.minimum_master_nodes` sets the minimum -number of master eligible nodes that need to join a newly elected master in order for an election to -complete and for the elected node to accept its mastership. The same setting controls the minimum number of -active master eligible nodes that should be a part of any active cluster. If this requirement is not met the -active master node will step down and a new master election will begin. +The `discovery.zen.minimum_master_nodes` sets the minimum number of master +eligible nodes that need to join a newly elected master in order for an election +to complete and for the elected node to accept its mastership. The same setting +controls the minimum number of active master eligible nodes that should be a +part of any active cluster. If this requirement is not met the active master +node will step down and a new master election will begin. This setting must be set to a <> of your master eligible nodes. It is recommended to avoid having only two master eligible -nodes, since a quorum of two is two. Therefore, a loss of either master -eligible node will result in an inoperable cluster. +nodes, since a quorum of two is two. Therefore, a loss of either master eligible +node will result in an inoperable cluster. [float] [[fault-detection]] ==== Fault Detection -There are two fault detection processes running. The first is by the -master, to ping all the other nodes in the cluster and verify that they -are alive. And on the other end, each node pings to master to verify if -its still alive or an election process needs to be initiated. +There are two fault detection processes running. The first is by the master, to +ping all the other nodes in the cluster and verify that they are alive. And on +the other end, each node pings to master to verify if its still alive or an +election process needs to be initiated. The following settings control the fault detection process using the `discovery.zen.fd` prefix: @@ -116,19 +188,21 @@ considered failed. Defaults to `3`. The master node is the only node in a cluster that can make changes to the cluster state. The master node processes one cluster state update at a time, -applies the required changes and publishes the updated cluster state to all -the other nodes in the cluster. Each node receives the publish message, acknowledges -it, but does *not* yet apply it. If the master does not receive acknowledgement from -at least `discovery.zen.minimum_master_nodes` nodes within a certain time (controlled by -the `discovery.zen.commit_timeout` setting and defaults to 30 seconds) the cluster state -change is rejected. - -Once enough nodes have responded, the cluster state is committed and a message will -be sent to all the nodes. The nodes then proceed to apply the new cluster state to their -internal state. The master node waits for all nodes to respond, up to a timeout, before -going ahead processing the next updates in the queue. The `discovery.zen.publish_timeout` is -set by default to 30 seconds and is measured from the moment the publishing started. Both -timeout settings can be changed dynamically through the <> +applies the required changes and publishes the updated cluster state to all the +other nodes in the cluster. Each node receives the publish message, acknowledges +it, but does *not* yet apply it. If the master does not receive acknowledgement +from at least `discovery.zen.minimum_master_nodes` nodes within a certain time +(controlled by the `discovery.zen.commit_timeout` setting and defaults to 30 +seconds) the cluster state change is rejected. + +Once enough nodes have responded, the cluster state is committed and a message +will be sent to all the nodes. The nodes then proceed to apply the new cluster +state to their internal state. The master node waits for all nodes to respond, +up to a timeout, before going ahead processing the next updates in the queue. +The `discovery.zen.publish_timeout` is set by default to 30 seconds and is +measured from the moment the publishing started. Both timeout settings can be +changed dynamically through the <> [float] [[no-master-block]] @@ -143,10 +217,14 @@ rejected when there is no active master. The `discovery.zen.no_master_block` setting has two valid options: [horizontal] -`all`:: All operations on the node--i.e. both read & writes--will be rejected. This also applies for api cluster state -read or write operations, like the get index settings, put mapping and cluster state api. -`write`:: (default) Write operations will be rejected. Read operations will succeed, based on the last known cluster configuration. -This may result in partial reads of stale data as this node may be isolated from the rest of the cluster. - -The `discovery.zen.no_master_block` setting doesn't apply to nodes-based apis (for example cluster stats, node info and -node stats apis). Requests to these apis will not be blocked and can run on any available node. +`all`:: All operations on the node--i.e. both read & writes--will be rejected. +This also applies for api cluster state read or write operations, like the get +index settings, put mapping and cluster state api. +`write`:: (default) Write operations will be rejected. Read operations will +succeed, based on the last known cluster configuration. This may result in +partial reads of stale data as this node may be isolated from the rest of the +cluster. + +The `discovery.zen.no_master_block` setting doesn't apply to nodes-based apis +(for example cluster stats, node info and node stats apis). Requests to these +apis will not be blocked and can run on any available node. diff --git a/plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPlugin.java b/plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPlugin.java index 4d2644707859..48fa49b9a8a3 100644 --- a/plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPlugin.java +++ b/plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPlugin.java @@ -19,39 +19,33 @@ package org.elasticsearch.discovery.file; +import org.elasticsearch.common.logging.DeprecationLogger; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.discovery.zen.UnicastHostsProvider; -import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.DiscoveryPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.transport.TransportService; -import java.nio.file.Path; import java.util.Collections; import java.util.Map; import java.util.function.Supplier; -/** - * Plugin for providing file-based unicast hosts discovery. The list of unicast hosts - * is obtained by reading the {@link FileBasedUnicastHostsProvider#UNICAST_HOSTS_FILE} in - * the {@link Environment#configFile()}/discovery-file directory. - */ public class FileBasedDiscoveryPlugin extends Plugin implements DiscoveryPlugin { - private final Settings settings; - private final Path configPath; + private final DeprecationLogger deprecationLogger; + static final String DEPRECATION_MESSAGE + = "File-based discovery is now built into Elasticsearch and does not require the discovery-file plugin"; - public FileBasedDiscoveryPlugin(Settings settings, Path configPath) { - this.settings = settings; - this.configPath = configPath; + public FileBasedDiscoveryPlugin(Settings settings) { + deprecationLogger = new DeprecationLogger(Loggers.getLogger(this.getClass(), settings)); } @Override public Map> getZenHostsProviders(TransportService transportService, NetworkService networkService) { - return Collections.singletonMap( - "file", - () -> new FileBasedUnicastHostsProvider(new Environment(settings, configPath))); + deprecationLogger.deprecated(DEPRECATION_MESSAGE); + return Collections.emptyMap(); } } diff --git a/plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedUnicastHostsProvider.java b/plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedUnicastHostsProvider.java deleted file mode 100644 index 584ae4de5a2b..000000000000 --- a/plugins/discovery-file/src/main/java/org/elasticsearch/discovery/file/FileBasedUnicastHostsProvider.java +++ /dev/null @@ -1,83 +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.discovery.file; - -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.logging.log4j.util.Supplier; -import org.elasticsearch.common.component.AbstractComponent; -import org.elasticsearch.common.transport.TransportAddress; -import org.elasticsearch.discovery.zen.UnicastHostsProvider; -import org.elasticsearch.env.Environment; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * An implementation of {@link UnicastHostsProvider} that reads hosts/ports - * from {@link #UNICAST_HOSTS_FILE}. - * - * Each unicast host/port that is part of the discovery process must be listed on - * a separate line. If the port is left off an entry, a default port of 9300 is - * assumed. An example unicast hosts file could read: - * - * 67.81.244.10 - * 67.81.244.11:9305 - * 67.81.244.15:9400 - */ -class FileBasedUnicastHostsProvider extends AbstractComponent implements UnicastHostsProvider { - - static final String UNICAST_HOSTS_FILE = "unicast_hosts.txt"; - - private final Path unicastHostsFilePath; - - FileBasedUnicastHostsProvider(Environment environment) { - super(environment.settings()); - this.unicastHostsFilePath = environment.configFile().resolve("discovery-file").resolve(UNICAST_HOSTS_FILE); - } - - @Override - public List buildDynamicHosts(HostsResolver hostsResolver) { - List hostsList; - try (Stream lines = Files.lines(unicastHostsFilePath)) { - hostsList = lines.filter(line -> line.startsWith("#") == false) // lines starting with `#` are comments - .collect(Collectors.toList()); - } catch (FileNotFoundException | NoSuchFileException e) { - logger.warn((Supplier) () -> new ParameterizedMessage("[discovery-file] Failed to find unicast hosts file [{}]", - unicastHostsFilePath), e); - hostsList = Collections.emptyList(); - } catch (IOException e) { - logger.warn((Supplier) () -> new ParameterizedMessage("[discovery-file] Error reading unicast hosts file [{}]", - unicastHostsFilePath), e); - hostsList = Collections.emptyList(); - } - - final List dynamicHosts = hostsResolver.resolveHosts(hostsList, 1); - logger.debug("[discovery-file] Using dynamic discovery nodes {}", dynamicHosts); - return dynamicHosts; - } - -} diff --git a/plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPluginDeprecationTests.java b/plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPluginDeprecationTests.java new file mode 100644 index 000000000000..643c7b2c95c2 --- /dev/null +++ b/plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file/FileBasedDiscoveryPluginDeprecationTests.java @@ -0,0 +1,32 @@ +/* + * 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.file; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +import static org.elasticsearch.discovery.file.FileBasedDiscoveryPlugin.DEPRECATION_MESSAGE; + +public class FileBasedDiscoveryPluginDeprecationTests extends ESTestCase { + public void testDeprecationWarning() { + new FileBasedDiscoveryPlugin(Settings.EMPTY).getZenHostsProviders(null, null); + assertWarnings(DEPRECATION_MESSAGE); + } +} diff --git a/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java b/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java index e47fe7a7a70e..f34798605d78 100644 --- a/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java +++ b/server/src/main/java/org/elasticsearch/discovery/DiscoveryModule.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.discovery.single.SingleNodeDiscovery; +import org.elasticsearch.discovery.zen.FileBasedUnicastHostsProvider; import org.elasticsearch.discovery.zen.SettingsBasedHostsProvider; import org.elasticsearch.discovery.zen.UnicastHostsProvider; import org.elasticsearch.discovery.zen.ZenDiscovery; @@ -40,6 +41,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -69,10 +71,11 @@ public class DiscoveryModule { public DiscoveryModule(Settings settings, ThreadPool threadPool, TransportService transportService, NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService, MasterService masterService, ClusterApplier clusterApplier, ClusterSettings clusterSettings, List plugins, - AllocationService allocationService) { + AllocationService allocationService, Path configFile) { final Collection> joinValidators = new ArrayList<>(); final Map> hostProviders = new HashMap<>(); hostProviders.put("settings", () -> new SettingsBasedHostsProvider(settings, transportService)); + hostProviders.put("file", () -> new FileBasedUnicastHostsProvider(settings, configFile)); for (DiscoveryPlugin plugin : plugins) { plugin.getZenHostsProviders(transportService, networkService).entrySet().forEach(entry -> { if (hostProviders.put(entry.getKey(), entry.getValue()) != null) { diff --git a/server/src/main/java/org/elasticsearch/discovery/zen/FileBasedUnicastHostsProvider.java b/server/src/main/java/org/elasticsearch/discovery/zen/FileBasedUnicastHostsProvider.java new file mode 100644 index 000000000000..f339ae43a703 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/discovery/zen/FileBasedUnicastHostsProvider.java @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.discovery.zen; + +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * An implementation of {@link UnicastHostsProvider} that reads hosts/ports + * from {@link #UNICAST_HOSTS_FILE}. + * + * Each unicast host/port that is part of the discovery process must be listed on + * a separate line. If the port is left off an entry, a default port of 9300 is + * assumed. An example unicast hosts file could read: + * + * 67.81.244.10 + * 67.81.244.11:9305 + * 67.81.244.15:9400 + */ +public class FileBasedUnicastHostsProvider extends AbstractComponent implements UnicastHostsProvider { + + public static final String UNICAST_HOSTS_FILE = "unicast_hosts.txt"; + + private final Path unicastHostsFilePath; + private final Path legacyUnicastHostsFilePath; + + public FileBasedUnicastHostsProvider(Settings settings, Path configFile) { + super(settings); + this.unicastHostsFilePath = configFile.resolve(UNICAST_HOSTS_FILE); + this.legacyUnicastHostsFilePath = configFile.resolve("discovery-file").resolve(UNICAST_HOSTS_FILE); + } + + private List getHostsList() { + if (Files.exists(unicastHostsFilePath)) { + return readFileContents(unicastHostsFilePath); + } + + if (Files.exists(legacyUnicastHostsFilePath)) { + deprecationLogger.deprecated("Found dynamic hosts list at [{}] but this path is deprecated. This list should be at [{}] " + + "instead. Support for the deprecated path will be removed in future.", legacyUnicastHostsFilePath, unicastHostsFilePath); + return readFileContents(legacyUnicastHostsFilePath); + } + + logger.warn("expected, but did not find, a dynamic hosts list at [{}]", unicastHostsFilePath); + + return Collections.emptyList(); + } + + private List readFileContents(Path path) { + try (Stream lines = Files.lines(path)) { + return lines.filter(line -> line.startsWith("#") == false) // lines starting with `#` are comments + .collect(Collectors.toList()); + } catch (IOException e) { + logger.warn(() -> new ParameterizedMessage("failed to read file [{}]", unicastHostsFilePath), e); + return Collections.emptyList(); + } + } + + @Override + public List buildDynamicHosts(HostsResolver hostsResolver) { + final List transportAddresses = hostsResolver.resolveHosts(getHostsList(), 1); + logger.debug("seed addresses: {}", transportAddresses); + return transportAddresses; + } +} diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 9dfd8d2a382a..7c0513f9eeb0 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -471,7 +471,7 @@ protected Node(final Environment environment, Collection final DiscoveryModule discoveryModule = new DiscoveryModule(this.settings, threadPool, transportService, namedWriteableRegistry, networkService, clusterService.getMasterService(), clusterService.getClusterApplierService(), clusterService.getClusterSettings(), pluginsService.filterPlugins(DiscoveryPlugin.class), - clusterModule.getAllocationService()); + clusterModule.getAllocationService(), environment.configFile()); this.nodeService = new NodeService(settings, threadPool, monitorService, discoveryModule.getDiscovery(), transportService, indicesService, pluginsService, circuitBreakerService, scriptModule.getScriptService(), httpServerTransport, ingestService, clusterService, settingsModule.getSettingsFilter(), responseCollectorService, diff --git a/server/src/test/java/org/elasticsearch/discovery/DiscoveryModuleTests.java b/server/src/test/java/org/elasticsearch/discovery/DiscoveryModuleTests.java index f2491b2db1f9..82ec987420bb 100644 --- a/server/src/test/java/org/elasticsearch/discovery/DiscoveryModuleTests.java +++ b/server/src/test/java/org/elasticsearch/discovery/DiscoveryModuleTests.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.discovery; -import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -29,6 +28,7 @@ import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.discovery.zen.UnicastHostsProvider; import org.elasticsearch.discovery.zen.ZenDiscovery; import org.elasticsearch.plugins.DiscoveryPlugin; @@ -99,7 +99,7 @@ public void clearDummyServices() throws IOException { private DiscoveryModule newModule(Settings settings, List plugins) { return new DiscoveryModule(settings, threadPool, transportService, namedWriteableRegistry, null, masterService, - clusterApplier, clusterSettings, plugins, null); + clusterApplier, clusterSettings, plugins, null, createTempDir().toAbsolutePath()); } public void testDefaults() { diff --git a/plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file/FileBasedUnicastHostsProviderTests.java b/server/src/test/java/org/elasticsearch/discovery/zen/FileBasedUnicastHostsProviderTests.java similarity index 63% rename from plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file/FileBasedUnicastHostsProviderTests.java rename to server/src/test/java/org/elasticsearch/discovery/zen/FileBasedUnicastHostsProviderTests.java index 5837d3bcdfe3..8922a38ea1e7 100644 --- a/plugins/discovery-file/src/test/java/org/elasticsearch/discovery/file/FileBasedUnicastHostsProviderTests.java +++ b/server/src/test/java/org/elasticsearch/discovery/zen/FileBasedUnicastHostsProviderTests.java @@ -17,7 +17,7 @@ * under the License. */ -package org.elasticsearch.discovery.file; +package org.elasticsearch.discovery.zen; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.network.NetworkService; @@ -26,9 +26,7 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; -import org.elasticsearch.discovery.zen.UnicastZenPing; import org.elasticsearch.env.Environment; -import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.transport.MockTransportService; @@ -50,16 +48,15 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.elasticsearch.discovery.file.FileBasedUnicastHostsProvider.UNICAST_HOSTS_FILE; +import static org.elasticsearch.discovery.zen.FileBasedUnicastHostsProvider.UNICAST_HOSTS_FILE; -/** - * Tests for {@link FileBasedUnicastHostsProvider}. - */ public class FileBasedUnicastHostsProviderTests extends ESTestCase { + private boolean legacyLocation; private ThreadPool threadPool; private ExecutorService executorService; private MockTransportService transportService; + private Path configPath; @Before public void setUp() throws Exception { @@ -83,23 +80,20 @@ public void tearDown() throws Exception { @Before public void createTransportSvc() { - MockTcpTransport transport = - new MockTcpTransport(Settings.EMPTY, - threadPool, - BigArrays.NON_RECYCLING_INSTANCE, - new NoneCircuitBreakerService(), - new NamedWriteableRegistry(Collections.emptyList()), - new NetworkService(Collections.emptyList())) { - @Override - public BoundTransportAddress boundAddress() { - return new BoundTransportAddress( - new TransportAddress[]{new TransportAddress(InetAddress.getLoopbackAddress(), 9300)}, - new TransportAddress(InetAddress.getLoopbackAddress(), 9300) - ); - } - }; + final MockTcpTransport transport = new MockTcpTransport(Settings.EMPTY, threadPool, BigArrays.NON_RECYCLING_INSTANCE, + new NoneCircuitBreakerService(), + new NamedWriteableRegistry(Collections.emptyList()), + new NetworkService(Collections.emptyList())) { + @Override + public BoundTransportAddress boundAddress() { + return new BoundTransportAddress( + new TransportAddress[]{new TransportAddress(InetAddress.getLoopbackAddress(), 9300)}, + new TransportAddress(InetAddress.getLoopbackAddress(), 9300) + ); + } + }; transportService = new MockTransportService(Settings.EMPTY, transport, threadPool, TransportService.NOOP_TRANSPORT_INTERCEPTOR, - null); + null); } public void testBuildDynamicNodes() throws Exception { @@ -114,18 +108,27 @@ public void testBuildDynamicNodes() throws Exception { assertEquals(9300, nodes.get(2).getPort()); } + public void testBuildDynamicNodesLegacyLocation() throws Exception { + legacyLocation = true; + testBuildDynamicNodes(); + assertDeprecatedLocationWarning(); + } + public void testEmptyUnicastHostsFile() throws Exception { final List hostEntries = Collections.emptyList(); final List addresses = setupAndRunHostProvider(hostEntries); assertEquals(0, addresses.size()); } - public void testUnicastHostsDoesNotExist() throws Exception { - final Settings settings = Settings.builder() - .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()) - .build(); - final Environment environment = TestEnvironment.newEnvironment(settings); - final FileBasedUnicastHostsProvider provider = new FileBasedUnicastHostsProvider(environment); + public void testEmptyUnicastHostsFileLegacyLocation() throws Exception { + legacyLocation = true; + testEmptyUnicastHostsFile(); + assertDeprecatedLocationWarning(); + } + + public void testUnicastHostsDoesNotExist() { + final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()).build(); + final FileBasedUnicastHostsProvider provider = new FileBasedUnicastHostsProvider(settings, createTempDir().toAbsolutePath()); final List addresses = provider.buildDynamicHosts((hosts, limitPortCounts) -> UnicastZenPing.resolveHostsLists(executorService, logger, hosts, limitPortCounts, transportService, TimeValue.timeValueSeconds(10))); @@ -133,42 +136,60 @@ public void testUnicastHostsDoesNotExist() throws Exception { } public void testInvalidHostEntries() throws Exception { - List hostEntries = Arrays.asList("192.168.0.1:9300:9300"); - List addresses = setupAndRunHostProvider(hostEntries); + final List hostEntries = Arrays.asList("192.168.0.1:9300:9300"); + final List addresses = setupAndRunHostProvider(hostEntries); assertEquals(0, addresses.size()); } + public void testInvalidHostEntriesLegacyLocation() throws Exception { + legacyLocation = true; + testInvalidHostEntries(); + assertDeprecatedLocationWarning(); + } + public void testSomeInvalidHostEntries() throws Exception { - List hostEntries = Arrays.asList("192.168.0.1:9300:9300", "192.168.0.1:9301"); - List addresses = setupAndRunHostProvider(hostEntries); + final List hostEntries = Arrays.asList("192.168.0.1:9300:9300", "192.168.0.1:9301"); + final List addresses = setupAndRunHostProvider(hostEntries); assertEquals(1, addresses.size()); // only one of the two is valid and will be used assertEquals("192.168.0.1", addresses.get(0).getAddress()); assertEquals(9301, addresses.get(0).getPort()); } + public void testSomeInvalidHostEntriesLegacyLocation() throws Exception { + legacyLocation = true; + testSomeInvalidHostEntries(); + assertDeprecatedLocationWarning(); + } + // sets up the config dir, writes to the unicast hosts file in the config dir, // and then runs the file-based unicast host provider to get the list of discovery nodes private List setupAndRunHostProvider(final List hostEntries) throws IOException { final Path homeDir = createTempDir(); final Settings settings = Settings.builder() - .put(Environment.PATH_HOME_SETTING.getKey(), homeDir) - .build(); - final Path configPath; + .put(Environment.PATH_HOME_SETTING.getKey(), homeDir) + .build(); if (randomBoolean()) { configPath = homeDir.resolve("config"); } else { configPath = createTempDir(); } - final Path discoveryFilePath = configPath.resolve("discovery-file"); + final Path discoveryFilePath = legacyLocation ? configPath.resolve("discovery-file") : configPath; Files.createDirectories(discoveryFilePath); final Path unicastHostsPath = discoveryFilePath.resolve(UNICAST_HOSTS_FILE); try (BufferedWriter writer = Files.newBufferedWriter(unicastHostsPath)) { writer.write(String.join("\n", hostEntries)); } - return new FileBasedUnicastHostsProvider( - new Environment(settings, configPath)).buildDynamicHosts((hosts, limitPortCounts) -> - UnicastZenPing.resolveHostsLists(executorService, logger, hosts, limitPortCounts, transportService, - TimeValue.timeValueSeconds(10))); + return new FileBasedUnicastHostsProvider(settings, configPath).buildDynamicHosts((hosts, limitPortCounts) -> + UnicastZenPing.resolveHostsLists(executorService, logger, hosts, limitPortCounts, transportService, + TimeValue.timeValueSeconds(10))); + } + + private void assertDeprecatedLocationWarning() { + assertWarnings("Found dynamic hosts list at [" + + configPath.resolve("discovery-file").resolve(UNICAST_HOSTS_FILE) + + "] but this path is deprecated. This list should be at [" + + configPath.resolve(UNICAST_HOSTS_FILE) + + "] instead. Support for the deprecated path will be removed in future."); } } From 214652d4af8188d4ba872626eeea3bcdff7096f0 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 30 Aug 2018 09:13:28 +0300 Subject: [PATCH 229/283] [TESTS] Pin MockWebServer to TLS1.2 (#33127) Ensure that the SSLConfigurationReloaderTests can run with JDK 11 by pinning the Server TLS version to TLS1.2. This can be revisited while tackling the effort to full support TLSv1.3 in https://github.com/elastic/elasticsearch/issues/32276 Resolves #32124 --- .../xpack/core/ssl/SSLConfigurationReloaderTests.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java index 3e36550e46f2..df25b2fa1261 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java @@ -78,7 +78,6 @@ public void cleanup() throws Exception { /** * Tests reloading a keystore that is used in the KeyManager of SSLContext */ - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32124") public void testReloadingKeyStore() throws Exception { assumeFalse("Can't run in a FIPS JVM", inFipsJvm()); final Path tempDir = createTempDir(); @@ -192,7 +191,6 @@ public void testPEMKeyConfigReloading() throws Exception { * Tests the reloading of SSLContext when the trust store is modified. The same store is used as a TrustStore (for the * reloadable SSLContext used in the HTTPClient) and as a KeyStore for the MockWebServer */ - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32124") public void testReloadingTrustStore() throws Exception { assumeFalse("Can't run in a FIPS JVM", inFipsJvm()); Path tempDir = createTempDir(); @@ -479,7 +477,9 @@ private static MockWebServer getSslServer(Path keyStorePath, String keyStorePass try (InputStream is = Files.newInputStream(keyStorePath)) { keyStore.load(is, keyStorePass.toCharArray()); } - final SSLContext sslContext = new SSLContextBuilder().loadKeyMaterial(keyStore, keyStorePass.toCharArray()) + // TODO Revisit TLS1.2 pinning when TLS1.3 is fully supported + // https://github.com/elastic/elasticsearch/issues/32276 + final SSLContext sslContext = new SSLContextBuilder().useProtocol("TLSv1.2").loadKeyMaterial(keyStore, keyStorePass.toCharArray()) .build(); MockWebServer server = new MockWebServer(sslContext, false); server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); @@ -493,7 +493,9 @@ private static MockWebServer getSslServer(Path keyPath, Path certPath, String pa keyStore.load(null, password.toCharArray()); keyStore.setKeyEntry("testnode_ec", PemUtils.readPrivateKey(keyPath, password::toCharArray), password.toCharArray(), CertParsingUtils.readCertificates(Collections.singletonList(certPath))); - final SSLContext sslContext = new SSLContextBuilder().loadKeyMaterial(keyStore, password.toCharArray()) + // TODO Revisit TLS1.2 pinning when TLS1.3 is fully supported + // https://github.com/elastic/elasticsearch/issues/32276 + final SSLContext sslContext = new SSLContextBuilder().useProtocol("TLSv1.2").loadKeyMaterial(keyStore, password.toCharArray()) .build(); MockWebServer server = new MockWebServer(sslContext, false); server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); From f097446066fa6ee9e09e7e86086194c9f6bdb0a3 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Thu, 30 Aug 2018 10:17:42 +0300 Subject: [PATCH 230/283] Fix/30904 cluster formation part2 (#32877) Gradle integration for the Cluster formation plugin with ref counting --- buildSrc/build.gradle | 9 ++ .../elasticsearch/GradleServicesAdapter.java | 68 +++++++++ .../elasticsearch/gradle/Distribution.java | 36 +++++ .../ClusterformationPlugin.java | 110 +++++++++++++ .../ElasticsearchConfiguration.java | 46 ++++++ .../clusterformation/ElasticsearchNode.java | 130 ++++++++++++++++ .../ClusterformationPluginIT.java | 144 ++++++++++++++++++ .../src/testKit/clusterformation/build.gradle | 41 +++++ 8 files changed, 584 insertions(+) create mode 100644 buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterformationPlugin.java create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchConfiguration.java create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchNode.java create mode 100644 buildSrc/src/test/java/org/elasticsearch/gradle/clusterformation/ClusterformationPluginIT.java create mode 100644 buildSrc/src/testKit/clusterformation/build.gradle diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 9918d54d7073..759edc3d36e8 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -23,6 +23,15 @@ plugins { id 'groovy' } +gradlePlugin { + plugins { + simplePlugin { + id = 'elasticsearch.clusterformation' + implementationClass = 'org.elasticsearch.gradle.clusterformation.ClusterformationPlugin' + } + } +} + group = 'org.elasticsearch.gradle' String minimumGradleVersion = file('src/main/resources/minimumGradleVersion').text.trim() diff --git a/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java b/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java new file mode 100644 index 000000000000..6d256ba04497 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java @@ -0,0 +1,68 @@ +/* + * 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; + +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.file.CopySpec; +import org.gradle.api.file.FileTree; +import org.gradle.api.tasks.WorkResult; +import org.gradle.process.ExecResult; +import org.gradle.process.JavaExecSpec; + +import java.io.File; + +/** + * Facilitate access to Gradle services without a direct dependency on Project. + * + * In a future release Gradle will offer service injection, this adapter plays that role until that time. + * It exposes the service methods that are part of the public API as the classes implementing them are not. + * Today service injection is not available for + * extensions. + * + * Everything exposed here must be thread safe. That is the very reason why project is not passed in directly. + */ +public class GradleServicesAdapter { + + public final Project project; + + public GradleServicesAdapter(Project project) { + this.project = project; + } + + public static GradleServicesAdapter getInstance(Project project) { + return new GradleServicesAdapter(project); + } + + public WorkResult copy(Action action) { + return project.copy(action); + } + + public WorkResult sync(Action action) { + return project.sync(action); + } + + public ExecResult javaexec(Action action) { + return project.javaexec(action); + } + + public FileTree zipTree(File zipPath) { + return project.zipTree(zipPath); + } +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java b/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java new file mode 100644 index 000000000000..c926e70b3f76 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java @@ -0,0 +1,36 @@ +/* + * 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.gradle; + +public enum Distribution { + + INTEG_TEST("integ-test-zip"), + ZIP("zip"), + ZIP_OSS("zip-oss"); + + private final String name; + + Distribution(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterformationPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterformationPlugin.java new file mode 100644 index 000000000000..779e7b61ed9c --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ClusterformationPlugin.java @@ -0,0 +1,110 @@ +/* + * 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.gradle.clusterformation; + +import groovy.lang.Closure; +import org.elasticsearch.GradleServicesAdapter; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.execution.TaskActionListener; +import org.gradle.api.execution.TaskExecutionListener; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.plugins.ExtraPropertiesExtension; +import org.gradle.api.tasks.TaskState; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ClusterformationPlugin implements Plugin { + + public static final String LIST_TASK_NAME = "listElasticSearchClusters"; + public static final String EXTENSION_NAME = "elasticSearchClusters"; + + private final Logger logger = Logging.getLogger(ClusterformationPlugin.class); + + @Override + public void apply(Project project) { + NamedDomainObjectContainer container = project.container( + ElasticsearchNode.class, + (name) -> new ElasticsearchNode(name, GradleServicesAdapter.getInstance(project)) + ); + project.getExtensions().add(EXTENSION_NAME, container); + + Task listTask = project.getTasks().create(LIST_TASK_NAME); + listTask.setGroup("ES cluster formation"); + listTask.setDescription("Lists all ES clusters configured for this project"); + listTask.doLast((Task task) -> + container.forEach((ElasticsearchConfiguration cluster) -> + logger.lifecycle(" * {}: {}", cluster.getName(), cluster.getDistribution()) + ) + ); + + Map> taskToCluster = new HashMap<>(); + + // register an extension for all current and future tasks, so that any task can declare that it wants to use a + // specific cluster. + project.getTasks().all((Task task) -> + task.getExtensions().findByType(ExtraPropertiesExtension.class) + .set( + "useCluster", + new Closure(this, this) { + public void doCall(ElasticsearchConfiguration conf) { + taskToCluster.computeIfAbsent(task, k -> new ArrayList<>()).add(conf); + } + }) + ); + + project.getGradle().getTaskGraph().whenReady(taskExecutionGraph -> + taskExecutionGraph.getAllTasks() + .forEach(task -> + taskToCluster.getOrDefault(task, Collections.emptyList()).forEach(ElasticsearchConfiguration::claim) + ) + ); + project.getGradle().addListener( + new TaskActionListener() { + @Override + public void beforeActions(Task task) { + // we only start the cluster before the actions, so we'll not start it if the task is up-to-date + taskToCluster.getOrDefault(task, new ArrayList<>()).forEach(ElasticsearchConfiguration::start); + } + @Override + public void afterActions(Task task) {} + } + ); + project.getGradle().addListener( + new TaskExecutionListener() { + @Override + public void afterExecute(Task task, TaskState state) { + // always un-claim the cluster, even if _this_ task is up-to-date, as others might not have been and caused the + // cluster to start. + taskToCluster.getOrDefault(task, new ArrayList<>()).forEach(ElasticsearchConfiguration::unClaimAndStop); + } + @Override + public void beforeExecute(Task task) {} + } + ); + } + +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchConfiguration.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchConfiguration.java new file mode 100644 index 000000000000..913d88e9fa11 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchConfiguration.java @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.gradle.clusterformation; + +import org.elasticsearch.gradle.Distribution; +import org.elasticsearch.gradle.Version; + +import java.util.concurrent.Future; + +public interface ElasticsearchConfiguration { + String getName(); + + Version getVersion(); + + void setVersion(Version version); + + default void setVersion(String version) { + setVersion(Version.fromString(version)); + } + + Distribution getDistribution(); + + void setDistribution(Distribution distribution); + + void claim(); + + Future start(); + + void unClaimAndStop(); +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchNode.java b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchNode.java new file mode 100644 index 000000000000..8b78fc2b627c --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/clusterformation/ElasticsearchNode.java @@ -0,0 +1,130 @@ +/* + * 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.gradle.clusterformation; + +import org.elasticsearch.GradleServicesAdapter; +import org.elasticsearch.gradle.Distribution; +import org.elasticsearch.gradle.Version; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; + +import java.util.Objects; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class ElasticsearchNode implements ElasticsearchConfiguration { + + private final String name; + private final GradleServicesAdapter services; + private final AtomicInteger noOfClaims = new AtomicInteger(); + private final AtomicBoolean started = new AtomicBoolean(false); + private final Logger logger = Logging.getLogger(ElasticsearchNode.class); + + private Distribution distribution; + private Version version; + + public ElasticsearchNode(String name, GradleServicesAdapter services) { + this.name = name; + this.services = services; + } + + @Override + public String getName() { + return name; + } + + @Override + public Version getVersion() { + return version; + } + + @Override + public void setVersion(Version version) { + checkNotRunning(); + this.version = version; + } + + @Override + public Distribution getDistribution() { + return distribution; + } + + @Override + public void setDistribution(Distribution distribution) { + checkNotRunning(); + this.distribution = distribution; + } + + @Override + public void claim() { + noOfClaims.incrementAndGet(); + } + + /** + * Start the cluster if not running. Does nothing if the cluster is already running. + * + * @return future of thread running in the background + */ + @Override + public Future start() { + if (started.getAndSet(true)) { + logger.lifecycle("Already started cluster: {}", name); + } else { + logger.lifecycle("Starting cluster: {}", name); + } + return null; + } + + /** + * Stops a running cluster if it's not claimed. Does nothing otherwise. + */ + @Override + public void unClaimAndStop() { + int decrementedClaims = noOfClaims.decrementAndGet(); + if (decrementedClaims > 0) { + logger.lifecycle("Not stopping {}, since cluster still has {} claim(s)", name, decrementedClaims); + return; + } + if (started.get() == false) { + logger.lifecycle("Asked to unClaimAndStop, but cluster was not running: {}", name); + return; + } + logger.lifecycle("Stopping {}, number of claims is {}", name, decrementedClaims); + } + + private void checkNotRunning() { + if (started.get()) { + throw new IllegalStateException("Configuration can not be altered while running "); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ElasticsearchNode that = (ElasticsearchNode) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/clusterformation/ClusterformationPluginIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/clusterformation/ClusterformationPluginIT.java new file mode 100644 index 000000000000..c690557537df --- /dev/null +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/clusterformation/ClusterformationPluginIT.java @@ -0,0 +1,144 @@ +/* + * 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.gradle.clusterformation; + +import org.elasticsearch.gradle.test.GradleIntegrationTestCase; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.gradle.testkit.runner.TaskOutcome; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ClusterformationPluginIT extends GradleIntegrationTestCase { + + public void testListClusters() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("clusterformation")) + .withArguments("listElasticSearchClusters", "-s") + .withPluginClasspath() + .build(); + + assertEquals(TaskOutcome.SUCCESS, result.task(":listElasticSearchClusters").getOutcome()); + assertOutputContains( + result.getOutput(), + " * myTestCluster:" + ); + + } + + public void testUseClusterByOne() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("clusterformation")) + .withArguments("user1", "-s") + .withPluginClasspath() + .build(); + + assertEquals(TaskOutcome.SUCCESS, result.task(":user1").getOutcome()); + assertOutputContains( + result.getOutput(), + "Starting cluster: myTestCluster", + "Stopping myTestCluster, number of claims is 0" + ); + } + + public void testUseClusterByOneWithDryRun() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("clusterformation")) + .withArguments("user1", "-s", "--dry-run") + .withPluginClasspath() + .build(); + + assertNull(result.task(":user1")); + assertOutputDoesNotContain( + result.getOutput(), + "Starting cluster: myTestCluster", + "Stopping myTestCluster, number of claims is 0" + ); + } + + public void testUseClusterByTwo() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("clusterformation")) + .withArguments("user1", "user2", "-s") + .withPluginClasspath() + .build(); + + assertEquals(TaskOutcome.SUCCESS, result.task(":user1").getOutcome()); + assertEquals(TaskOutcome.SUCCESS, result.task(":user2").getOutcome()); + assertOutputContains( + result.getOutput(), + "Starting cluster: myTestCluster", + "Not stopping myTestCluster, since cluster still has 1 claim(s)", + "Stopping myTestCluster, number of claims is 0" + ); + } + + public void testUseClusterByUpToDateTask() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("clusterformation")) + .withArguments("upToDate1", "upToDate2", "-s") + .withPluginClasspath() + .build(); + + assertEquals(TaskOutcome.UP_TO_DATE, result.task(":upToDate1").getOutcome()); + assertEquals(TaskOutcome.UP_TO_DATE, result.task(":upToDate2").getOutcome()); + assertOutputContains( + result.getOutput(), + "Not stopping myTestCluster, since cluster still has 1 claim(s)", + "cluster was not running: myTestCluster" + ); + assertOutputDoesNotContain(result.getOutput(), "Starting cluster: myTestCluster"); + } + + public void testUseClusterBySkippedTask() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("clusterformation")) + .withArguments("skipped1", "skipped2", "-s") + .withPluginClasspath() + .build(); + + assertEquals(TaskOutcome.SKIPPED, result.task(":skipped1").getOutcome()); + assertEquals(TaskOutcome.SKIPPED, result.task(":skipped2").getOutcome()); + assertOutputContains( + result.getOutput(), + "Not stopping myTestCluster, since cluster still has 1 claim(s)", + "cluster was not running: myTestCluster" + ); + assertOutputDoesNotContain(result.getOutput(), "Starting cluster: myTestCluster"); + } + + public void tetUseClusterBySkippedAndWorkingTask() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("clusterformation")) + .withArguments("skipped1", "user1", "-s") + .withPluginClasspath() + .build(); + + assertEquals(TaskOutcome.SKIPPED, result.task(":skipped1").getOutcome()); + assertEquals(TaskOutcome.SUCCESS, result.task(":user1").getOutcome()); + assertOutputContains( + result.getOutput(), + "> Task :user1", + "Starting cluster: myTestCluster", + "Stopping myTestCluster, number of claims is 0" + ); + } + +} diff --git a/buildSrc/src/testKit/clusterformation/build.gradle b/buildSrc/src/testKit/clusterformation/build.gradle new file mode 100644 index 000000000000..ae9dd8a2c335 --- /dev/null +++ b/buildSrc/src/testKit/clusterformation/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'elasticsearch.clusterformation' +} + +elasticSearchClusters { + myTestCluster { + distribution = 'ZIP' + } +} + +task user1 { + useCluster elasticSearchClusters.myTestCluster + doLast { + println "user1 executing" + } +} + +task user2 { + useCluster elasticSearchClusters.myTestCluster + doLast { + println "user2 executing" + } +} + +task upToDate1 { + useCluster elasticSearchClusters.myTestCluster +} + +task upToDate2 { + useCluster elasticSearchClusters.myTestCluster +} + +task skipped1 { + enabled = false + useCluster elasticSearchClusters.myTestCluster +} + +task skipped2 { + enabled = false + useCluster elasticSearchClusters.myTestCluster +} From 9c541b8f729038fa063f0a41bbc67f64da8352ea Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Thu, 30 Aug 2018 10:20:38 +0300 Subject: [PATCH 231/283] Upgrade to latest Gradle 4.10 (#32801) Upgrade to Gradle 4.10 --- buildSrc/build.gradle | 1 + .../src/main/resources/minimumGradleVersion | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 56172 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 759edc3d36e8..25d2a97302e9 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -17,6 +17,7 @@ * under the License. */ import java.nio.file.Files +import org.gradle.util.GradleVersion plugins { id 'java-gradle-plugin' diff --git a/buildSrc/src/main/resources/minimumGradleVersion b/buildSrc/src/main/resources/minimumGradleVersion index 899dd4f5927a..9add3349f9ea 100644 --- a/buildSrc/src/main/resources/minimumGradleVersion +++ b/buildSrc/src/main/resources/minimumGradleVersion @@ -1 +1 @@ -4.9 \ No newline at end of file +4.10 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0d4a9516871afd710a9d84d89e31ba77745607bd..28861d273a5d270fd8f65dd74570c17c9c507736 100644 GIT binary patch delta 49416 zcmY&;Q*hty^L83GwrwYkZQHip*!VQIZQD*7+qUhb$v0M;|1c>21nfT@+Hj_{9>$d45!cN1Z+|hhm6CW2B$%*5##x71DzUtpf+V16 zElzocPFpPR-eheu_0Co-aw!U1GS!|WRu+eS6=*i-bu&rxIBQLttSh~9sU3AMFX@2x zq%nmhrmaWTa$Ct!O~!1!WQ2cB>#PVPX^U&i__nHAv8JqMdRZztZj@P>Ije4h7stiA z5>XAUu`FWqh&*O#H0gDtLkPKb&n>np&pb;FZ;X7U)h8~S>JK>~s4pP{H^-r&Zn?4q zOe(8!5hij5_EHq0@P7%~wWj&SjBqI%W+?e`JBVb zG!Aj3G~GvDF6jYFTuy5-Rz>CI7@bBJzfB$-8?DU{ry^ryXiS0;mM{k;l5{|%a*d`z zSlS*6b34IVZGdrG^Da#Ld0zq7k=6_Umc|SF7M0VsB}8v6#1JBv`=mQsAEP@S`TH*# zBvQLWt%2~4q4~esejaalD^Xq=cc_A8vcB>qZq3tBUvPp01?euaWWVWuw=pb!s1I1Z z>JMPQCV_Q%$NGB5QcE(S)29J3^%#D!julEoQic^cZJLKyBj((htn@yDwizDk<#CPP z%Z3Lk*m}zkxB~w6q*b_i-1REBEhkZTD1^IuM2a-8SbLGny;eB?^4pV4K9b+lf9{%T z7&HZJbvFqAT_B8JGb-Ac7`>cbWmsZNvuV1vsfu4CJ|JsnYoFp|<}d`b=ENRg&+1%1 za%`Q0spPh^84OFPbI^`T>#%3*we>0cvt2OdxbJs|5KRm!rF*Au)Uyj0v3+Gb6my?j zE1S4hc^7rTRtvmGxHnIyRi`pvmqt~D*?wlMRfZ$4eYh}-2G?iN?9vq#oeuvs#<9oa za6sXys8k3woLd~_SIXc;h z$0DlfmT^PMmuViF*vMg@(T8lL`A(?1%4HbZ7bSd3bWF#)I?n@;?uh*$(`aNN3r(Tp zRCp!-r;M%RIQ!mK#HTir7AZ{8FO4?&E_va9H4^biJ?J^oX|ymz(v+NY{al#5Dj3xp z{Ofv{<}W9AL_p3U*x$Q|L%6_`64Z^k4&2oy>@JlUo(MP`sh|W%r&Mcc6qaCV6QR69 z8kh^Nxo`Y(6`^xLuvc6S(n^Pu`&{xLsHDeWfgpxM)BcZ^Qes=H4Lwog+v~x!d!&_8n@{A>qdp}NL8Wx>qI^~B-q?W= zUuS%19~TQkFOBs0moFW^!vi_QznpC8&o4@sA!Ss+1r!@pUzYx%4D6njpp$rW#i1Ju z{g%!igq3xG&*hJ_xkoAc!!5xlQ_`-ug&WRrD*PwtmUNKS5@!p<>}`_aj(7G5D9b@W zVt|2t|Nh^`6#>&EDj!L?2w6B^Ue3cisG7jPAeQ#%T?OCoblG< zSac0a!mq!Knvf_-!7PgoEq8`KqMM5ySdyTn(i9zqjDLY855La$`F1EsLa++4Sw)3j zf7lcJtZ=y4hxlKhYcA%!$?`DjfI(H$a=rBrDo8U^QTb$B^rdoIdE*(6aD?C#BlE-L z-gQZI;Fr*(7fc13tnN;Dr{^D@m)TBNE8ySzGeo~Xe_NvDKv-~4Xe!3fP3;j~xGP!) zyqC0C#O*fJa4VCgLBg=(W)iqZ*RT$)Z42pTbG07~%fZs!e+aMPpTcg#0SxD>rhy@Y zI`C#<81&zSZm?UL_fE<;7i|%4^ZUAV&}SMLup02*?er09b#g2BdQn_r2QQenZXx$Z z;{h15yZS-~1%nOiXg=ACAF3SG4Z@NLc7~1r@__vSiWZ@4^+u=kZwS@9nO2S(@<)wB ztYQZQWWhb;zEz}#&}Dz80a$fbRRZ%LN*uF^BR}?5f2&<)r0scxHaX3SzzXgh8SPW7 zkyya%At}5B@*EU~N+Fbfb;*Zq6KkmT-)9oCnTJP`#_Rr@EvO#(Cbl^(A!B;^Z757D zzrY$(yjB+E7I83va41w}S#CxlI;ERkU8R~MPSJg67)vU26w#A~3*0W^I+%w3!-F}E zCbuk;j<*c-5Y+Jz$=Ko-y@uyuF}G1b3MO_=)U*=+6-{rBK29p6Sf{#d{B|4B=ZZd- z;hJ3M9PbyPQ|J>+Vr<%IMLnkm_R zLasPMuE9B#p{a6D8Sj6__X_sZz7+`!ES?Yyj5OI9nh0Prvo~>d&DDW5##rY|d z&FheZQEn2d;74KOoYnScr$xc1#KGtH#ZF5s;fb#!@{KJ13Ty$_?d#sh?d@MBzmqQJ zZb;)~eFVVxdp-1gEVEYA%T?7|lx=_*M_;8TFg0xV?UOTLtp64orT-S{?&CEW>>mnH zl2uQaC8Rl%59tK+Z@GUB!B5~9@23u!$fq|L5ZI?awWmB|l=*rE_78ebeP*xf^cMWp z_frh)D*{lRxk@#EnxpE)?C;w*Lj25*1dxpx{{h9|mr9Jrhe8mq%%7f^y|G`g`v3@_ zl3bqlT$XXH$mBxkt_*prvn|{Bu1;P>tESLt>xUstCl7*g5=T&zSEECOg=~N@;w+6a zYQ&IfyY$Evup=vid_zSXhHbo+gK~qgmUdMKl72=kQ0wdw1X)r}^xso1H$70-8 z4Hzt|VL8@RTZNuyzLM8*oOHO{-b$r-DzpDlI*=l%mL+e)Svr!U>E2HmX3uV(OzH_o z(!hJ$n{0yCAY9*AbMp+dK5}@hnM(>_%{Y%R`Z%DB>x2`=J+wypg_roJ%|yKbAaqhc?|bw#4C$xw9_ z$;?Go4`s^{_ENK;##3Gw?&=)#&-Dr5#ryC<-8?-+aA+lUvf;voC3E<(i_7$9=$T|xr>WO4DlfHF&Qr!@f&_#zowL_8 z^Onr@TH-6Hb42`G4)Ip4Ea6S|P}Ns#XQPDmt$p=uQ9PF$A7WS*D;^TO5Aqho5!AjLxm zRgX$?8&|zZ_1C@lBs#-HwJY~YY&k#PPUBdIKGw7>vYkiTDQ$OU@7*8)jXP1 z53y6v>PrBF-v*iQ?@#px+q4C)X-)=4bdHi((fQXK4_038twwy=cZWL8$>6|&&x~HJ z)teY(>Im}ko04XWhuzl##n7!wxe)Ej;W|R+?rEy_Es!KS`{g}2?;37gqMn^)>|_FPs`*L2$4NF52h8!Y2psVV!Zk2=9jc zRg1-79)w(G+IIGM7VB(hlaFCn7mK}?U={bU9T_4Q8YD4xF^fiS2^l@yxXwIc%UN0KDn85}3eHR{bYuD8qsU{T#w*b+7Whzn=v zkk-9ZPafofza2Jf*{?W@ZI7YfS5Crj+DRhx(wt{SRxVRs$f-< z?G+iOe8^=t#dgC0TqOOJ^Eqyy>~RUvv6jgfN8quQ=_6=9?m)YC_Qn@L(GWf;8x^n` zPxvOMI4&b%AlQz42BC;DsYAkVLOle9ua}~~O~n|tZ8bsLxt9RL;H_*w_=~dKVm^tK z$0o8k<|k%YCgqUVL=x&s%p|H@QI)48ht4smpAPTXj??(=%yjY1qB zq$#hkkp%ph_pfx++FZ{vKAGhlo|Lw-x%g4+opYIfej`3{v<)-q}m{441sJVj0 zx{a*{ik9+P*Eu`}jYQtc%E zPIz$CFv3OJt7N;Ra=0EH@{+O1A+0Q!pG_mGYsWv+<>uO!<;-c%B9EfUXoxIOCl07c6($V)3lv|Rdh~&f-^i=gw`lUD zIH3vAlxdx7l^S(Z;QE(lYVk+@47pw50@yECxY2e=t8N-;nk+kQS8uhtHPIOKd9zc# zs8o^2bQ`+P^46m|j5elbPiN56UFw}w+c;YN5)wVZp~*kagX)sK=HanYI-qqW!kg;d z!@I$W&6mq5y}G3}8B3;=*SoYSLRb~Ns5JV`KwDbqlgBwtOzwR=*S&kiclwYT1@vZO zM*-XiaT~h~n6lbcA#gf$H3KNWNuBn`Tyh3bYKU>bq1q4{G!zR%aUA1&tbd^6AYTv| zsgi%eXm*)7TwsXybd~rE?xZn<0ZiiI)faXbzTx>HlJ0S+v$Gon=fjU8>}mX1ucK^t z=XVsab;8Fw@CZ?kPhBst*vR2wVVLShS$><$!RS~sLSC<-^@Jq5mc7StJn*VJwax+0=V=zi>TqP z*enw`TH9w02H_I2o&7xQ^la+W$;Y3ypSWrP6AF^cS_A*235em_o9T^b^S|z+9;@-A z3>N(>+mNMH-WD}_RiR*JlKll81i_`uB|I8k*3dzHtYjvWU0=v@oIR+T<)@R4dJiQ= zN*;ScYJ^9>Nr$z1^kA7?2}riv!>a$Rt=iTltooj5Q$w(>s`E@ST4$z#S1lAYk}tvC zK^3d#XnZ5=w1ctQ*InY{d_w7K_mo8Uw^moR^?Q4)M!-HSJEtGg$-p~1C&60s>?+;t zt0P&3U~Du*dYZaWJhV`jnC08yBs*SyWFuxYYq( zwjSSBpk@E$%x%6WY|W-=4aE3&rsJ_3SF?Xd-;sR8BMX3mFsFvNG352Z&TYcqnxE|> zJRdfEG$kfmk2mA)XSg;WB{Yv9G^KH0q7~?vR{kwoRqhNqx5&{addZyh&VjIB%^g^O zM9~|l_Dc1q90TH&6FA%LmpM=qp>yifZfO13QZ+kW9Nj+R`~2p7Rr8e#Rl5sO|Dk_X5XK}*r0G4poNr@XB%FjR0V^TEOq>N z;cNNu@yeY%0i}oPI|1zO^2F{%>n?CPm^)5rCpD)J#A}?4!P)&Dn`>JVgxgcDSP#on z1S|_D7DXO3_7!f#3sYJ#ifN~9k?HJ#^IUI)jm)ux+*b+S3`x`TucXltc)a?lU13cB z7Ml8DT{KW(exOWba%Db!x=mah`?DJt)Js_-b4TANAVDBq++0rUr5ua=#!W<1FQygJ z?9gjTD&mPJ;DAYLR1s9Y191{NFl6(XRB(n#&^cL)+WK@}Q%jnkLgx6eBm5~Q{^CcB<@OOTF2U}XgD)AjpnZe1SBx|V#c`Hc z3@NW+y>pDB6K4;Ns!-nO3c9=H9j|1Dlap^8tSW`xbY8@i*Km1@i@WtDh=ToXGuVF0 z{&YcY@2|n-b_zGfmcN);_m9fSSo^sn5njcvaY+I5e)|}N<@fA$oSDQcoScavc_My5 zgeSiwgxJjF0mCjho()-WfbPJib}KMOSZC*xJ1)J|ieYq`UbmfubRS-~9pxG2T-_hV zlE^=@2QIG}s&6tcKc%mXXK%QIsc7=Yv>jM0)iMw%iTX@<>{lGg&HvXYI?7MYr z!;HXhFP*xmPC#eLEon}J~N>NIA+nmb(>^!pY=q%i| zSZeM}2^sk;596Gm$J!8JWox^bHoxz+;$iy>?jTmD22vQyXTPb!um;s8xmEwumJmLM zQ>*3C-W4dHQ+vzTMdcrLln zxzwkY{HqhK%UWE=m7EEhE@EVR$kSnxV|IOX8K~2&k#kvZIxz+cN$68qmRPJxq@<24 zg;8+iL!GsMN`juk%c~qWGJ~!#s`97_h1kqD+nUz%4N;#WFQS|F=`HSEHtw--4!;{T z?F{DhKPM~{?he*ma4*(DLgS3&$foFD5TZA4!eeIY6lNKGjS&Z0S}?0ObvF8M-=Akz z>tEiowh0Hhr&#QfHAIwma~x#^tQ03U21Z`L+Y-EMKOqRn zxR!FUE#{Nxk1=d%7TC&`>dekix|--BZTyrFRB0`7WGe>@+8rRVNm;+|rimWWIL$%x zU(-ORcvc%08on5KO4{Tntu2IC^FEvO#Ti~>&udARAMw4`_4nBEfuvWOq8FDy0)9qg z$anmpWh?Acw@+o%_d9Yzp;36=fTA>P5V(l5-dJT-@ummQEjUcrc9j7;!5SA_9=pFy zibASX@j3vOx!VBFS(edJyqpnH2z1RrV<8~~I?z*+r8o*e0)_UdmbGvL7+horzq3JEaa@F}z zD0}F^D*iB0AZ?Pyr+U#!E=fMsp>ueWrtVX0L!?E@CqYHl`p>6FoY38t>g}gA+Iq8z zP@Sr{<54M(D6ReP@}NLl4L>5i+Q}%LRmE_aa{4-hrxTf_e9ZOViDt{`Bxf#2UHA(- z7C?$kZ#xQ=`x#;9RdJV@#%k%r=P+GciUp^Rt+>u!-sBD=p9+$Kuft-x5mgqogN^1f zo!DDw6tkRsjTvv9?RTUy_oL7N$Oo7mzZIcjY;W`=X;Q>MqC7I`0yPD+KYS}c4%jBU z1kF2$_HCm06)qtP&UnS=+K^E$HvWCf+69n9h>?bi^t(F$NWwoGqd(uvLH`hC;g3=Z zF+vH%MZHcb{QifnmVmg5zQHZFvG|}TUY7yGkq9`* zw0+_RN3E9jywV{tnO>iU>)3m8Ia4F?Jly^q2kg;tJ?{-O1=^Fz1fkNDXay?rdTj}_f(+Z-&GJ_Dbo`-; z1ESx)U<5I4rcJumYw~EKBBWu?{DDguB#rtrZF&^AE?5YtkfE9>9?fC%h3I3NW*^Me zYaPuq@6|I(g5&wW>{jMjj>W`%17Nl(y2B)lL?hwcVE|b;U;_Cy>dauGF7701%`L+m z7s+%y%o!7r%gywB$28Lg!HsKt%z&yks-WwIfLFU}O)ZD#Jl~J#`L~582-vZ5is;x- z$jzs$g{55(QG#C9V)>^C#Gyz?d}4}u%L*h$D`1#7J_)eES+m6qFl!MeCB>NY`y~S0(%##lg&ig^^ z=rwdb{&9I2hIqdq!SxweGeqVu-1TyQbM=!9BKkz%^!aI8-_I8j*Q?}`663(Wg;8-O z%3!@gGn)_)$7Qtt#43cLLL~5n`oA|W_jv-+2OJD6EIFT; z6WCJR5=Ie#C-sQwTxJ)B!;saJUPQyTmSe_<4Eh%7qynCJkCADPi7*;R5FQnv!HiZw z_SA^{x2)>22Aw95-&l^Vx%kL+w#F0q`U^tuN5H~(JdBJfhokyIOIk+~5)xCQEfie< zGe;>;8}VIP*4$teKCzr~S~|l5N1sy@3-Dax3tgE$blIf2wv3fGaj+XQo?nDHZLHiX zQ(JBR@A90yAdqu8hu%!Dwk2*%V6VpVi>)PHsrxU1T+0lOh5>j-`_s$$yY3V1f8b)i z{E24Y89OQ83#c7<{F*&l#(D@{=S~O1_1JH6E_6(HtiwM2t0xnry9{-heJv+Wfp_Y; z8ugCydg!Sgx|h{$@;r~7A>Gg#&-CnfgdUK%zxdRO7DG0A8U0oE$Pi*P+PJjW1j!us zO_&Ph)oM2xwsx2{eP+tO+Cu!dqWY^++Ysp+bn!uY%zBow0>gC7J1)q+PBpq+Q^gSh zYXk@`U}P-IBE4e_rL#y2gv1^M)RbQi(w%|=P446P#F73*+uso0740ooU2(Q6RtXB3 zo-?@q_YH~M5HiV#*c0TA)U=}Agp;Nwm3Q0bDHH8*mqk$XEV93jT!+}}Y4H`>6~&J~ z|M|E`Xe%Xe|3ic2QH6?s#o=)jrKb%EJt=Ze);WQf*Fv(B+#C9tE-uhRdG?)RMDcMJ0nx-FZbq=2RiHlL2e4Qh@@|z~7=!4Ik z1+R-!S7-!eygl{>_TOCM{C|EQ)xj9n8ypx|Hx3vW-G7D&RQ-eBcV>|kVrb9_079yZN%E2rD z_${E*Pnd~V-C1as&P#Y8*w9ZpNU+8fRjD85RYwsSi^97En4)DXqX zUv#LFlk6Bjabo83)`6XWcbH7q{hG23=nfpw2?ozqd4cDv0U&Qbkr8YVC6tlo9VTY> zod%}S&|1%HSjk%%_MYmU!`nNd_iG9NXPxRy8AG2@dR{fI*P`;DGgm_IBzIpDs$t|oC`*`nqCfZzB zTXL^(d=d5pjOu?eyQvEm!W8@1Q>;6VkT)iB22LWer8PWea8yVohY1%%bT``-^zbdO zWS*$yN^UgQciEA^8|1(+lF@CUMM~7vf>S;r27~7zxn?X{hLf=rbMbhrMRi&^?naHFk-7farm>k|_LT!#ZU&mMivR}17Ucqh0=YQ_P7 z2TYS4E`0n#KC2AkpqAOsa((m*CI=!i1{@MA29~No9TQ^orW6$7!jai!E4ZXTH#&%v zaQt)I2z8G{Jq*!j(hJLS zQiJjsr$yGtzC}a;5KMFPU8`(JWGUArCSrOF>~DJiv_8HyH`b7fDF!WqKZXrz&nnm@ z)0-*!qh@hUrldq!ERkAj5BU3bc~j9?$q)ezUvATjN*1w+l&g*x2WA5A9l0|e(XVVe zrJ=}pppcm^D)P<(VI}VGimA#29f?1ZK@AIeByolDu+vkv+l2T^x=0x%n+N6;yxnWx z@e`ftqb-~BSCs~g*1gsPF+WokB#j;Zj<{BiOKHtUoQp;lKW}OBNO1I3ROyunBd!7a zx`^Z*IztK_DnlyU<7w`Vwo7+hpSgMTk4!^jdj#PvJTlX#Dwc6!zkiy+4*CwF!-wz^Nmjxw04jLSSGjUhZ$6gVB=|468dow%R%~x zSbx;TGpR5RFtyJQdw*_p7prKNPMHAZ-mF+0PbQpjXU8RB*hic`Dk*1OjAD&??UGyQ zux2rlcM{?TRml@sO2P9xuv>e2)(sIUXcu&f7}vFJ<_sj#9cqqKplNDH!p zh?6XBQl`QsCgqOD7q{MPh0&Vvj6{JH+ZuCRJJZzVHP;YJThrDG^!3fcG1D?k9M1DN z7AjE^f)(^gXCF?d;TV|3OK7hi$hhH|M&S&u0}}xg$V*MiOWpg$mfJfOa(kO1Lw4!N z*evU$!%vZ}hU_Cni?cWDSZl!2+IQwxEShvNB&$L&9fjFYit5qo3&#RkP*#V!K2<{^ z|6(mQO0%yL3CqtAaw)P=ClWM1+Vt7 z^^u6CoHpx3v^dUmb?euGvcc47A`*sw_|wU1Xz_O0vCV|@5M~6Djn6+ z3I9ej7ENWNQ&nA^y&uWZJ=5~pKAl&u&fl>7Gx3kW32T{Wx=q!UTE?W3%p|9OXV~iW zx}0*O;mVBK990#?8Q8~ktIFL}T9&PL#1ZnCd-NTba++#)w*Lv5-qGTZpt<0Y*{{Og zX5rCaSKq>edv=kQ)4c(jOx`cyoo%k8qT}h*(=59CoLrpncHWB;%_N(ZdM%)|%o^8r z!Z$lZn_#>C1jX$9NuJI<@X7KNSO>=#Y6~hOcAG-hLY+-L@j3MPS zbQiTdpdL9XQkBKy1;TCS4!+)AIjZ5KK0Vp!>9#_w)=D2L$KnD;i+|@>9l{dXSLLhV z>_GSb&P-hV+*q1N@;>9{T;z>z|IQ~|e&L5*JFc!5)EF&Qvos(|5M|(Ff-r`BPd}Tv z_4-YF*!?}_6D#L7rY9yxbOV=Sbn}|?|tb(1?@+`QRou$-dHHMmP)WCj+e6q6N?ti z!iS!IYB$d9HAlOTWyWVyvEAa=?E&Zh&$$msq|DRCV{QS!5GXJ~nQFw+`{uPjM)EAS zZ~MoxFz2H%r)TnSV2(fK3ykZtMhVJ&bJ&%82;>`vUmB;PFu!pz!J)1tMEapmT~;s{ zVfY(^6wc5vStGZYGiHbYXQWPHnM}%~6X;Q~(9Ig$qL!$S8p@(?PwoACw-{yb{Xf!p zr5mL}GAIWCPoH)`1Fai?YnI_DZ(TN`vpIVgh^2kiouX3Nsd~i}ohnlnOYx?iRw`Vb zONOd7>}&T%3+U8Y*X^}Li`LcX+vE-%5&L&f%W!b1#~W#Z{$1YWEn$x@5EH$_9FBg% zMgy+8{AL%Hcum9Y!B$sS%9Eo2D^@#*s%8xDS{NR9p109DVtuzHL=aUnA^$l*wH>9o z9mvQXOz(tr({?*b_=R#CLD(Js#=;{Dq?v)E_D=lbAD^34Nl~8l=MJN3eC{8{F@1b{ z`m{=t!{s3ub{;NN*5s2~RPmy95D?l(I`#H`yy_Vc2gH=;)x`itr5FCOEe$-jL|Gg(;2u2wP)SPhOmL_ru%_ zH4+SxA3WPgx_-MhRS!PHj~|vXL_f0|*48?~*}d95x4s6^>hlIp!EHPpH&o9w9ku&r z?O+&CKke$ow$j$fnfSxutFCuk8LEesHP`?ZHsINFDw5UpjG>G##(|LUb$9$hwPKX9 zf0H|0z~^|;9FKR=|VkOA4gYTxi`Dl`GfnuY#G)Alr*5y+NZxB|CrbL_@LRr{ag>SLWI_Qjcna=BNNxNaO7kP~@m4i12r_yg@-@NWdyH&xJ%8wqXYr_v|Ig==1}#Y^w7;zJvMw$= zMY1vs<}3 z=nb12Mt9$rEs0?&O6HEI*KybPW#_5uML^)=mO2lz0TnP1b&*e!kztT2qiCdbHU-v0 zf}*&_P(rJVaKsRxctFQmv{6Sc{G?invM#bozW;gO2;SWsVeJPuD%Aq(gh-3b63rd1D$WgCsDG(5!{(^bFFH7ixzr4>*@tA?sv` zm*THzmt-d1JOVW*k~yMxe_g=o8{~GLichc+`cAjnhT9Lx#l|As&$CXXIbRoB)*W9a zDHZVDbablEt~945@*Z4<2K^VL2yX1lOFcV^wUvB$ADz*Ylv4vI3}lsS2V5&vSl4-o zBD}nt*INyZHP{$~JrT`+T#6kHtyO0rc;dj{7+1Ji#>H+D&EVw6~j#TdX{xYcK0bjmU+v zVu>%=#=W1@z$i6$zi(1hUO~(Ojki$jtw`!Xz{Nlo)7!E}tC*~%d&?U~-C!CZuZUUY zd|}VsWY(M9MN{Okyu_9)<})BP_^tu6z%)_;X`~@2)W4~ygE)MQ0K}C127mOim>72R ziHgi-_xfYii@Z<-cT1Gd%%m&4w)9J@^U_5s%@N7mq->U;2%FT9Cs|HY)B4=T9I`}K zb1!e|>cg0@`XhWij{^=oPP6E++N5s0E%rx~t}w#&N)xVL8J(vpc0c%f^o};v!1gqG zdbwk&&)uXLDNgTM3u=Cv@4_`n8JX!Q=Azxd!kMi-WUq+=;O#C%7vRB({4I$Qrk zBnJT}?LdZfm>lA&qnzf?nwQ~!_FO>l9;-K=;qWc{e|(Pw0MPl-r2i^TOnPj_t?4w~ zvcTs;s+ONQq4a<+po%f`dJsc>sr-JX{?r*VDuE4V)vm^(rA@K0D?Cr5r8cSZrT36^BkO^yVw|SbySpk(xnvy_R#c1XA;eH|aOEvYu z#DCJdCb3f!V6fP#dCe0~&TBWW1Q#WW@lkA`a55{>nRBqutF?44h(g}eZ_9|#LAchJ z<$ICGAgn&d{cvZV>p#zoQ8{L8%zF`kkNN=6xlT=6n<5)JcgrVxxlXg}mm}ZSq&$yi za3^HX@m*JcCaiKQnXPP=aoKH7oP0LS%88Y#Fs6k8uau!IqiI9e4325*!LC$}DO9%| zF3B*}s9OP%@7dAT?0O&Fg`_19YUDRjXg%WB-@=iTp^at6)n|4e`(}P}ci)voWwkCE zB%c2a2(9w&;kM5)cc*I;Qb+8Nz?*lagr0wIP1o5IkytkBb)O65`P6XOU~uSsV70G= z892cJ95e>7DvdwB{=z*-EEJMXF1oruH9bWI(`4(Sl@!(Th=Lrd-Vc0#%ZF8ay(To8%f-q4H_dAK1-&KA(J ztVDSt+_>@6BuV_pPb@BN1^MiQ5B)v}3AX=$7rg6{n*>LpU8UoqM`aunT7zyc)CI8# zS8H0c35j8(Tq(OBOOw`D9Z@AQrDtzVhqe_l7E9Vabun=^HTyvc(F3C^%^j!qPEAK9 z6_dC$g}FKO&Dw(9ws^_WmpUp-OXb6nV&(y3mGCMN zOU1-7mXxS&6ZP;EPf}nW6M2qIqVWnCJ344aDKd(QVEGrK<|n_deeor%fReoxF~g4P z9b!F=7U^vYVo!WUY4nPd_6yVY%forYSLzM<)`;gjqPtC%)M4;=gv1i`Hzfz_2nP+> zRGUA_|Ij%mqMU^;Z|EBc9O-?XZ<4sIoc0RI)Tpq;eff9j`6r)e_>Y($l-r?z|FF0A zVMEYDh!>=1lG1(zcO+^ITD^`+|F=!TB89JhoD=%xHiV;N_@gJx92np5imM-5KSduY zOEk7u+Vr{+lq$IIS<|Y$f}^JR9tA;LH$Di<=72xT zq^ffewZg$tf}1Y{KSUf)c6NJ0ZF1Q98I@>9{@M2h{=bL%!Q#oD>AwKJIXxH{$^TQ} z{){04xjHb$>L-L>U#>?6aWJAJ{VYo6=4hN~7>K0O?2r@)5E_0mhDpKrpg)8Zw$-iL zH3q913%r+uA{y#wKfqT5@waT#TXn5!yISj_%KE-u{xGE(4oI23y@P5LQY@3wAGnRO zz?rdK64UFBO>L;`^bbo(`6Zr}R=l#p%YD6}=a#+F(k1WgnTe-d&CArYI`N3lN*n!R z(&Nv(4e5^1j~~QsCQCB+;-|a=M=^;ED?l<$W{K%k&ZX1&*9U4P^=K~GQ@X9`VIVXB znK|{}vc2IbhoRvteK$_+vR7)lB?R`WVse>g)(W^utnh1GxKes18@pjOD3gejU!NNN z$&fb@zl5b{dt3pJ%Z?!7fR|b0-iMj-uwaywnGx$en)~`7!mmY|&FHAPTYX3+*33|s zwCAK$!!IWMXwfmjvNxT}r8<0z->(fo@m1u^pW4Gr>peSoC=oKEdFvg;_L>O6S@eLXoT5)xuQ$+b^FvirT;T=Gf>qGi z2NfteG}%#pW=$>k@uS7*NUrPJKpvHaGn=`MY<0xnrhEjkg%v@V~iiM5^87l!h}m^)IJS(F*FS5 z<9yGuczUvAXj}h|AJvv5k3t6^`GD$Xv}8fdZ&$=*NN^31RJ^Q8DGaeG;VFmaKlsXUj@s5>7Y-;gD+sH?C@W~ zyT2&?;crm>Sc2O+LN_zQVE=c8=#cj z+A_2B=f`W#CY|KA!S$EEo1~ILdm3~%s>lo2&G>og z8Q`}#YD24WCTwE-K)e-O@pG5OFfhY?y28e1oSa2rM%m)%RXsAYb%QD52B)g2n`oIF zM5LdwE@)&lD3~Hc#o0_T6g{U87JTMl*a5@JX#DT_BdgWTq$mK?9K;Q}HgcLeRLYOA z=f~)_v=YZx-y4BC>Nf9jNv(rd=*vD`A3P`t)vZ2_|pZn16)}m9_tk&hjbgiApE>9vj=RK zLne?7lUZ#0QQ3XsCoqxo34gJVTDmp6-E>ShU^>XrO2ZOM$)POKnM(u_v?NE79;(TZ z$Ss~yD#8I3zcAp37Jq4k9FekNo)bnfSOwE=Nb>RFa)&>S)X^-z+NSW(*s%9X9vKa^ zU6C_7*pSBBxEPKqr)NrU5s?aoH*rfTn_qCG#EdCrvW8aDnL*2!O_3w%>eQ;0WYW#F z{8U$i(l|BG7EfjIFS9M7D6j!P?;wXsF%+XTQ4j*yYsA6|C~9!Qt6!y!YySxHt0Cq4 z;DpDse_FOG-j1@$HB1Ybx|j1Qp)FsNpK2nlRa7|g{mNVC(rLSRWVl(o5PO#{7iiHc z2Nt(@1!Utd9{qm{mp`2QH2@Jy&?6YGnBY~KdaMG*Nn-6Ne!IcdgXGue?+wD zg(GPpUJ0J{1nuzd4Q;virr(N{Oy~z$`}NK8br_r>?N&Z81qf#;<`QEd)h&8+^+d`q z_M>0P`VW`sCe>m69D5`TWmU9Otwo(XhoK|UwU_MPO)mlLN%K>oo| z7XK`ClnR_hejw*Hkea_O{>+&DAi?~p38hnDIAR8rEaXRqNkYKca{GvSiFROdW;p{& zcsR@$U6z!a_$Ml_s@&lH1Ngy}L%fAScK_dzr2gs~CdS^KQ?9e_!=M|bz zviWS8or^fkKlv-YyJOPKfXA^(;c@v%&{O$v`})HBB_cT|X7w1R-PF#6qUcqSF$XWb z3PT^{lVQ-5;<>^VPDZ0n&}=ok{1)$*Ec}){3!L^9XfC|fo5qN-tyy)vS^?vA&snp~ z_j{15a*LlmFh-T{3TNqWROWt(MPB{%6Al7bKXSiFw?_S3Y13jz1w=)IKtcD?{Obk! z*iW?jWI6@!dcWz_0W%8S%XBAD)oA_2C$CTle>b}|9t#Uu-*(2xL4it#T7e4gx+J-M zWc5g+R1EzA5- zzO2a$jILdN@;FNE^LzD6fwFT@0m)LXn{mfw83<)rdA0w#j~&Rbcfki0pT~ z)nkIUF3Ti4g<9l#@b8nEg>lJ`!H7YFs*~7JkHc#Ik?n?c$Fa&-4UhjvvYN49rxo-LVG@ujP6Y?cY~1wUjCeIbAd5MA{QO zv{Nm*$!P6F@WNPt!(NNjlwfX_5Tbex4?~#Xzx@S=B>Kom<(6Iq<3hvLvFL4UB+spu z0Rfa1(e3OwY3eE&4GOb633rfIhP~Ze2$#5YD_a&q^i35Xu+&`jti?AS&Q~@~S*Z%} zOo^HL=2EArFFW}@Pqe(;TA6LWHPT5)j40LLTeUmPf8%Qay2@Jlg8i$HsrHihu574e z@x_h(8;2G162!Y|SuWXDq-|q9A~HqE)t&z2?S$<$Ojxj^jB5IYR<;ge0^e@kNF+-8 z7#N^9cCZfzaLY*J{xg{;yHYpsl;K)2W0*13noky_>{SFRj$gcIF0dmQrPI7g2CRSm ziQ{?n#ZyKAAeBxexzQxQ;k0DpWu+fbiiGlJxT?#r8YJu}(QQhS8mM>5yB-OJHlcQ3ovV?IO(D-BUU#9dm%(fI?>OC)pn>~f} z|G0Xm;LN%&+B@vHW81cE+qTUoPM+9)qE5%|*d2Ck+qR7kI$yq1=iZR&nTprtQ8YZVGD}H59W}yl;ixf&O)1kX=)s9lg z@3=OV*bXJ*1M28|_3u3AfX#0+$CITLIm<-T>M)HQ+5s$NYHp4aot&pOb|wB~Zpn#L z1@6(O9WP^r*QyCXgEH2s*Y>X?C>UYRB?1y<2zHc0HWvm?FMbQuDRTwKQ zr_>MXWSnSOI`Lr@44i1~1eqVW9|Y`TlS+3f5rIag{DjRwoXMWEkT+YfZ=rHILe~6; zsqMx124_S5z`PDn!~)jKhLEdG^pseXP4@NlDS;(;mX?gy9~Po-CTpj>8@j`EVih2|0{#(hwIDYBKgLE z)k}^kAIQghTgbAedDcq*EQNUE=Fxurj;FUK&nyz4nJY~_L$RohGR&&=jA|2BsVkL+ z&5TY(k*o%?DCbQ50+cJ%35A~YxQ(DE)GcOVBaN72CVyg~Tv)t`iqbY1spb4A2PjaH zJ4HHEDf&^Y9ZoF7erI9Q10l>E!!cjqmi}b40B17-U8x6l?$QHhfDc*cdSE0zkjSxU zTSO7)1+K)})?PPRJ-dmwSv2bze`OheMUHpRiPR@j^!67_alvL7d!aj0Y3&!eskt&_ zrj9iLTw}L_X-g6v_l#gm7G&psm)$|zy`-wq2--mj_p8?nb8!nWYioUOFvnJ$Dh!+C zop*2|U(>2v{l+g0gXP7P*3aD$P{c=>ZD$FTuae@gXGWmLo@Md{>pLB&s)3?rM>dlh zNK({=vFViR7=AEuMZQ$5dx5f`k3{gXv-??FD|Va`%y}(1aEJb zlbLCxeoj~O3-FfRr~B*ND2o(}cfJi|<*58)lBZ+|A;<;&F;p#*B=-eHJ|Vm|8`c@9 zE22&J{TTdjef}G%%YNS!?}I*{wyRhzM5%^ef%`@a4g(@Q|8~E!E%$@@99k?_uolI2kzH} z`wcdWRM;DPX@ySJ@v30IgZe8An(b+T$mL~VmjrRjL9*0El0*i_REM|XX4QV&X^VVrN8*z}-a{2O zkFNaEP`V47c9hvK!NnGV@uFC=IY&d6ew*5BhL%u+($$)EK;N(;`aOLi-S=shB`>{k z;RzzItTac-Fr0>R*HZUU3(qdpFu_#VueAY>Dl<{18ZbxK!^c?GlA{$nSYThqdHF!7 zJHbmBdbz`hZ3RGGEdR%*m&^j}T;#yGWJLL_Mp<=~G5u{moWns_Uw|=dDm3sr=1PiS zEkOprTsZN;^9e@MyzdVa@(LN$g1+mAy|5a6^G*`z*`NDFH`vl6qunc;SF|l!Xib*< zk^2wKVUi;&H&WI&$QHLt1Q-@0e~R&n{(+0vhU51s1{~bLw0kIIyA1%+HeVNY(>5bZ z1^LSU@Mb!B^BWtWAK5>JFj7|Ae(4r3%HZ^sECSj*(9MG5g|;z`&Yu2^I9(DW~b~Y@e;0c@eV-N1MIkEJ%fDUQ-F9;dBL%Q2!M_cG+Qc-b!>hHx-}h&bO7Lo{Q_X`2f{gDq2W3)n=)u6dCdwC|P&`i_C zab@jM2+@ta544!`c;Ww#bxoOcb6Vy016lJrBWW@?l@-E94h@|LAsneP84?ZTFQkRZ z^*!^9#j^sa;F@vAc;G7W_V!(c$qQX@rk}}^I;y6JHn~YlB}(Axl*w@ueo#g-WP1xD zdWFK~LY78+-$7HY_>LyXug$p6ReoliDPv$~A|K!vii8&lxXRnDd%O9Lyiz*H!}jlpUUt&h)lG~@U&QmS!6inVo)HH ztV+hgxV6OMAS0`y3K{OT32+qgHL?}`wE`IBlq%JynF~5QKw>$GmgZ za1%J?=jV^zrarxxPOpW9f#5dzxYCHf&00g&Sv6GRiz3=Nyo%fCCtDyidlNzoBThzF zP?#Y3O?14ibRyoeN!5V7wFpl7uUzKaQ_{|k3onVb0vzpyA2R~QEy>nS!bpiRhG$?G z9DYM;7LjN0H~e%Y^ZtcNS1saPI=gNb<#kzK6fG!@m6rKnr$0oIEJhv;l>&c7=}L@@ zA)v5_>L!bEqmxBFCqSuWvW%NP^-_zR7%l}@V~~n=Xpn54d3zm5&}Jrr{gheg29iV6 z4-hAJwPguGE>8kpKo4_F7Hs^$3iVy}Kf>^0zW!&R5V&^cu1zz)jRMJ;wPm@g^r~Li zq-2`xkmczII}15rFWh4{cnup+U+~ZO$5*9dn_Q}Xf*B=l}5WJVhi>`UnoFT^~St9D%OW*Z8&KQo2E z&2+whRCn{6O47NDXuoie9V;~s`NKBHZf-?qdmSD#cdjgvWHYNOG>8Dxc50s}h!kOq zRZJ=71!<#ilFK-h3aeW}>&q2OlbY#hA^jX_zRai$ljOz`disA8!?v-wEdM!=)|6@J z2DEpOP2XxM4vjunRi59m!of&zAu@>oQTM?i7@%?bZNP|&gwo-DGCz+vRi#N3Prle! zQ=an{sIXQtAe;B2ec}>*vO|XrK}r{}LZ!+L6<*#@-NsDAO$GJ^k3V}n{DO&b+Lf>u z#3+0u+OV(T&uk*Fl44blRqejPgOf9$Wyd#X9VB+JXd=MJ1r%|HuKv-Q;Jy`0gbtQY~ zs097gTukJeGM2O8M~%J$^x{tygllSS=Tn))lI zb~E*J56o~&zG~rYr~m!S@&|p$i~NP?5AzOl*lj3_Q7VYJ{G}j&t;J|f^(9VuQeZex<~ z$^J_JJEx%MTzv%X3Cd8A%n&)l59?jX}oYlGX_wt0n>XM|F z$Bm;Z%odtU^y-=sJv?5PEo?bSXGKOFt|0{O)La#H%UU?ZW?fer9dqkl3B8qSufsdA zdesj*kZYtpnzrI4V!BNUZ*iYA% zjI86-gYiv9mOUwkyeRTzPKjh{xQ3rqNkUBG$1#@$)C> zU%$CVH#)d1JMt9v*i#ni&#KRWG)^*Vj<`KhB;qe8f$$PbaGs4fgtDE^7Y*kq_33)n zTe?y2tTIK?HF^kT7#g<{vR}aqq~a4EX2wZPjW8aw!|0y7u}=;R&A`kKcm@EOxr|hA z^^w>!4-Kakg!1>i@B$NsS)}m(MG4!!u(x1(v=4CEFmXB^&83?kge_JUvcz=57Io9{ zERlS|`<`{UTaL4G#|bj~QNthlb6H_#ukej?TImfopx?t=NjP*%CDL{##6-|t*}VMp zHo$C62z6BL$6qo1_YpXJu44s^#UIUTiRrd~iKH;WaBd)@ygxRts>#%iNvp=!+Qk|j z*kTGBsN!eu@ZHpLe{!6B3lIF{?;I2$AQsRm8vfqY?fUzIZy(A2Bfnv$1PEbSpC7S3 zd-zS=EO*2CQ3^gR$0M>*rs|1?Oj)p?Id|0obVha!Fw9W%YE zXLR$As(g0W;( zzbuCSX96wL2>nr0vY*@HEK5;9NkPSOy8h0btTFW)eH1>c64nBUCawu(jn+R(Cw2_U zuaa@7*&35hD3M-ib0}At&$u>Kz~564LaynNjeK=Rv{{C~S#~Zb{wQ8RNT9Fe0n;I_ z>EX1zaVi>Ro?U~u2TSe?A)7+fM-^eX%;ZySU$Z%c(ciDP+kGN=R2W_6q;^iw?*@S- zEOLyK1s8FL8bJb-;dk^-Rg21c=lR=9+KQ zo1x#=sJhR!bEjfpJEm9|;g{sv#yQG8A`J{bJ~)th7$DpLk(xYDpv?Muy14O*NOfsG z01!1?u05?_>JLw7ttvii?Ehk@|07zf9CnJ^=g~WLhXDaMl@I0zumXwmKS>|<*o36s zwT7&}-wXI<-9ca(I!oDINY@=S0OQ<6Uip;eP61R!W%M}S<dD%Pp*341l0ts_suJxro_3K-)lPBWIJ6f$OY>ox$L~#GUJBIM+Y-QReW(H$>%_ zNur_CKNG;DVyhi7;@-%X$X5tuO&1{H2an*l0K_@rcp}f4GMbNa>&mjBd>DG~7#vX# zQ7?Mg{X;aSNu~@enolW>!HSvP>saMupRD8_MXI7(_;1D%JhymREuh)3jVa)$s@%ANA!WjvpsB;}fxi^H@qt0JyWPfQ zPXg&=XfoAr?7$c^Z_JGn5Y)0kW~^YC$CU5QR-FmlS+`V7GPmgh7PH8VL6j6FI_Y8= zGqIC0q}4qtF0%Q8^_iLYKigT@`31#Uxrq3s4-Y6WW3WbTtmsl)9QqX`q4kvXt)^xb ztc}JG0Kr43{T$h<;L7FBMm>IOdZjIZIUviUcc7*0@8hPDIHnw3Sw{BS)d+)(R3mD~ z1E_D*jQp6gt7~pjiB)TQT$Ss|1HMzU+Exy+2*We()|?=#pQIke9%V~(E-V^|%XMV? zQc{Uem>E99BNC-e49|a&K+7}Dn87BPTvk^1O0yD~CMG$Seh$74jK8c2``K*o4S-9_ zr!tM|e=#>bDtNPu+KZ_P3-RLmAv)-$5_#LJdD9UCrCSIZ1+rq!>Y%gaqX1HjUyCe7 zqVIaXiFw4xRXC?I`0V>6bUa)^nBbs%=Q&xf;NLf!lF`=xShUj+f+(N9SYa|)t)iY! zYg()@Il3-jbFmBOa}Wv%NAFDi2>enPkZEG;HY)a5a#3v7`Hrj7YFUr+JC342PD^LYa|b2(rRf|nHqk3NQm*e%?)|h^be{$FdU2=sd#}U1~&S-S%&n% z3#s1H3aQ`n{!zK5{G)cu`N#B1!$0;K>XrCo6Yip_+G9yVQwn-P4(}Rtc$F!?zGU|c z-2sU0KrVI4TNj^g4R3ER-GU{X+_S}~e%O%)Ys_YTXrY|K%^FPz@5ub?k7I?JP$B2b zoO>Y7*0qi#CIWpEp^OAD2M9}bKC?HpJ#v*YL+JA{HY}#u6l0IQ&pmcAuH@9~GUpKa=xvdO$KL)U1`+G9Mjw``|Gf7YTZfE2^X$>CAf%b@}D9|S?3s$;!b{UAU z=Y>5dzj{I!3aeTn3HCZOT@MF|LlN_H(D()o?!6^CVo?7^;R$=^1NBevqS)OJ% z%jDdT+uj0*tQVw>Aij$eS*9@T&3&0qm>6e4rk;OHXJx{xjD% zr#^8ZUsS6dQv$#+NrE}fWM@LBKLKIt+wL%UQ_3(G8x93f(4smc4}1>o$UD2ybPkP@ z6#>WWH{w1WZkJT8@Q&x6Mtd41lbWwxOll3l-`DIovvWM>$-bg^J?dG*Opj;9iAIh2x;@q0{rUb5ogX4?YtZKpk0377FSASZ9J`^>+BQ82>6D(LqqIjF>S^Qm z$DW(-&!#7il-=+V^}Gwi_R8v(|5(yxHL&(a?qCwKpI+%a$C-wEmfJS}ys#P0x#D8t z#Mqo$mwR8*yIEcY$Q$E{Dt9&EvX(_c{?IUD#rG}g>_^t6jx#?s(s+O~mtODYC1hi> zeLI(Mti^j(c+1V_FWtcKk4HWgMS~sjvVN$fAv_# zuPd?+?K?K6Yx!^ftKi4JqmDccM(^NmseYOvn_A}I+T3=FA344Nzt6>`pA`|P7B~^p ze^5Ttyzv@FY)0JGccl93A_$zbKY-JOx(aF> z;c#PiyCh=@$=0nQm3wHR0=0vW*D?Wu#OWv@2(Ir20`c}@ono7)Th-HmWMI39EX*pI ztY5_Y*HvUu$v3Ckol+Xrxx_x_t$XIjxcwvkS>gBCmr zrcj2IQZ_IOdJ(Hs5^=VX6tsf`JPVm`J_=dXkRomn7FuvANus&*qH=agVTcWKS9}n0~X+4;QSHp zCtW?cbQIWka5OP{F)(v}C>oWTxnvZn76PMe5%l(wm}&}$VK+-$nGPp<{`UHx)%vzx zo-iHwTrxV1={a?P=YZ|!Tk40#OCJi-aVeIk(Cu@dt!sDT+V_p_yg;lTtateVd&y3w zFlh>n-AA%564I!7G^oyGeDlCxB_g83 zT|@udO#y+Sg+@|=se5$)J0hbl?y%?E_dp^cls*`rz?4zqy~RlJ7uady!(9Vd;{Ct- z0{&tH)Z#A~^f$IvpK)RQHyD*X{gwBfuMV(z=8w!Q{gSAS0@JL2mR2trFAcC%Pb(v~5nW+mRxnU_GjPL0@e{^LAUe zB0bp)Be&JR%?b^JHGEQcF)BL?jxfbHYcXj?hhh+UENmdKq`6o6+$vUuIA$h$X-_1{ z4dK^0;Vp%H`WAs@BzyE0^#yde2%5xf4Ib2$^goKJ<;C<8?({TE4H`#aHrxDXn-bxk zD#`v(?ZHc$i*a`0rTBWNrNcn%&3;nbN-Vm;>bg_-9=z(^i=Q2>javoS61F>~1j_x< zHtoRZ*Fd6o03XaGu|E&6NJ7g(qRszn1p&=Kc;DMCn)U*$5Cd;m{eGryaS!2^hmJ^> z2ADlhTi+?Gy}-Ef$m^rw_rKbtQkK#i3kgp3K^C2`%;~OR%&D#@n`}e6n;E7WLteLnDWF>o z`$#pFF9V2hk}DeW*bAvV-M$RnQE?nG>u}T;Y0FqLf>8D-iL_ z9W8vAo%{=~I?za}E*RJwX3SQwRR&B!=YKTgGUtuy&^QQ7R7MIIyE~P7TBx-scQ~)b zec^toj@!OHd&Vg-N~5(e(be+S8=_S(9>*?o)`RlZ=;A#%1u`5XvwcwnRr1tZYo!UeuQs=`Bm#rAZnRBZaL zn37WWQn$JNJm0)0gQM|YS#e?0ZU@S(K;*u?v3AV5q31(-dx8a_$|8X!oxl2ZEgE~A z4>L-%rNH$YE$S<5k06?WP0rQ}T7K?%kKjHW4#1uL=Bi1@pvz1LRR7t=#*8rI{P5Bd`AoK(jp}Os>B*dLh{-h@ zm}lLc{Y04iA-Cq``}BJ@u-kfD?2TzUkg;#0#g_1B%5idG+}22@!EZ)BJD}-Y0;xJ^ z$OpFnz8o(hyz&?&Q`*J*JEuE}H3P6U5YKT4ATvj^o^gYQ(2fPkv;!p(n;tbu_Q0bx zQRK*hFp}trWj2##a7EqKc_%yDFP_xRj;3ZZwY`8)J(^*OwOnHB%m$X}R%MFs zGh|&frakF40$Rp|IwplWas)7b4BEZb4GI~Uv$m{R+g~;UmR4WgZO$4u>UurJvo~kB zB3BOYi-Ayyd#nv|{p!%wSHdIf ze_mZvh6(b-cHCeL3fs1^hydR#dBXbB<*iSKSWK!{l0%yJ5Pvlov?FSMk|x=ql9MGw zwPJ@vWz5lbKXJI(AoW!hwxwlbUy~+M)wpe`Oc{uLFsz+ed_}=AtXc1sBt=w@c+3eO zeC#MkW2NLZQL?2MX`}q!7osV~wnq=S5zv!}BUS7G%dv{m2XMA6^8rIKLtMP)8MutY zF%X=y`qTUGTg_MWoO1eG#wN}vF<(`b;Zxb{n~Op~x{pLZ(115QYye|7 zUQa{ljVq$vImaZAw;NL6)rEHD;*hLdNzq6(?^G4UN~?PCvV^jTpowo;KUvLA(dr-c z4=s3Wd3YB5XraHz4-iQmGBWxrQ+=O>u-a8qNJ{wC*Tv2iVbZ~LL}}PCt=T_s7=d&c z0ir~6s%`sf1NrV_Y#cpzT=cmip>w~M0T<)oh1%h?!wY(CFFuvfZ>mGaQzaoeuTe)V z1lP4PV0?0!<*IA~y-E|jG>y{~!!`R#uJ{SgO{WJSl6vyc1w6#58y;OzVPJwPx1o0& z@nd5>kVD)BuLQBpg?^s{7prJVMhlcFrH)!Azdbu_s%T&#c4NsaHk_%T9y6oMo$P>! z52i1-MVz3hA<4|t1zS*1+>f?4g@M3--&CYl zMIiY3Jdq^o%teOpd;CznbqRS1eF&i=ycZu0bj_oz+{+NxlDF$|weV}KiIM#r^_#pq zQ;wvL zs-`Qg6OYZGdA0uNg?5#izfgZV#iK^_JM72h-K%BzDV-9ALsg~WUK9sd={KYxh!=+= zL^f417UtWhJglYn3NnnuKeyqmgqYeJ^-S~vw<~dCj6BB95SLQ>9tlbq%Y zm<_k_PgcxZ7d;3Q#|!f5`E|15%iUtG#G3&)TB-=*+EH+TUYl2O(^cn<{KOxXuPww2 ztfo6$;;-%JY^no)qS3x9Z1l@V^dV7&k@D6?oBF}2?y|9~0(KGoTByqu3nFG<1KkLL zi%FXokL)P^wyd1&h_zVX9i4I|EIqBtPo}mb|r_&+v-qnW_Z>2O-?IUrz zH|^zwE4sOW_a%$v9IvQGR`YodqH*TvwIf6coZ`lZ7Q=u%|M$W=lWct zcXie=!X9^?t{6T&h1rg_xx5U^t;L+SqS5LH<)oX(q!GvSGbZ`;B2(57I4UR5KAlu! zCgI0-ZTSQzJVeNPZ!2wH6*b90n&O6p5`1Vg!l$~eB?3uiJD-SIF6$P%Q$YM?g{{MR zc!Ok8Q9a1yXMQzpx?-z0NeHFaB+IQ^;2zg`snUK8DY=tsb$HNjI!t^e+cpKKK7!Qh zZ0s+~5)ORf`m%{M1vv z*ZlYGP;7!I9cRM?^OUSdXf^h6ork|qO-sX+kEt*1eesRKlIntBipyivOUhW73WKIb zb%g$a`IwIuGfvF@(R+t>f?K4?p0&rW({PZ%=;B6ADPN;2b2RCvaq}{;X76Y!EQ36~ z!D>~lnV)akps?{Z_o&^XGr=_CAIR6TGny559|v7j!I_*|EG;>b$~+{`p@_sRGVg@U z)AFK##>_YZ9OTAS6jp1Yv%~#aiU?!*ZG@+1nT?L;pr6}ATkky$br{32lN~_s_J&EO zXn9=CSQ|q%3tg$QwTuWXb4|n@GFtK}e;gw6RmZw6hRZ2@Z$>tb=TF5w9?p9JL1IvPcGRbxajbtOOcVg(;5 z$rtF&(j~mT3gtg9xNui9n-ncbZjRWeEjmWX@Yl}2Q__H59y5X3?fp)3jv0STr~(K8 ze49^{2?J}h;jXXxtWf_@d%YLuU%p+cai}|zu{@%$BsSfch7IM*$C+SsIvIY;9)=b{ z@wRt4zI_$wwchhV00BxU_nzkcSy5jw|0sTHVE(%(6#*qxlhtKck+o(QSL;YF41tjvS4Bqxw)24SW;oGT$3=*e=_+oH+3j*u!)u4 z)qxN1#|P>SB(3GlDl($8QEC*WwD1|U-x0!`K}mvS1{=UoA2E)2r~ptHQw)8pgdaHR zMiEBm{_F3>8)t;;xow4`8Yc8wWNmz(omAXH1NVFK_wb~sb09h8UceJf#J}Pqd?tuGMZ4mLuE2e)sS=SNAy$vcF#~d=k02DW1Q+pM@(K8=k|w%;aGIgZ>XR_zo6O zJ>?_)<$jR=f?TYi5KDP@H=I#FJGSh3Rwb5k%t z!Km2;Yvl^^bG!CZ9(fx+#}8uXkf$*j4|Bu@U?V{sXCvf3!Jb9_6AUOel8`)kR3DUF z5yV5oU^>i1)d*l(al}f@#G6qqPRnA3#6$YEN0k>F?$L)Hf^J+fgMdjWJ%IouyD}W( zDK&E-!KRxqqnfKc4iYRjh+zuGZ=@(FDEa3mI%BkV`I9Y^=C8$(e*a*QRW#LNh89Zd zE1Noa$I+1UIkS1$*O`l&0EZbeC2S;F_s03jgif2dx`)Pi@W`B*2FZ%<;Q?5h_ffi5 z>R^ZVseh*DORrj6NaNR8EE|%jk~_Cg-^-5!OtMssWdn+=4<4^t_=nXG0{fmk=Kq8f zPK;r(cztItqvNU?R9rWFgZj_Xf;DdMy@E!aWPt))B_xunurw6j+ok6Xl$#@->aXhp zFOcu5dtBjY3h;(Ho=1~^o+EzbZPz-3J@a_Y09@o;QWO|lxxxaOvm2oA)l0)Z&04MY zQ(-7C00M{6aGi7#Cjw09|7jvy=KBn-4UW7c+s{6 zR+OkIDmSA?MB zTSZuWSEHeD6n~!W5G3zvXlz$moeJ-X$Ye1-L?BXathCgS7!DuPcq>^yY(euuGfh=h zotx-^xW6$b0((@*i&?a^sz43g_4%Hw_|VrBIGH4OANgrZ1hlmwrpo07i`lvOgH$Ud zIh!AI?7E9|Sy^G1X~vOq^5TddTf7 zpMep3eeBsw@U-f`M`fZG`s86xS2bLXnp;yyCXo3bExNVH}!7e!u?A& z&_gdFn~-pIHDWItaW1To_ziMFbvXd-(e%{%oC``PS@4Mf*|>owK<8jH162K%Qs=LD z#=C)IE)VZd$fQ6VuuEO&NC~;DyhXRuPqtTfkQ~N9RxaHbdr$l#QJ%cpa*Qwv)j$=F zOkJTE~VdmqN1M9c8doc#$@=19}ii$kb{aH%Lfr?KNC)dvQmZpw^j zV=)+}?U6|2UYq)%r8>-!?m2I^+0kb{;`aoq9%wYmL>Wc7t6C?%g#v4k%_+LGF4AV2 zXXuA3=x@Y2Ee&OElI$gf%Y!C>C1Ca%qYCClpZ-DsZ=Y;)T@MXBmuX|%Z_LinOZb*( z>DraHk{!d&VNvxPp(&b|uT&PBTBnG(L=68p<&}1}hfJgVz(S;hd=r>lRh9&0ObQfp z)#(%7kagkiy)uO$bvNjcPFV#=R@g*+7`J7#H`Lrh0{{esj7}dV*qz$t!_lZ-njbfc?!N64GjT<_UpdpWS}9 zx)jDZ{n64HkZcv0H$WRXB3etbW-li&wS$fXMOvyQbH_2oV=|nb(A<~+KDFzW^x?JZ z+C_e4j^=yQM+NIOK0_^{`BoqoMS|rUn%T zH_hn0*y*S4gSTrp&VlwaUN7+Akw+%e4p5EW@AMt}==g9a_ zoQf#P3einbRW|@87|&2~a5%>)Z)8vdPzvywXuCb+sAPK(rO*^OdU4Bgvp{q+92q-b zq0HqH1JUnR2y!e1j!trROux$S8j7(k1*O(NF%>xKh>hZpi=T@ut8MUID>rnZf(3^b z5lT}!bdFXQA8o0$C}sR#g;ZZfG5x!N)mmEzqTsD|MAbBn@2pG)o2AUR`zSM3l2 zEjtM_A5l~OSBKNO_0O^9%7Hkj_}FK!;^t)Bwp55$6IHKbg8y_~pHD!fA!So1wPixt zbZHq85{GN51nW$ifa`qliEAK(#h45+SL%v2%C@@L`Vco9JGn#78W?6h5$$B)(ihGI z-l#NTYSg1njg@3^Qky{%5#659slu({s9&v$88953{iOwN)MKQ%8$u!s_jDwpL*X&7 zSH_k1(j{(GTmGqHiOc;pj~6}K@a4bs_-msu3`bVxOn4Ubs_9{dsw1ait;R)<9h-ZL zqG{U{JcQGd)oei%4kUTRx^5&#v^(o`U_sQHir26*oWgtaqSuAUoqga#TkFt6KPFhK zcYEMi>qJ0O?3S7h%rU*mHrS>9l>xK+DVOufQGQZJ45SuB%^V+u!Mn^d*}soJ0wCuc zooq-foIVDGOrGCg7oI}qIG&U=hmNl>WUUeqI1qgeq>+_+1-3?w2elQAN(e;QTTF@t zBxf9$6FOGZ8wlu=H#-CsI)*s!YkK?Xjw_Im_ zch_#`CDE6_))!-z@9o!8@ajx2fb`mHjRtoMy>^2>VGrEsEOarTDVaCKF1 zUfsoz{{gSw7fcH-1YvzN<$M%$4S~bzPQbw`)+*tVHQ_k}s!HTp zj^3(^#UX>&rr(S*gLr4e8iY*Yj}n3c4ocpxIQ5gwQg*yx|M!3U>Co5`RPP%Iu_*;& z^+N~t2&8kAXD%cSX)w#S$JiptPS~-qmKBzp<&vtqo&%UFDD`y^izt^Xe{d#h6OLE$ zwERu=L#k|dJ82w9YBxt%?;d9n!o3#Kw34Z5)eJc4#s- zg1GtIc0E4gsvmQ}xZmB-Z$r{WO;QHu!QFW12+4|7BG8B*Gb8Kh%yA5?*dS*_aOp;z z97t%fZZUj=Qp+Ee(4p0KCw^ab@HEl8@{INml%#F0vLt!2uC1q`hEy=C^TVya&9KHdW8Bk zznV-xd;j3?SVAQZW`lniR1k0Z%)g*b@y#C*B9|w&O_Wi$nK-!si*fQ7HOte0{&={O z4$%^lMqUpaYDpqr2fEA?`eZ{u$eQ5_ma$@pVcVn{$?ciPriFBdF_R&d462N{QJKW; z_>Xui(9`g_Z6|MVJWR`sAo>Y4gVTf8&3HJ1yOXNOVauq={D~ST_2sS71!?TltG$wH zg4i8kY^|i#7WFKuWv9mbq{X)a?viiS+g-t0w$pc|DEc*Gpl?jGI{B!7IX)nfCPd!;C!em&ubK+xi?rz)Vfq#m%PQ#6>-zFi0oei;1)JCm4X&qO7{@rA7nRxcxq7v)D*-aR{%4(w>ytVtG_tbUN`&=*&QzcVS{T z$h-3bZuWC(uf4m?d*KdnRu-~n??uF0bycnZUUZ9R*zGL&)!b3xc;h-wSPd>7Rgg1~~5Wx`H|?Fbn{u7LguFT<^9)CnK>@s- ztdL+1>P)3i)YKP-J*kVHxUCi30L!w{nV7olDvQt^UVK#172A=x^hT`V=IoqH_+;2zw~4VG|e zgaV+NGeo+!gi+{AB^UyGsq_K6(|$cqJLj1L7`;bbwr6m#W(cknv`09zpgzPd)A=i@ zaNtXkr&iyF3#`PRzkLVv9pt-4rVHI&^y_}78bO>bT!@B z{~~(+fX*JZFAjDGx(-$bDmu@Lpah)zx59l#vc=C=(UU9qTc|V!UZ8=A0!OO`FZ0WH zn_|6gu(`5X{Z{)0xy^+Z4QAj6FpP${u|-ou4HgaNdSdF254&^c+uygyL@=&f5(IRw z#>`oH_~(ZPr^4cXOLR?iPV{HoIsLh`UM}%MjV*JTmd;?4%u5+F2vMw4elfp;Po%WLZI3#l);b{Ej(6i3no{7> z+s+0VlbEOPihqvkg47G9{Y<{bzhJ3Ug$s8gu(gP5Nj?WMaKbUNL!4iXi<(E;| z_+cYri5;AWe~ZEr2-(&>-9tmYjO6|K_+2F0`xNK{@$%j`lw z_tpaL(m&|Qq|>p2@zCU@?#HqSsbG%_WAXK@g-%6{)f#TKfLjGSz+ki+KHsPqRtCdL zxxP)%+Qg@lrRn#3DjUzl&*$S@^F%1t3FG1@Q^0Ez&e2{au;IKpvZgGoX1dO$w|O>$ z!ac%D=Z91xpI|xe>m;tPr!0=IQd6UiHK+tXNaIj2&g%eYWHxK*|Lf{3fZ}M@whh58 zxG(PR!6CujJ-EBOZEy{?xVyU(+}+(RSa1Tt$-i&Dzs`GdrfRB&;hG-n>FJ$*p8Njv z3npc>g&e|M`AZiAXCIF?I-hj2QdJgdUFR)MSyA0DkVz!9NO7P)^wFy3!Fcz z6Txnxan6*Br$N6%?*)5jxJzJDjX7<+FiS^ul%w=Y==2)qnoOEep(aeGUT_J0a%~{? zJ>S`D2HJj*GIoUE8hT|AlUCwOgTAWP5MmaMBmY3no`U7FBh}ea#2T_-b$%Ld?17je?GvqcuGzO zjeFxBJ6>H`c>sH%S820nSNk@7(UZ-%NRVy2mwu zv>YBqBS2%x`-DUm-A+X*0-u{h$A_BXwbCb=5C(b3O_}-$S*Dc+7>r~X0uIMd!W1jV z62&twt|D77A_Q(R>~^8y>;;e#Bj+KYhSWl#{+yy96Nns6y}l;RHXH*Lc_J$QyowIC z@OtLm^6$c8*^x$IsXvy*&`-XK{9^l_@0-;vU$Q+VWrs{+6NvV9)ebLxF$vt!Zh+CD zC{D7J*RmEUYZ^)bdfTM%vgQ(8hud3nmnK(fbAb+6aA|)r!e;_ul@&uxLqSsR9GGkF*IzLfzA~pR?l_{&+&-lwMMyakzYJcd_VxndB$mK_S z<-QN51X&v7T)Fr$J&Sj%yVj|8<&f~362m0($hTNgFw1rWO_7)D&Vz<#TD&;Q2Go~h%Y{rm<=}CxGBVrF=peu&x(GpOJ_n=y|btv@Q4X{4s2v}Bit71Xi^+1(jqHBuVP=T zRV(gllpAu^B`<(e4j7t$PMj)H|7?<;13mt7en@%Fumw}Su(;+Xv$0@1$VxP*++rDN zO?$#;nfKWx|NWBvTIJwK!z)kEMJMw<3II!M)rxklC%1u#q8PcNncXr?)LS=-!6PFX zga7LZ4iLd%%oE9BTq`O(S~Psj6HXc75>Yo3_l9`0nXaN^B9G#^MMhkK@7XuCQDXA< zUTO5bqgdZFyHHOUyHMY!a%_B(eN22wUOXbfvcdw@LG#A-v08I0jwK*+j>`GyLp0k1 zfI_wQeoS(RwqGc8A7cn6(RYw?A1M>9^}DcsJ@DN*N96^b$WP~)bPG3mc+Sb03Ll>pEVY2lz7`86Zh-jN`RZUc&5@0h{4u^1)(#7umXUUIl>WN}H$ON9eN zq1&b+BYk9{1Sx*e(>L994Vf#&`3G;-Xd!Q=75_p3)u#TSy6;fq%@kd&5v-x<{aM;O zuqVNgak||yqu!Q;hx2l@o!OKBd&&|LdIr&BE~(t2pWmkdJx7;%+MB;!{gU-1n*CYU z3zcpkQ5SBWLeFTd-KaHAp3PJy0==oj7)sz3z44@8%O_E>*sWUU1HnY2;mrPZgl4#$ zc}>H_!VX{7yzLg&c_#tVM4AXGx-20pFpND(!>72m==w*Hh;T&TEvN$TVB^?QeX4UR zt8BB4!%e`W*j}n8Vcb&h%lPx1_ft&)7bDdhQ?{1!Mcmr72 zJp7CvT%KnuaM>N3i)FxPI68KG$&6Sf}RGz`Z-g>@jb+PCYI_e2`eJ?!ZY6 z@vJu!Q>!)GNa*=kjpmRYZ4Yyg4^iCr2SKu+c-QW!o_-!$d^Sf{lY4p^-SOvVR?H0J zVo)A}_$Pz-o#7xF7L{P8{Wn6Bb~rbX?%kKlF$7ZtAc7R0UqC!sjcGiB0%-N<9GL#T^pXG<(TxeI)|T3CotHKl%-Quw(jzDj0X zCHyjd*@!IvWle!5j3?r8Pj+nBfwp{mkNKJ?bnt$K&Og`VId+A`e@OZosSCGzkLEff zN)s-x1yp|p8|gykZisFV9FPw_mF}xuo?N9!d?x-{`_t*X=VvQ^%R^dSc>OunlGHJO z2nNFGo5&sA$i?0UyLs0nYM%HI<$IOZy0SKUC3Q|%CtDpn1E)=$^J_B<tT6Cq%^QYy<*)Lsx!I*VqL(lo;nf7P3 zm%(sf+KSI1MmLQK5oHO4mCei7b#A4JUfYFYsD}$6=FM ziQ(!FpVuiuPxN(w6Rhh#Q25l}T7ryzVD%#qWYuo;6V1H_III^Vs!rW?uzuT)%qpf6 z2({(5Fg<}v4%R6$;zCK_6k7;BHVtp2DHK0WqpyV$LmbQrzlbLwbBXgx6lh`V*%GzD zusE^hWF6*B*pWpXKjw?AL;Hp$(j?+UQs=;w4coNPZU?9W4$eA4;`+U}lOFMsxTb?D zL=r~Z9RBF!O|o`x(yA!LOo-R)c!E|!nNwA^cZA7Jp?A*Xs{6_;{Flf(i#6p|S#z0VZhODcIb*9gwgXE?#CS$$=z_SAnr z$rVb~Uz~TFlS7dU3Nw7xk)GA~2aQHA`~pj5U`3(g3})JZ@ZVj(&Z@fWT5ug6g9XJ0 z(}1pGh=F#B^GaC$7aB`d4EE#`xW2t?rnWtXGI6*DU&6^0IMcKPw=bIc7(UC|^Uvn& zv0O!waH4Z1{47ObZLkf5pf%r<8e_)o(pf!VKDa{}XCKf3@yC2>4e6oh=uA7-12Mfd z1Fhp|HlUFEm6~rOyAMOCTL?nx8RRPJIIlCwCd5T?Hj)aHSC)$Hh%g3XvD??8m|OSu z&3?`2H@~J{deVIV8lcgeN43iopY|0y_wSy9xWRrATegJ2v>F+1^%TCl%w_A)ebYnpW9Hbb z9rv-e%{P{oheC^n*&=uEYFAbT{Jhv=g`9ab*n@DW(L#WCWN_XL07c1SI$o1p447)m zp0W)!URMKuKz)z)_Wcs(yhuP+)TBVF7CDqQIUjIN=7SCv*Psx~Okq9aU`b$6j@Nxd zN<@|PMIWgfbH5Uc@g#4JW=OPXjYQ9}!1W_8PkoMNRlbWi+eY&WTM$}cIvGV)C-#dS z5HkMURpEELrD1KI5Fk%BR?P1jr6BoD+~A(CNk)U%Bpr>dzJZcm5@+<-;qN-lVYT<%3$D{u;C7k&&lk6=7+BDcc&>N$<{+{J zCLrTZNFb*<)@_s|iMiPXT^wsoZa55n6nioj<)Bc689u|iH?g7eXfAv{(dlni8E>$n z0yP)xQdAZ>9LQ>N84lcpJ{x`}XWE7MA+M!2IB8(bpA6kb~Hwxu=YT+K%Y z_;-qe1bN93^H!tcp`(^zNDuF_nv4)@^2P$3Of8d*mYd2i)7|w@ZK2b~TJSx7z4x44 z=I6539^!-2c^Ik3a+^(FdG-q3aGOEyRBx~+^jBS|x~U0H@3OK2Ms^bGMWN(tlmiv! ztFBXuE#I^xA9z1yiNFTY2@HltTOczm+xjYv|3W7?wMTp5#m}I#-Q}I!I&f#Tl?Y|T zCuUhEZV{a4j`g%Si+~qH+^1#Rh$>I-F~g7iE%*u~Vmt=)OSLS`l1j$HG*wT%lEUi^ z_jas(fK|UZ)y$?cG{Uosb!{RqS@xh|W`S>MZ1f!98 zI+N5N_JoOFd=Jxmip*i!y>zLNgL1H|g_Sw$BX3vuQY;TTaG3QufbLfVwi}Sn@O=T| z366aWyY>)KLhupP#vt^g!ffwFJf&GPZTqv4JA)~ieqo8l)GW8`Z_L7&8XjXl_+<2M zU8#B*8^%i1Q~jJe2*-GPzVkAZW>%`Cv_f16XY_MM z^o&0HpZW=tjdXKZ3Ytn(0G?0}Q7%e#+b&=ggjeiE6~cc|(t+2Jy3TU|nXV|TOpKis zKH$Fa`qAI^f&+RnE^Xg^A|E&t$mI8!7EJNE*bsi{hH{Kn6k=bw+BNPMo$RL;{nem0 zm&s;I(5{JW&v1bhngq^IFy=f%djH#_T#Y$@G%KGF`>izso4MaW|x1lh(3py1$ zj@b?M$L?aFa%I?)CJ%R>i*dsXPQRx>za__S+}n+-=sM@9u9yzz#E-1YSIR(gGMREa zHi{zqT{vxI;?Z@hsw}Kg$VGr?hS$Zp2K5#OhpuTf!=_VS+g5ALhIL4&SX^#;Sks8E zt}odKj|zU(uUJhi1BI;DA8##3Ud45dSJH(MMeT@3t7b9Xjk*~D{FrS&W|>GSqv{&(QKt+L0pC>;?p|?f zXqlrCuIi9m8$TqfR_?e(R%prAS^uOO_vJtrrgEj;JM+5!!JW#Aqgc@5nUdV8_()PJ zNSl)P6Wwch7||%gF$=9|gW~UoMK3fAss`M!bbzLlg@MXwf*$MjX4*1L2Kltmk&Q@Z zbombfkqfDK=yFQPZ|%0+W=`$vXUq~LgEUtxlgTXO{u|9qOPgz5OPR(!x` zK3KEdrio3Z|C6*a=;g%MnpCx+z(Qfg@$pDqzXZtV7h>El(Y{P!@8>y4m=F^QhkP;; zTcfT7fv2xudiRqq`M@(#u|Xs;B-LcZfHF2O>T@zt=;O@`W059e3P5UotH}+>U3+#E zv(o<&sV`b;)(Hk2hklD#S6Tq4ZLCi}Tk0zCt~0v#cV3DIQHwPkU$&w2FnlP!mh`Y2 zas|+HOz9hzTj-haOH&omdgN4VOu^6;2}S2a;K~;q34!oF4~53bnQim7!TZ}QuL2_< z_55|_+`+}Ov|>JQV)W=a#ne0!t-Odi9_jH2y7{z(E`4toKU2Geq}+yK9+AFOzK?UM zLJ;dIWv}c^(wCq&vf^A2Rh*GDavYFTp&v+E4XJ`@mbxHD7UH@l(I7`_XE^&1YRc>u z9>aq72I=+_-pa^5A!pVJ{+uB+hUMFo?wPW`i1QCLEx9CtTfCOheON#G#E2Gp`?uM=V4?9)Mdjln*<~R83G*qgwtl5FhXP*=I-%a4Pb{~lu@a9`_lZTtiVWQ(I<~B<+91X-d@4Ji;4t$V-pLH6Zc{HvI{wyq@j8qAA~8F)y|p+@QGsk}EHf zDm_P4G62>hQAP$UG}A?3U=x?Hz6(tfqJ1EKhS-;nA*<(3KOY(BN~)!^w+^)Uk9zoOeD@em?zSp0Z|}*u4Y1CRO)U z^SXlbyxCjsjlyMl!e#XN@R>}cl#;l^b^7+!Xa4862Cv`G=ll>}8yaZ+t%2|5TxaNM zCJI?q_eg6D2{@E>zwQwP>Hcahkz^Z20|Ey&>o2M!AcCHHn%z8B_YUgy%~Q3UbSW%a zJ1kGy0?iJSvs5D12eIduDq7?D9t1?g+hIkW5AcD#E;ub;H`0&Nn?Ffj_mL$VJ)GSWZR&Y`zyc?`k8Wo& z$g^R!K|;x7l;Wb6Nmxubf^HGM&|nlf)PSeW_8+L=hzQwg}* zDi3bw+2P@}4nj3Lf+(K&Qzon*58oA5o>SZ$!aw*BX;lZX#IdVi0lZEquTV)c%c&Ri zDfjb#lT#1z=V})MoWw=esen!H=xdURnZ6^X@HoFPoqknR7m6h%@y$hzLoUP8Nj;Gl ze>EFM?QxG$yXzgER&%jdA9SRe;SHSyiyX~$O&KR$^HUg2Sd8a`URcHVQbP!Eey29@ z$gAs<#|GnNXzQXf0RJ(_{x5&*5h~HvA`A!!W%_@F>Yy7jKuUACV0Rub!^Fj=-&qqk z@?E~R0T2a@=oq6i5M&}+P+~CB5J84o17wd z1uOK`(@R~;UCRgReqDntjhAj-epM=@mmZhg9BI<9n9q_Ioi3NWN7*hHyxl$*DJ|0- zj({FUFD6`mM;WdXpt{S@fY~t@G_}AnmpJ9{zQGtz zY%O+1#n5s1MX>8yV#H^vZJGl4=w!A~tW-Q26F+u^SK=Ci?tGqMv69SF5Q4RNzBP4O z+Ncm+A+vL~Vlf{3q8OX^;*@wORtD{-8pSCKmU%VRd>$EfplUI6B10Q?d0Aoh`|VqS zvIP8ceDe`G%tiL`I3&4=q}9*~w!*am-?kaCuM_HGKatDW>a@I!x*~L%RG5&EwJnX? zFj}R|A580R#xR;xPDRDe>(#i%<+PdItX<>DaF2|dboW5mp_OX=vWv+TzA81h_E)3U zcRpXqY~(rVfW0ZBtTBc0lDQMs2^gYFkKdimmc21Qn*|Z*?SiNevc4AECQKIF#xwZU zt6TN`Y#mGdV*69NLC2PD0OzA&6@1wFalH~LPJ>BcUB&u-uT(1=F-qv?6mj<^xcmT5 zO6w8#-tT4ssh#A2hW+RGITy0{~8|ZfSu@C z>wb}iHWH$QS=tsEZ7J%I@y42V)z{`9RBI~jg7%VGtEpuCFebMX;=PuPTPt;6z0VPY zKhqkPbqZoMJx>y1$nfU6dyraNoVClwrLdtNhMsI;MC*5&Z$>Vnd?vI$J#DTo(`%`| zsD}*zE&^kExB|*!JXzJSn?9cls@uGCvc}G4!9#aS)LTnSWF!CjO=w0TePhVbh6ikv zI@kAYVi9I)T8VT+=Kwynn;yfi%K9s;j)=Ru3$0dCt(`inE&te*fmm9>yF4XR4?oA) z%>>7kk00jV3FbKI>*DJb=2MgdnNWw`t&4ofI@SZCA;axQL02ct-Mzc7mx`*oJyJj+ zYHwbhb&FJr>!zOrz*nJ)1^w_ao+ENVQn^V~R9<%akaX8r5x>)yAP;rUbk6drp{Z~* zFSz5dD~FRN3NKJ+GaIbX%!l5)k-%AwEx=OrFw@-7*oQEUXpU-aI{SsE1gnO~-pKZ+ zp$01uo_thF_5Eaimke${S-SL=l(B>6N9hp+95&oJW6AaYm&}~>DNYdj+}3t2V@cAa zrz!_EyYtgdu?2zP2M#U1br2-zoU=Z-8}Y&jYpDZtlBa!l-Z&oTt}jTSrh%fp5cl54 z*?=Bh1|tBc0_C1cLwOY9(`w(wLZAkRVhbveYpEf^2`*nbyP4lKZ1t=4Mb7V>Y*&P6 zsX>gzX%bTMWCYc(?wQEArc;b~h-lX;>{oGQniaKq5!d4;0UE^3!mr`h>+5L<3ct`T zDy3i#VHqBp9cIaZlyE&@H62(X^UhOSQR7i*L+Viy(zm2gFCM=8&hA(#O0+T#KKre0cTZ zJ~*jAueOwivtt4(TF!IPLQ=iilKC=$Z$4L!#_r`>*Y>9?U3yt_50HdRiwxH7=jCD} z9sSSH%(SY-)n*7ZTA7>7;_PDcLM~$^&Wp&L-0O8(vzhoD(1TP#1!8nHPnm)oPdrET zQ%IqGytx9>k*OtO4>)m?Wr!0)Qj(2nT*)Grbx@S@n$C=8!$L?NyY@&1N(DZ^Ybs-n zZJZQnW4n!^xc9Qv@tZhZfi(z-a6t!60*OfXt?8T)LpW2y=T_xZ1PH5Yq-C| z%GxJpcs6J`H=H$n$7xGqo&OEb)Fy9V($2uZ^q9FpRtj*Pzh&1rcca;Dt#_VJLTX9; z?3}V;Ke~D2C3_LiqE)Z>3sX@Bh%+pMiG@^>qJ1OYrit!1n(+MH-?&&`bKDx+U_fX| zETV0UX?{Ev%E~gi^6p3etkgmRT;kk5Y_SyULVmrZ<#N!CnQRw193yWj;5zQC<$J|V zEtLW2ndyWv_!OEV1zoj)%0A>9N>1DoF?DLG9Cx#t>vp)Y=DBpcbo3xSPiI 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 delta 47706 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 Date: Thu, 30 Aug 2018 10:32:08 +0300 Subject: [PATCH 232/283] [DOC] Repository GCS ADC not supported (#33238) Make it clear that automatic default credentials (ADC) is not supported for the repository-gcs plugin. "Service Account" method is the only alternative to authn requests to Google Cloud Storage. --- docs/plugins/repository-gcs.asciidoc | 139 ++++++++++++++------------- 1 file changed, 70 insertions(+), 69 deletions(-) diff --git a/docs/plugins/repository-gcs.asciidoc b/docs/plugins/repository-gcs.asciidoc index 829232573850..e3978e65f447 100644 --- a/docs/plugins/repository-gcs.asciidoc +++ b/docs/plugins/repository-gcs.asciidoc @@ -10,71 +10,66 @@ include::install_remove.asciidoc[] [[repository-gcs-usage]] ==== Getting started -The plugin uses the https://cloud.google.com/storage/docs/json_api/[Google Cloud Storage JSON API] (v1) -to connect to the Storage service. If this is the first time you use Google Cloud Storage, you first -need to connect to the https://console.cloud.google.com/[Google Cloud Platform Console] and create a new -project. Once your project is created, you must enable the Cloud Storage Service for your project. +The plugin uses the https://github.com/GoogleCloudPlatform/google-cloud-java/tree/master/google-cloud-clients/google-cloud-storage[Google Cloud Java Client for Storage] +to connect to the Storage service. If you are using +https://cloud.google.com/storage/[Google Cloud Storage] for the first time, you +must connect to the https://console.cloud.google.com/[Google Cloud Platform Console] +and create a new project. After your project is created, you must enable the +Cloud Storage Service for your project. [[repository-gcs-creating-bucket]] ===== Creating a Bucket -Google Cloud Storage service uses the concept of https://cloud.google.com/storage/docs/key-terms[Bucket] -as a container for all the data. Buckets are usually created using the -https://console.cloud.google.com/[Google Cloud Platform Console]. The plugin will not automatically -create buckets. +The Google Cloud Storage service uses the concept of a +https://cloud.google.com/storage/docs/key-terms[bucket] as a container for all +the data. Buckets are usually created using the +https://console.cloud.google.com/[Google Cloud Platform Console]. The plugin +does not automatically create buckets. To create a new bucket: -1. Connect to the https://console.cloud.google.com/[Google Cloud Platform Console] -2. Select your project -3. Go to the https://console.cloud.google.com/storage/browser[Storage Browser] -4. Click the "Create Bucket" button -5. Enter the name of the new bucket -6. Select a storage class -7. Select a location -8. Click the "Create" button +1. Connect to the https://console.cloud.google.com/[Google Cloud Platform Console]. +2. Select your project. +3. Go to the https://console.cloud.google.com/storage/browser[Storage Browser]. +4. Click the *Create Bucket* button. +5. Enter the name of the new bucket. +6. Select a storage class. +7. Select a location. +8. Click the *Create* button. -The bucket should now be created. +For more detailed instructions, see the +https://cloud.google.com/storage/docs/quickstart-console#create_a_bucket[Google Cloud documentation]. [[repository-gcs-service-authentication]] ===== Service Authentication -The plugin supports two authentication modes: - -* The built-in <>. This mode is -recommended if your Elasticsearch node is running on a Compute Engine virtual machine. - -* Specifying <> credentials. - -[[repository-gcs-using-compute-engine]] -===== Using Compute Engine -When running on Compute Engine, the plugin use Google's built-in authentication mechanism to -authenticate on the Storage service. Compute Engine virtual machines are usually associated to a -default service account. This service account can be found in the VM instance details in the -https://console.cloud.google.com/compute/[Compute Engine console]. - -This is the default authentication mode and requires no configuration. - -NOTE: The Compute Engine VM must be allowed to use the Storage service. This can be done only at VM -creation time, when "Storage" access can be configured to "Read/Write" permission. Check your -instance details at the section "Cloud API access scopes". +The plugin must authenticate the requests it makes to the Google Cloud Storage +service. It is common for Google client libraries to employ a strategy named https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application[application default credentials]. +However, that strategy is **not** supported for use with Elasticsearch. The +plugin operates under the Elasticsearch process, which runs with the security +manager enabled. The security manager obstructs the "automatic" credential discovery. +Therefore, you must configure <> +credentials even if you are using an environment that does not normally require +this configuration (such as Compute Engine, Kubernetes Engine or App Engine). [[repository-gcs-using-service-account]] ===== Using a Service Account -If your Elasticsearch node is not running on Compute Engine, or if you don't want to use Google's -built-in authentication mechanism, you can authenticate on the Storage service using a -https://cloud.google.com/iam/docs/overview#service_account[Service Account] file. +You have to obtain and provide https://cloud.google.com/iam/docs/overview#service_account[service account credentials] +manually. + +For detailed information about generating JSON service account files, see the https://cloud.google.com/storage/docs/authentication?hl=en#service_accounts[Google Cloud documentation]. +Note that the PKCS12 format is not supported by this plugin. -To create a service account file: +Here is a summary of the steps: -1. Connect to the https://console.cloud.google.com/[Google Cloud Platform Console] -2. Select your project -3. Got to the https://console.cloud.google.com/permissions[Permission] tab -4. Select the https://console.cloud.google.com/permissions/serviceaccounts[Service Accounts] tab -5. Click on "Create service account" -6. Once created, select the new service account and download a JSON key file +1. Connect to the https://console.cloud.google.com/[Google Cloud Platform Console]. +2. Select your project. +3. Got to the https://console.cloud.google.com/permissions[Permission] tab. +4. Select the https://console.cloud.google.com/permissions/serviceaccounts[Service Accounts] tab. +5. Click *Create service account*. +6. After the account is created, select it and download a JSON key file. -A service account file looks like this: +A JSON service account file looks like this: [source,js] ---- @@ -84,19 +79,26 @@ A service account file looks like this: "private_key_id": "...", "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", "client_email": "service-account-for-your-repository@your-project-id.iam.gserviceaccount.com", - "client_id": "..." + "client_id": "...", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-bucket@your-project-id.iam.gserviceaccount.com" } ---- // NOTCONSOLE -This file must be stored in the {ref}/secure-settings.html[elasticsearch keystore], under a setting name -of the form `gcs.client.NAME.credentials_file`, where `NAME` is the name of the client configuration. -The default client name is `default`, but a different client name can be specified in repository -settings using `client`. +To provide this file to the plugin, it must be stored in the {ref}/secure-settings.html[Elasticsearch keystore]. You must add a setting name of the form `gcs.client.NAME.credentials_file`, where `NAME` +is the name of the client configuration for the repository. The implicit client +name is `default`, but a different client name can be specified in the +repository settings with the `client` key. -For example, if specifying the credentials file in the keystore under -`gcs.client.my_alternate_client.credentials_file`, you can configure a repository to use these -credentials like this: +NOTE: Passing the file path via the GOOGLE_APPLICATION_CREDENTIALS environment +variable is **not** supported. + +For example, if you added a `gcs.client.my_alternate_client.credentials_file` +setting in the keystore, you can configure a repository to use those credentials +like this: [source,js] ---- @@ -113,19 +115,18 @@ PUT _snapshot/my_gcs_repository // TEST[skip:we don't have gcs setup while testing this] The `credentials_file` settings are {ref}/secure-settings.html#reloadable-secure-settings[reloadable]. -After you reload the settings, the internal `gcs` clients, used to transfer the -snapshot contents, will utilize the latest settings from the keystore. - +After you reload the settings, the internal `gcs` clients, which are used to +transfer the snapshot contents, utilize the latest settings from the keystore. -NOTE: In progress snapshot/restore jobs will not be preempted by a *reload* -of the client's `credentials_file` settings. They will complete using the client -as it was built when the operation started. +NOTE: Snapshot or restore jobs that are in progress are not preempted by a *reload* +of the client's `credentials_file` settings. They complete using the client as +it was built when the operation started. [[repository-gcs-client]] ==== Client Settings The client used to connect to Google Cloud Storage has a number of settings available. -Client setting names are of the form `gcs.client.CLIENT_NAME.SETTING_NAME` and specified +Client setting names are of the form `gcs.client.CLIENT_NAME.SETTING_NAME` and are specified inside `elasticsearch.yml`. The default client name looked up by a `gcs` repository is called `default`, but can be customized with the repository setting `client`. @@ -146,7 +147,7 @@ PUT _snapshot/my_gcs_repository // TEST[skip:we don't have gcs setup while testing this] Some settings are sensitive and must be stored in the -{ref}/secure-settings.html[elasticsearch keystore]. This is the case for the service account file: +{ref}/secure-settings.html[Elasticsearch keystore]. This is the case for the service account file: [source,sh] ---- @@ -185,7 +186,7 @@ are marked as `Secure`. `project_id`:: - The Google Cloud project id. This will be automatically infered from the credentials file but + The Google Cloud project id. This will be automatically inferred from the credentials file but can be specified explicitly. For example, it can be used to switch between projects when the same credentials are usable for both the production and the development projects. @@ -248,8 +249,8 @@ The following settings are supported: The service account used to access the bucket must have the "Writer" access to the bucket: -1. Connect to the https://console.cloud.google.com/[Google Cloud Platform Console] -2. Select your project -3. Got to the https://console.cloud.google.com/storage/browser[Storage Browser] -4. Select the bucket and "Edit bucket permission" -5. The service account must be configured as a "User" with "Writer" access +1. Connect to the https://console.cloud.google.com/[Google Cloud Platform Console]. +2. Select your project. +3. Got to the https://console.cloud.google.com/storage/browser[Storage Browser]. +4. Select the bucket and "Edit bucket permission". +5. The service account must be configured as a "User" with "Writer" access. From 51cbc611353b50126c003c88a27303f0b9ddd80f Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 30 Aug 2018 09:38:23 +0100 Subject: [PATCH 233/283] Fix docs build after #33241 Recently-merged PR #33241 broke the docs build, and this fixes it. --- docs/reference/modules/discovery/zen.asciidoc | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/reference/modules/discovery/zen.asciidoc b/docs/reference/modules/discovery/zen.asciidoc index d90be42d9178..e9be7aa52e89 100644 --- a/docs/reference/modules/discovery/zen.asciidoc +++ b/docs/reference/modules/discovery/zen.asciidoc @@ -68,14 +68,14 @@ startup. To enable file-based discovery, configure the `file` hosts provider as follows: -``` +[source,txt] +---------------------------------------------------------------- discovery.zen.hosts_provider: file -``` +---------------------------------------------------------------- -Then create a file at `$ES_PATH_CONF/unicast_hosts.txt` in -<>. Any time a change is made -to the `unicast_hosts.txt` file the new changes will be picked up by -Elasticsearch and the new hosts list will be used. +Then create a file at `$ES_PATH_CONF/unicast_hosts.txt` in the format described +below. Any time a change is made to the `unicast_hosts.txt` file the new +changes will be picked up by Elasticsearch and the new hosts list will be used. Note that the file-based discovery plugin augments the unicast hosts list in `elasticsearch.yml`: if there are valid unicast host entries in @@ -86,10 +86,6 @@ The `discovery.zen.ping.unicast.resolve_timeout` setting also applies to DNS lookups for nodes specified by address via file-based discovery. This is specified as a <> and defaults to 5s. -[[discovery-file-format]] -[float] -====== unicast_hosts.txt file format - The format of the file is to specify one node entry per line. Each node entry consists of the host (host name or IP address) and an optional transport port number. If the port number is specified, is must come immediately after the From 5cf6e0d4bc9bde825c67207c75d7a9c0b6368e68 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Thu, 30 Aug 2018 11:41:39 +0300 Subject: [PATCH 234/283] Ignore module-info in jar hell checks (#33011) * Ignore module-info in JarHell checks * Add unit test * integration test to test that jarhell is ran with precommit --- buildSrc/build.gradle | 1 - .../gradle/precommit/JarHellTask.groovy | 8 +++- .../gradle/precommit/PrecommitTasks.groovy | 10 ++++- .../gradle/BuildExamplePluginsIT.java | 13 ------ ...portElasticsearchBuildResourcesTaskIT.java | 8 ++-- .../gradle/precommit/JarHellTaskIT.java | 42 +++++++++++++++++++ .../test/GradleIntegrationTestCase.java | 31 ++++++++++++-- buildSrc/src/testKit/jarHell/build.gradle | 29 +++++++++++++ .../java/org/apache/logging/log4j/Logger.java | 7 ++++ .../org/elasticsearch/bootstrap/JarHell.java | 4 ++ .../elasticsearch/bootstrap/JarHellTests.java | 22 ++++++++++ 11 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 buildSrc/src/test/java/org/elasticsearch/gradle/precommit/JarHellTaskIT.java create mode 100644 buildSrc/src/testKit/jarHell/build.gradle create mode 100644 buildSrc/src/testKit/jarHell/src/main/java/org/apache/logging/log4j/Logger.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 25d2a97302e9..dce14b10fcb8 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -176,7 +176,6 @@ if (project != rootProject) { it.tasks.matching { it.name == 'publishNebulaPublicationToLocalTestRepository'} } exclude "**/*Tests.class" - include "**/*IT.class" testClassesDirs = sourceSets.test.output.classesDirs classpath = sourceSets.test.runtimeClasspath inputs.dir(file("src/testKit")) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy index 4299efd95a38..119a02764994 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/JarHellTask.groovy @@ -22,8 +22,8 @@ package org.elasticsearch.gradle.precommit import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin import org.elasticsearch.gradle.LoggedExec import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.OutputFile - /** * Runs CheckJarHell on a classpath. */ @@ -35,9 +35,13 @@ public class JarHellTask extends LoggedExec { * inputs (ie the jars/class files). */ @OutputFile - File successMarker = new File(project.buildDir, 'markers/jarHell') + File successMarker + + @Classpath + FileCollection classpath public JarHellTask() { + successMarker = new File(project.buildDir, 'markers/jarHell-' + getName()) project.afterEvaluate { FileCollection classpath = project.sourceSets.test.runtimeClasspath if (project.plugins.hasPlugin(ShadowPlugin)) { diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index 60469622484e..be7561853bbb 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -31,7 +31,7 @@ class PrecommitTasks { /** Adds a precommit task, which depends on non-test verification tasks. */ public static Task create(Project project, boolean includeDependencyLicenses) { - Configuration forbiddenApisConfiguration = project.configurations.create("forbiddenApisCliJar") + project.configurations.create("forbiddenApisCliJar") project.dependencies { forbiddenApisCliJar ('de.thetaphi:forbiddenapis:2.5') } @@ -43,7 +43,7 @@ class PrecommitTasks { project.tasks.create('forbiddenPatterns', ForbiddenPatternsTask.class), project.tasks.create('licenseHeaders', LicenseHeadersTask.class), project.tasks.create('filepermissions', FilePermissionsTask.class), - project.tasks.create('jarHell', JarHellTask.class), + configureJarHell(project), configureThirdPartyAudit(project) ] @@ -80,6 +80,12 @@ class PrecommitTasks { return project.tasks.create(precommitOptions) } + private static Task configureJarHell(Project project) { + Task task = project.tasks.create('jarHell', JarHellTask.class) + task.classpath = project.sourceSets.test.runtimeClasspath + return task + } + private static Task configureThirdPartyAudit(Project project) { ThirdPartyAuditTask thirdPartyAuditTask = project.tasks.create('thirdPartyAudit', ThirdPartyAuditTask.class) ExportElasticsearchBuildResourcesTask buildResources = project.tasks.getByName('buildResources') diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java index 3e18b0b80af3..aca990670115 100644 --- a/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/BuildExamplePluginsIT.java @@ -153,17 +153,4 @@ private Path writeBuildScript(String script) { } } - private String getLocalTestRepoPath() { - String property = System.getProperty("test.local-test-repo-path"); - Objects.requireNonNull(property, "test.local-test-repo-path not passed to tests"); - File file = new File(property); - assertTrue("Expected " + property + " to exist, but it did not!", file.exists()); - if (File.separator.equals("\\")) { - // Use / on Windows too, the build script is not happy with \ - return file.getAbsolutePath().replace(File.separator, "/"); - } else { - return file.getAbsolutePath(); - } - } - } diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTaskIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTaskIT.java index 98fea2ea15ab..99afd0bcbe0a 100644 --- a/buildSrc/src/test/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTaskIT.java +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/ExportElasticsearchBuildResourcesTaskIT.java @@ -40,7 +40,7 @@ public void testUpToDateWithSourcesConfigured() { .withArguments("buildResources", "-s", "-i") .withPluginClasspath() .build(); - assertTaskSuccessfull(result, ":buildResources"); + assertTaskSuccessful(result, ":buildResources"); assertBuildFileExists(result, PROJECT_NAME, "build-tools-exported/checkstyle.xml"); assertBuildFileExists(result, PROJECT_NAME, "build-tools-exported/checkstyle_suppressions.xml"); @@ -61,8 +61,8 @@ public void testImplicitTaskDependencyCopy() { .withPluginClasspath() .build(); - assertTaskSuccessfull(result, ":buildResources"); - assertTaskSuccessfull(result, ":sampleCopyAll"); + assertTaskSuccessful(result, ":buildResources"); + assertTaskSuccessful(result, ":sampleCopyAll"); assertBuildFileExists(result, PROJECT_NAME, "sampleCopyAll/checkstyle.xml"); // This is a side effect of compile time reference assertBuildFileExists(result, PROJECT_NAME, "sampleCopyAll/checkstyle_suppressions.xml"); @@ -75,7 +75,7 @@ public void testImplicitTaskDependencyInputFileOfOther() { .withPluginClasspath() .build(); - assertTaskSuccessfull(result, ":sample"); + assertTaskSuccessful(result, ":sample"); assertBuildFileExists(result, PROJECT_NAME, "build-tools-exported/checkstyle.xml"); assertBuildFileExists(result, PROJECT_NAME, "build-tools-exported/checkstyle_suppressions.xml"); } diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/precommit/JarHellTaskIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/precommit/JarHellTaskIT.java new file mode 100644 index 000000000000..03f2022bc66e --- /dev/null +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/precommit/JarHellTaskIT.java @@ -0,0 +1,42 @@ +package org.elasticsearch.gradle.precommit; + +import org.elasticsearch.gradle.test.GradleIntegrationTestCase; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; + +/* + * 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. + */ +public class JarHellTaskIT extends GradleIntegrationTestCase { + + public void testJarHellDetected() { + BuildResult result = GradleRunner.create() + .withProjectDir(getProjectDir("jarHell")) + .withArguments("clean", "precommit", "-s", "-Dlocal.repo.path=" + getLocalTestRepoPath()) + .withPluginClasspath() + .buildAndFail(); + + assertTaskFailed(result, ":jarHell"); + assertOutputContains( + result.getOutput(), + "Exception in thread \"main\" java.lang.IllegalStateException: jar hell!", + "class: org.apache.logging.log4j.Logger" + ); + } + +} diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java b/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java index f00ab406a6c1..a1d4b86ab760 100644 --- a/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -66,15 +67,24 @@ protected void assertOutputDoesNotContain(String output, String... lines) { } } - protected void assertTaskSuccessfull(BuildResult result, String taskName) { + protected void assertTaskFailed(BuildResult result, String taskName) { + assertTaskOutcome(result, taskName, TaskOutcome.FAILED); + } + + protected void assertTaskSuccessful(BuildResult result, String taskName) { + assertTaskOutcome(result, taskName, TaskOutcome.SUCCESS); + } + + private void assertTaskOutcome(BuildResult result, String taskName, TaskOutcome taskOutcome) { BuildTask task = result.task(taskName); if (task == null) { - fail("Expected task `" + taskName + "` to be successful, but it did not run"); + fail("Expected task `" + taskName + "` to be " + taskOutcome +", but it did not run" + + "\n\nOutput is:\n" + result.getOutput()); } assertEquals( "Expected task to be successful but it was: " + task.getOutcome() + - "\n\nOutput is:\n" + result.getOutput() , - TaskOutcome.SUCCESS, + taskOutcome + "\n\nOutput is:\n" + result.getOutput() , + taskOutcome, task.getOutcome() ); } @@ -109,4 +119,17 @@ protected void assertBuildFileDoesNotExists(BuildResult result, String projectNa Files.exists(absPath) ); } + + protected String getLocalTestRepoPath() { + String property = System.getProperty("test.local-test-repo-path"); + Objects.requireNonNull(property, "test.local-test-repo-path not passed to tests"); + File file = new File(property); + assertTrue("Expected " + property + " to exist, but it did not!", file.exists()); + if (File.separator.equals("\\")) { + // Use / on Windows too, the build script is not happy with \ + return file.getAbsolutePath().replace(File.separator, "/"); + } else { + return file.getAbsolutePath(); + } + } } diff --git a/buildSrc/src/testKit/jarHell/build.gradle b/buildSrc/src/testKit/jarHell/build.gradle new file mode 100644 index 000000000000..17ff43fc7403 --- /dev/null +++ b/buildSrc/src/testKit/jarHell/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' + id 'elasticsearch.build' +} + +dependencyLicenses.enabled = false +dependenciesInfo.enabled = false +forbiddenApisMain.enabled = false +forbiddenApisTest.enabled = false +thirdPartyAudit.enabled = false +namingConventions.enabled = false +ext.licenseFile = file("$buildDir/dummy/license") +ext.noticeFile = file("$buildDir/dummy/notice") + +repositories { + mavenCentral() + repositories { + maven { + url System.getProperty("local.repo.path") + } + } +} + +dependencies { + // Needed for the JarHell task + testCompile ("org.elasticsearch.test:framework:${versions.elasticsearch}") + // causes jar hell with local sources + compile "org.apache.logging.log4j:log4j-api:${versions.log4j}" +} diff --git a/buildSrc/src/testKit/jarHell/src/main/java/org/apache/logging/log4j/Logger.java b/buildSrc/src/testKit/jarHell/src/main/java/org/apache/logging/log4j/Logger.java new file mode 100644 index 000000000000..a4332c664fa3 --- /dev/null +++ b/buildSrc/src/testKit/jarHell/src/main/java/org/apache/logging/log4j/Logger.java @@ -0,0 +1,7 @@ +package org.apache.logging.log4j; + +// Jar Hell ! +public class Logger { + +} + diff --git a/libs/core/src/main/java/org/elasticsearch/bootstrap/JarHell.java b/libs/core/src/main/java/org/elasticsearch/bootstrap/JarHell.java index e171daeb79b8..3de0ae5117e6 100644 --- a/libs/core/src/main/java/org/elasticsearch/bootstrap/JarHell.java +++ b/libs/core/src/main/java/org/elasticsearch/bootstrap/JarHell.java @@ -255,6 +255,10 @@ public static void checkJavaVersion(String resource, String targetVersion) { } private static void checkClass(Map clazzes, String clazz, Path jarpath) { + if (clazz.equals("module-info") || clazz.endsWith(".module-info")) { + // Ignore jigsaw module descriptions + return; + } Path previous = clazzes.put(clazz, jarpath); if (previous != null) { if (previous.equals(jarpath)) { diff --git a/libs/core/src/test/java/org/elasticsearch/bootstrap/JarHellTests.java b/libs/core/src/test/java/org/elasticsearch/bootstrap/JarHellTests.java index e58268ef1925..95c56f94ee4e 100644 --- a/libs/core/src/test/java/org/elasticsearch/bootstrap/JarHellTests.java +++ b/libs/core/src/test/java/org/elasticsearch/bootstrap/JarHellTests.java @@ -76,6 +76,28 @@ public void testDifferentJars() throws Exception { } } + public void testModuleInfo() throws Exception { + Path dir = createTempDir(); + JarHell.checkJarHell( + asSet( + makeJar(dir, "foo.jar", null, "module-info.class"), + makeJar(dir, "bar.jar", null, "module-info.class") + ), + logger::debug + ); + } + + public void testModuleInfoPackage() throws Exception { + Path dir = createTempDir(); + JarHell.checkJarHell( + asSet( + makeJar(dir, "foo.jar", null, "foo/bar/module-info.class"), + makeJar(dir, "bar.jar", null, "foo/bar/module-info.class") + ), + logger::debug + ); + } + public void testDirsOnClasspath() throws Exception { Path dir1 = createTempDir(); Path dir2 = createTempDir(); From b6f762d131455fa16a7c5699372eabc42a1e2b61 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Thu, 30 Aug 2018 10:53:01 +0200 Subject: [PATCH 235/283] Watcher: Ensure TriggerEngine start replaces existing watches (#33157) This commit ensures that when `TriggerService.start()` is called, we ensure in the trigger engine implementations that current watches are removed instead of adding to the existing ones in `TickerScheduleTriggerEngine.start()` Two additional minor fixes, where the result remains the same but less code gets executed. 1. If the node is not a data node, we forgot to set the status to STARTING when watcher is being started. This should not be a big issue, because a non-data node does not spent a lot of time loading as there are no watches which need loading. 2. If a new cluster state came in during a reload, we had two checks in place to abort loading the current one. The first one before we load all the watches of the local node and the second before watcher is starting with those new watches. Turned out that the first check was not returning, which meant we always tried to load all the watches, and then would fail on the second check. This has been fixed here. --- .../watcher/WatcherLifeCycleService.java | 1 + .../xpack/watcher/WatcherService.java | 10 +++-- .../engine/TickerScheduleTriggerEngine.java | 2 +- .../engine/TickerScheduleEngineTests.java | 44 ++++++++++++++++++- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java index fd46ce67bbe6..279d768fde81 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherLifeCycleService.java @@ -112,6 +112,7 @@ public void clusterChanged(ClusterChangedEvent event) { // if this is not a data node, we need to start it ourselves possibly if (event.state().nodes().getLocalNode().isDataNode() == false && isWatcherStoppedManually == false && this.state.get() == WatcherState.STOPPED) { + this.state.set(WatcherState.STARTING); watcherService.start(event.state(), () -> this.state.set(WatcherState.STARTED)); return; } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java index 49915674fe9e..599287bb50a7 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherService.java @@ -183,9 +183,6 @@ void reload(ClusterState state, String reason) { // by checking the cluster state version before and after loading the watches we can potentially just exit without applying the // changes processedClusterStateVersion.set(state.getVersion()); - triggerService.pauseExecution(); - int cancelledTaskCount = executionService.clearExecutionsAndQueue(); - logger.info("reloading watcher, reason [{}], cancelled [{}] queued tasks", reason, cancelledTaskCount); executor.execute(wrapWatcherService(() -> reloadInner(state, reason, false), e -> logger.error("error reloading watcher", e))); @@ -221,6 +218,7 @@ private synchronized boolean reloadInner(ClusterState state, String reason, bool if (processedClusterStateVersion.get() != state.getVersion()) { logger.debug("watch service has not been reloaded for state [{}], another reload for state [{}] in progress", state.getVersion(), processedClusterStateVersion.get()); + return false; } Collection watches = loadWatches(state); @@ -231,7 +229,13 @@ private synchronized boolean reloadInner(ClusterState state, String reason, bool // if we had another state coming in the meantime, we will not start the trigger engines with these watches, but wait // until the others are loaded + // also this is the place where we pause the trigger service execution and clear the current execution service, so that we make sure + // that existing executions finish, but no new ones are executed if (processedClusterStateVersion.get() == state.getVersion()) { + triggerService.pauseExecution(); + int cancelledTaskCount = executionService.clearExecutionsAndQueue(); + logger.info("reloading watcher, reason [{}], cancelled [{}] queued tasks", reason, cancelledTaskCount); + executionService.unPause(); triggerService.start(watches); if (triggeredWatches.isEmpty() == false) { diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleTriggerEngine.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleTriggerEngine.java index 05aa7cf30281..4c10f794880b 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleTriggerEngine.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleTriggerEngine.java @@ -56,7 +56,7 @@ public synchronized void start(Collection jobs) { schedules.put(job.id(), new ActiveSchedule(job.id(), trigger.getSchedule(), startTime)); } } - this.schedules.putAll(schedules); + this.schedules = schedules; } @Override diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleEngineTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleEngineTests.java index 7949998867b4..6680b38ab94b 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleEngineTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/trigger/schedule/engine/TickerScheduleEngineTests.java @@ -35,7 +35,9 @@ import static org.elasticsearch.xpack.watcher.trigger.schedule.Schedules.daily; import static org.elasticsearch.xpack.watcher.trigger.schedule.Schedules.interval; import static org.elasticsearch.xpack.watcher.trigger.schedule.Schedules.weekly; +import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; import static org.joda.time.DateTimeZone.UTC; import static org.mockito.Mockito.mock; @@ -50,8 +52,12 @@ public void init() throws Exception { } private TriggerEngine createEngine() { - return new TickerScheduleTriggerEngine(Settings.EMPTY, - mock(ScheduleRegistry.class), clock); + Settings settings = Settings.EMPTY; + // having a low value here speeds up the tests tremendously, we still want to run with the defaults every now and then + if (usually()) { + settings = Settings.builder().put(TickerScheduleTriggerEngine.TICKER_INTERVAL_SETTING.getKey(), "10ms").build(); + } + return new TickerScheduleTriggerEngine(settings, mock(ScheduleRegistry.class), clock); } private void advanceClockIfNeeded(DateTime newCurrentDateTime) { @@ -104,6 +110,40 @@ public void accept(Iterable events) { assertThat(bits.cardinality(), is(count)); } + public void testStartClearsExistingSchedules() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + List firedWatchIds = new ArrayList<>(); + engine.register(new Consumer>() { + @Override + public void accept(Iterable events) { + for (TriggerEvent event : events) { + firedWatchIds.add(event.jobName()); + } + latch.countDown(); + } + }); + + int count = randomIntBetween(2, 5); + List watches = new ArrayList<>(); + for (int i = 0; i < count; i++) { + watches.add(createWatch(String.valueOf(i), interval("1s"))); + } + engine.start(watches); + + watches.clear(); + for (int i = 0; i < count; i++) { + watches.add(createWatch("another_id" + i, interval("1s"))); + } + engine.start(watches); + + advanceClockIfNeeded(new DateTime(clock.millis(), UTC).plusMillis(1100)); + if (!latch.await(3 * count, TimeUnit.SECONDS)) { + fail("waiting too long for all watches to be triggered"); + } + + assertThat(firedWatchIds, everyItem(startsWith("another_id"))); + } + public void testAddHourly() throws Exception { final String name = "job_name"; final CountDownLatch latch = new CountDownLatch(1); From 557eabf7b5e8461f9566224dee6b23c6f8da266b Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 30 Aug 2018 13:59:19 +0300 Subject: [PATCH 236/283] [DOCS] TLS file resources are reloadable (#33258) Make clearer that file resources that are used as key trust material are polled and will be reloaded upon modification. --- .../securing-communications/tls-http.asciidoc | 12 +++++++++++- .../securing-communications/tls-transport.asciidoc | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/x-pack/docs/en/security/securing-communications/tls-http.asciidoc b/x-pack/docs/en/security/securing-communications/tls-http.asciidoc index eb8e985a65b5..06e70b036735 100644 --- a/x-pack/docs/en/security/securing-communications/tls-http.asciidoc +++ b/x-pack/docs/en/security/securing-communications/tls-http.asciidoc @@ -77,7 +77,17 @@ bin/elasticsearch-keystore add xpack.security.http.ssl.secure_key_passphrase . Restart {es}. -NOTE: All TLS-related node settings are considered to be highly sensitive and +[NOTE] +=============================== +* All TLS-related node settings are considered to be highly sensitive and therefore are not exposed via the {ref}/cluster-nodes-info.html#cluster-nodes-info[nodes info API] For more information about any of these settings, see <>. + +* {es} monitors all files such as certificates, keys, keystores, or truststores +that are configured as values of TLS-related node settings. If you update any of +these files (for example, when your hostnames change or your certificates are +due to expire), {es} reloads them. The files are polled for changes at +a frequency determined by the global {es} `resource.reload.interval.high` +setting, which defaults to 5 seconds. +=============================== diff --git a/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc b/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc index c186aebbe243..c2306545536a 100644 --- a/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc +++ b/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc @@ -95,7 +95,17 @@ vice-versa). After enabling TLS you must restart all nodes in order to maintain communication across the cluster. -- -NOTE: All TLS-related node settings are considered to be highly sensitive and +[NOTE] +=============================== +* All TLS-related node settings are considered to be highly sensitive and therefore are not exposed via the {ref}/cluster-nodes-info.html#cluster-nodes-info[nodes info API] For more information about any of these settings, see <>. + +* {es} monitors all files such as certificates, keys, keystores, or truststores +that are configured as values of TLS-related node settings. If you update any of +these files (for example, when your hostnames change or your certificates are +due to expire), {es} reloads them. The files are polled for changes at +a frequency determined by the global {es} `resource.reload.interval.high` +setting, which defaults to 5 seconds. +=============================== From 13261996cee0225f7ac1191317e29b2c3d155841 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 30 Aug 2018 07:55:13 -0400 Subject: [PATCH 237/283] Add NoOps to Lucene for failed delete ops (#33217) Today we add a NoOp to Lucene and translog if we fail to process an indexing operation. However, we are only adding NoOps to translog for delete operations. In order to have a complete history in Lucene, we should add NoOps of failed delete operations to both Lucene and translog. Relates #29530 --- .../org/elasticsearch/index/engine/InternalEngine.java | 8 +++++--- .../index/replication/IndexLevelReplicationTests.java | 6 ++++++ 2 files changed, 11 insertions(+), 3 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 b31328579ecf..f11f1f296f67 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -844,7 +844,7 @@ public IndexResult index(Index index) throws IOException { if (indexResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Index(index, indexResult)); } else if (indexResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - // if we have document failure, record it as a no-op in the translog with the generated seq_no + // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no final NoOp noOp = new NoOp(indexResult.getSeqNo(), index.primaryTerm(), index.origin(), index.startTime(), indexResult.getFailure().toString()); location = innerNoOp(noOp).getTranslogLocation(); @@ -1182,8 +1182,10 @@ public DeleteResult delete(Delete delete) throws IOException { if (deleteResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Delete(delete, deleteResult)); } else if (deleteResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - location = translog.add(new Translog.NoOp(deleteResult.getSeqNo(), - delete.primaryTerm(), deleteResult.getFailure().toString())); + // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no + final NoOp noOp = new NoOp(deleteResult.getSeqNo(), delete.primaryTerm(), delete.origin(), + delete.startTime(), deleteResult.getFailure().toString()); + location = innerNoOp(noOp).getTranslogLocation(); } else { location = null; } 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 6b43021b169b..fba71dd1e529 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java @@ -404,6 +404,9 @@ public long softUpdateDocument(Term term, Iterable doc try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } + try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } } // unlike previous failures, these two failures replicated directly from the replication channel. indexResp = shards.index(new IndexRequest(index.getName(), "type", "any").source("{}", XContentType.JSON)); @@ -418,6 +421,9 @@ public long softUpdateDocument(Term term, Iterable doc try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } + try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } } shards.assertAllEqual(1); } From 1404dd2a42c88fe72159587429f59f96251e6d41 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Thu, 30 Aug 2018 15:15:50 +0200 Subject: [PATCH 238/283] Fix nested _source retrieval with includes/excludes (#33180) If an exclude or an include clause removes an entry to a nested field in the original source at query time, the creation of nested hits fails with an NPE. This change fixes this exception and replaces the nested document source with an empty map. Closes #33163 Closes #33170 --- .../fetch/subphase/FetchSourceSubPhase.java | 4 ++ .../subphase/FetchSourceSubPhaseTests.java | 40 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhase.java index 2da74c56f6a3..a7f333abfa2e 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhase.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhase.java @@ -57,6 +57,7 @@ public void hitExecute(SearchContext context, HitContext hitContext) { if (nestedHit) { value = getNestedSource((Map) value, hitContext); } + try { final int initialCapacity = nestedHit ? 1024 : Math.min(1024, source.internalSourceRef().length()); BytesStreamOutput streamOutput = new BytesStreamOutput(initialCapacity); @@ -81,6 +82,9 @@ public void hitExecute(SearchContext context, HitContext hitContext) { private Map getNestedSource(Map sourceAsMap, HitContext hitContext) { for (SearchHit.NestedIdentity o = hitContext.hit().getNestedIdentity(); o != null; o = o.getChild()) { sourceAsMap = (Map) sourceAsMap.get(o.getField().string()); + if (sourceAsMap == null) { + return null; + } } return sourceAsMap; } diff --git a/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhaseTests.java b/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhaseTests.java index 5cc4e2ddc68a..7790e8d6576c 100644 --- a/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhaseTests.java +++ b/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourceSubPhaseTests.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.util.Collections; +import java.util.Map; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -78,6 +79,29 @@ public void testMultipleFiltering() throws IOException { assertEquals(Collections.singletonMap("field","value"), hitContext.hit().getSourceAsMap()); } + public void testNestedSource() throws IOException { + Map expectedNested = Collections.singletonMap("nested2", Collections.singletonMap("field", "value0")); + XContentBuilder source = XContentFactory.jsonBuilder().startObject() + .field("field", "value") + .field("field2", "value2") + .field("nested1", expectedNested) + .endObject(); + FetchSubPhase.HitContext hitContext = hitExecuteMultiple(source, true, null, null, + new SearchHit.NestedIdentity("nested1", 0,null)); + assertEquals(expectedNested, hitContext.hit().getSourceAsMap()); + hitContext = hitExecuteMultiple(source, true, new String[]{"invalid"}, null, + new SearchHit.NestedIdentity("nested1", 0,null)); + assertEquals(Collections.emptyMap(), hitContext.hit().getSourceAsMap()); + + hitContext = hitExecuteMultiple(source, true, null, null, + new SearchHit.NestedIdentity("nested1", 0, new SearchHit.NestedIdentity("nested2", 0, null))); + assertEquals(Collections.singletonMap("field", "value0"), hitContext.hit().getSourceAsMap()); + + hitContext = hitExecuteMultiple(source, true, new String[]{"invalid"}, null, + new SearchHit.NestedIdentity("nested1", 0, new SearchHit.NestedIdentity("nested2", 0, null))); + assertEquals(Collections.emptyMap(), hitContext.hit().getSourceAsMap()); + } + public void testSourceDisabled() throws IOException { FetchSubPhase.HitContext hitContext = hitExecute(null, true, null, null); assertNull(hitContext.hit().getSourceAsMap()); @@ -96,17 +120,29 @@ public void testSourceDisabled() throws IOException { } private FetchSubPhase.HitContext hitExecute(XContentBuilder source, boolean fetchSource, String include, String exclude) { + return hitExecute(source, fetchSource, include, exclude, null); + } + + + private FetchSubPhase.HitContext hitExecute(XContentBuilder source, boolean fetchSource, String include, String exclude, + SearchHit.NestedIdentity nestedIdentity) { return hitExecuteMultiple(source, fetchSource, include == null ? Strings.EMPTY_ARRAY : new String[]{include}, - exclude == null ? Strings.EMPTY_ARRAY : new String[]{exclude}); + exclude == null ? Strings.EMPTY_ARRAY : new String[]{exclude}, nestedIdentity); } private FetchSubPhase.HitContext hitExecuteMultiple(XContentBuilder source, boolean fetchSource, String[] includes, String[] excludes) { + return hitExecuteMultiple(source, fetchSource, includes, excludes, null); + } + + private FetchSubPhase.HitContext hitExecuteMultiple(XContentBuilder source, boolean fetchSource, String[] includes, String[] excludes, + SearchHit.NestedIdentity nestedIdentity) { FetchSourceContext fetchSourceContext = new FetchSourceContext(fetchSource, includes, excludes); SearchContext searchContext = new FetchSourceSubPhaseTestSearchContext(fetchSourceContext, source == null ? null : BytesReference.bytes(source)); FetchSubPhase.HitContext hitContext = new FetchSubPhase.HitContext(); - hitContext.reset(new SearchHit(1, null, null, null), null, 1, null); + final SearchHit searchHit = new SearchHit(1, null, null, nestedIdentity, null); + hitContext.reset(searchHit, null, 1, null); FetchSourceSubPhase phase = new FetchSourceSubPhase(); phase.hitExecute(searchContext, hitContext); return hitContext; From d0630093cdeada66904e648824a42f5e4950b895 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Thu, 30 Aug 2018 18:07:58 +0200 Subject: [PATCH 239/283] Fix serialization of empty field capabilities response (#33263) Fix serialization of empty field capabilities response When no response are required (no indices match the requested patterns) the empty response throws an NPE in the transport serialization (writeTo). --- .../action/fieldcaps/FieldCapabilities.java | 4 +- .../fieldcaps/FieldCapabilitiesRequest.java | 1 - .../fieldcaps/FieldCapabilitiesResponse.java | 8 +-- .../TransportFieldCapabilitiesAction.java | 2 +- .../java/org/elasticsearch/client/Client.java | 2 +- .../client/support/AbstractClient.java | 4 +- .../FieldCapabilitiesResponseTests.java | 53 +++++++++++++++---- 7 files changed, 54 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java index 21bb452430e7..5cfdba929463 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java @@ -166,14 +166,14 @@ public String getName() { } /** - * Whether this field is indexed for search on all indices. + * Whether this field can be aggregated on all indices. */ public boolean isAggregatable() { return isAggregatable; } /** - * Whether this field can be aggregated on all indices. + * Whether this field is indexed for search on all indices. */ public boolean isSearchable() { return isSearchable; diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java index 22d231d3711b..e9e77df5f903 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -111,7 +111,6 @@ public String[] fields() { } /** - * * The list of indices to lookup */ public FieldCapabilitiesRequest indices(String... indices) { diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java index 178639bd4348..f908ec7b1b28 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java @@ -35,6 +35,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -56,15 +57,15 @@ public class FieldCapabilitiesResponse extends ActionResponse implements ToXCont private FieldCapabilitiesResponse(Map> responseMap, List indexResponses) { - this.responseMap = responseMap; - this.indexResponses = indexResponses; + this.responseMap = Objects.requireNonNull(responseMap); + this.indexResponses = Objects.requireNonNull(indexResponses); } /** * Used for serialization */ FieldCapabilitiesResponse() { - this.responseMap = Collections.emptyMap(); + this(Collections.emptyMap(), Collections.emptyList()); } /** @@ -81,6 +82,7 @@ public Map> get() { List getIndexResponses() { return indexResponses; } + /** * * Get the field capabilities per type for the provided {@code field}. diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index ef0d19a26558..b8d1f477ac10 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -90,7 +90,7 @@ protected void doExecute(Task task, FieldCapabilitiesRequest request, final Acti } }; if (totalNumRequest == 0) { - listener.onResponse(new FieldCapabilitiesResponse()); + listener.onResponse(new FieldCapabilitiesResponse(Collections.emptyMap())); } else { ActionListener innerListener = new ActionListener() { @Override diff --git a/server/src/main/java/org/elasticsearch/client/Client.java b/server/src/main/java/org/elasticsearch/client/Client.java index adb2f509b999..f97f618347af 100644 --- a/server/src/main/java/org/elasticsearch/client/Client.java +++ b/server/src/main/java/org/elasticsearch/client/Client.java @@ -455,7 +455,7 @@ public interface Client extends ElasticsearchClient, Releasable { /** * Builder for the field capabilities request. */ - FieldCapabilitiesRequestBuilder prepareFieldCaps(); + FieldCapabilitiesRequestBuilder prepareFieldCaps(String... indices); /** * An action that returns the field capabilities from the provided request diff --git a/server/src/main/java/org/elasticsearch/client/support/AbstractClient.java b/server/src/main/java/org/elasticsearch/client/support/AbstractClient.java index 86d9d2c445f3..553c92e6de86 100644 --- a/server/src/main/java/org/elasticsearch/client/support/AbstractClient.java +++ b/server/src/main/java/org/elasticsearch/client/support/AbstractClient.java @@ -651,8 +651,8 @@ public ActionFuture fieldCaps(FieldCapabilitiesReques } @Override - public FieldCapabilitiesRequestBuilder prepareFieldCaps() { - return new FieldCapabilitiesRequestBuilder(this, FieldCapabilitiesAction.INSTANCE); + public FieldCapabilitiesRequestBuilder prepareFieldCaps(String... indices) { + return new FieldCapabilitiesRequestBuilder(this, FieldCapabilitiesAction.INSTANCE, indices); } static class Admin implements AdminClient { diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java index b38240632421..90b730660ddd 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java @@ -28,11 +28,15 @@ import org.elasticsearch.test.AbstractStreamableXContentTestCase; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Predicate; +import static com.carrotsearch.randomizedtesting.RandomizedTest.randomAsciiLettersOfLength; + public class FieldCapabilitiesResponseTests extends AbstractStreamableXContentTestCase { @@ -48,22 +52,46 @@ protected FieldCapabilitiesResponse createBlankInstance() { @Override protected FieldCapabilitiesResponse createTestInstance() { - Map> responses = new HashMap<>(); + if (randomBoolean()) { + // merged responses + Map> responses = new HashMap<>(); + + String[] fields = generateRandomStringArray(5, 10, false, true); + assertNotNull(fields); + + for (String field : fields) { + Map typesToCapabilities = new HashMap<>(); + String[] types = generateRandomStringArray(5, 10, false, false); + assertNotNull(types); + + for (String type : types) { + typesToCapabilities.put(type, FieldCapabilitiesTests.randomFieldCaps(field)); + } + responses.put(field, typesToCapabilities); + } + return new FieldCapabilitiesResponse(responses); + } else { + // non-merged responses + List responses = new ArrayList<>(); + int numResponse = randomIntBetween(0, 10); + for (int i = 0; i < numResponse; i++) { + responses.add(createRandomIndexResponse()); + } + return new FieldCapabilitiesResponse(responses); + } + } + + + private FieldCapabilitiesIndexResponse createRandomIndexResponse() { + Map responses = new HashMap<>(); String[] fields = generateRandomStringArray(5, 10, false, true); assertNotNull(fields); for (String field : fields) { - Map typesToCapabilities = new HashMap<>(); - String[] types = generateRandomStringArray(5, 10, false, false); - assertNotNull(types); - - for (String type : types) { - typesToCapabilities.put(type, FieldCapabilitiesTests.randomFieldCaps(field)); - } - responses.put(field, typesToCapabilities); + responses.put(field, FieldCapabilitiesTests.randomFieldCaps(field)); } - return new FieldCapabilitiesResponse(responses); + return new FieldCapabilitiesIndexResponse(randomAsciiLettersOfLength(10), responses); } @Override @@ -138,6 +166,11 @@ public void testToXContent() throws IOException { "}").replaceAll("\\s+", ""), generatedResponse); } + public void testEmptyResponse() throws IOException { + FieldCapabilitiesResponse testInstance = new FieldCapabilitiesResponse(); + assertSerialization(testInstance); + } + private static FieldCapabilitiesResponse createSimpleResponse() { Map titleCapabilities = new HashMap<>(); titleCapabilities.put("text", new FieldCapabilities("title", "text", true, false)); From 07faa0b06a7d8ed8cd84ef234692c4413994e088 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 30 Aug 2018 14:26:32 -0400 Subject: [PATCH 240/283] TEST: Mute testMonitorClusterHealth Tracked at #32299 --- .../org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java b/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java index f56f96efc788..f7ecb6d58e52 100644 --- a/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java +++ b/x-pack/qa/smoke-test-watcher/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherTestSuiteIT.java @@ -106,6 +106,7 @@ protected Settings restAdminSettings() { return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32299") public void testMonitorClusterHealth() throws Exception { String watchId = "cluster_health_watch"; From af2eaf2a6c93aa80671b9e89d583509f75644dcd Mon Sep 17 00:00:00 2001 From: Simon Willnauer Date: Thu, 30 Aug 2018 21:08:35 +0200 Subject: [PATCH 241/283] Remove usage of `index.shrink.source.*` in 7.x (#33271) We cut over to `index.resize.source.*` but still have these constants being public in `IndexMetaData`. Those Settings and constants are not needed in 7.x while we still need to keep the keys known to private settings since they might be part of the index settings of old indices. We can remove that in 8.0. Yet, we should remove the settings to make sure they are not used again. --- .../cluster/metadata/IndexMetaData.java | 16 ++++------------ .../metadata/MetaDataCreateIndexService.java | 5 +---- .../common/settings/IndexScopedSettings.java | 6 ++++-- .../decider/DiskThresholdDeciderUnitTests.java | 15 +++++++-------- .../decider/FilterAllocationDeciderTests.java | 8 ++++---- 5 files changed, 20 insertions(+), 30 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index 11c489f63abc..117f663398f6 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -485,22 +485,14 @@ public MappingMetaData mapping(String mappingType) { return mappings.get(mappingType); } - // we keep the shrink settings for BWC - this can be removed in 8.0 - // we can't remove in 7 since this setting might be baked into an index coming in via a full cluster restart from 6.0 - public static final String INDEX_SHRINK_SOURCE_UUID_KEY = "index.shrink.source.uuid"; - public static final String INDEX_SHRINK_SOURCE_NAME_KEY = "index.shrink.source.name"; public static final String INDEX_RESIZE_SOURCE_UUID_KEY = "index.resize.source.uuid"; public static final String INDEX_RESIZE_SOURCE_NAME_KEY = "index.resize.source.name"; - public static final Setting INDEX_SHRINK_SOURCE_UUID = Setting.simpleString(INDEX_SHRINK_SOURCE_UUID_KEY); - public static final Setting INDEX_SHRINK_SOURCE_NAME = Setting.simpleString(INDEX_SHRINK_SOURCE_NAME_KEY); - public static final Setting INDEX_RESIZE_SOURCE_UUID = Setting.simpleString(INDEX_RESIZE_SOURCE_UUID_KEY, - INDEX_SHRINK_SOURCE_UUID); - public static final Setting INDEX_RESIZE_SOURCE_NAME = Setting.simpleString(INDEX_RESIZE_SOURCE_NAME_KEY, - INDEX_SHRINK_SOURCE_NAME); + public static final Setting INDEX_RESIZE_SOURCE_UUID = Setting.simpleString(INDEX_RESIZE_SOURCE_UUID_KEY); + public static final Setting INDEX_RESIZE_SOURCE_NAME = Setting.simpleString(INDEX_RESIZE_SOURCE_NAME_KEY); public Index getResizeSourceIndex() { - return INDEX_RESIZE_SOURCE_UUID.exists(settings) || INDEX_SHRINK_SOURCE_UUID.exists(settings) - ? new Index(INDEX_RESIZE_SOURCE_NAME.get(settings), INDEX_RESIZE_SOURCE_UUID.get(settings)) : null; + return INDEX_RESIZE_SOURCE_UUID.exists(settings) ? new Index(INDEX_RESIZE_SOURCE_NAME.get(settings), + INDEX_RESIZE_SOURCE_UUID.get(settings)) : null; } /** diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java index b19d65090c6b..c07061cde97a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java @@ -723,10 +723,7 @@ static void prepareResizeIndexSettings( .put(IndexMetaData.INDEX_ROUTING_INITIAL_RECOVERY_GROUP_SETTING.getKey() + "_id", Strings.arrayToCommaDelimitedString(nodesToAllocateOn.toArray())) // we only try once and then give up with a shrink index - .put("index.allocation.max_retries", 1) - // we add the legacy way of specifying it here for BWC. We can remove this once it's backported to 6.x - .put(IndexMetaData.INDEX_SHRINK_SOURCE_NAME.getKey(), resizeSourceIndex.getName()) - .put(IndexMetaData.INDEX_SHRINK_SOURCE_UUID.getKey(), resizeSourceIndex.getUUID()); + .put("index.allocation.max_retries", 1); } else if (type == ResizeType.SPLIT) { validateSplitIndex(currentState, resizeSourceIndex.getName(), mappingKeys, resizeIntoName, indexSettingsBuilder.build()); } else { diff --git a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java index 137378f509d6..46e3867f7aea 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java @@ -202,8 +202,10 @@ public boolean isPrivateSetting(String key) { case IndexMetaData.SETTING_VERSION_UPGRADED: case IndexMetaData.SETTING_INDEX_PROVIDED_NAME: case MergePolicyConfig.INDEX_MERGE_ENABLED: - case IndexMetaData.INDEX_SHRINK_SOURCE_UUID_KEY: - case IndexMetaData.INDEX_SHRINK_SOURCE_NAME_KEY: + // we keep the shrink settings for BWC - this can be removed in 8.0 + // we can't remove in 7 since this setting might be baked into an index coming in via a full cluster restart from 6.0 + case "index.shrink.source.uuid": + case "index.shrink.source.name": case IndexMetaData.INDEX_RESIZE_SOURCE_UUID_KEY: case IndexMetaData.INDEX_RESIZE_SOURCE_NAME_KEY: return true; diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderUnitTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderUnitTests.java index 10fc358e4d4e..da0e0a9b0bcf 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderUnitTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderUnitTests.java @@ -286,16 +286,19 @@ public void testSizeShrinkIndex() { metaBuilder.put(IndexMetaData.builder("test").settings(settings(Version.CURRENT).put("index.uuid", "1234")) .numberOfShards(4).numberOfReplicas(0)); metaBuilder.put(IndexMetaData.builder("target").settings(settings(Version.CURRENT).put("index.uuid", "5678") - .put("index.shrink.source.name", "test").put("index.shrink.source.uuid", "1234")).numberOfShards(1).numberOfReplicas(0)); + .put(IndexMetaData.INDEX_RESIZE_SOURCE_NAME_KEY, "test").put(IndexMetaData.INDEX_RESIZE_SOURCE_UUID_KEY, "1234")) + .numberOfShards(1) + .numberOfReplicas(0)); metaBuilder.put(IndexMetaData.builder("target2").settings(settings(Version.CURRENT).put("index.uuid", "9101112") - .put("index.shrink.source.name", "test").put("index.shrink.source.uuid", "1234")).numberOfShards(2).numberOfReplicas(0)); + .put(IndexMetaData.INDEX_RESIZE_SOURCE_NAME_KEY, "test").put(IndexMetaData.INDEX_RESIZE_SOURCE_UUID_KEY, "1234")) + .numberOfShards(2).numberOfReplicas(0)); MetaData metaData = metaBuilder.build(); RoutingTable.Builder routingTableBuilder = RoutingTable.builder(); routingTableBuilder.addAsNew(metaData.index("test")); routingTableBuilder.addAsNew(metaData.index("target")); routingTableBuilder.addAsNew(metaData.index("target2")); - ClusterState clusterState = ClusterState.builder(org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY)) - .metaData(metaData).routingTable(routingTableBuilder.build()).build(); + ClusterState clusterState = ClusterState.builder(org.elasticsearch.cluster.ClusterName.CLUSTER_NAME_SETTING + .getDefault(Settings.EMPTY)).metaData(metaData).routingTable(routingTableBuilder.build()).build(); AllocationService allocationService = createAllocationService(); clusterState = ClusterState.builder(clusterState).nodes(DiscoveryNodes.builder().add(newNode("node1"))) @@ -330,7 +333,6 @@ public void testSizeShrinkIndex() { assertEquals(100L, DiskThresholdDecider.getExpectedShardSize(test_1, allocation, 0)); assertEquals(10L, DiskThresholdDecider.getExpectedShardSize(test_0, allocation, 0)); - ShardRouting target = ShardRouting.newUnassigned(new ShardId(new Index("target", "5678"), 0), true, LocalShardsRecoverySource.INSTANCE, new UnassignedInfo(UnassignedInfo.Reason.INDEX_CREATED, "foo")); assertEquals(1110L, DiskThresholdDecider.getExpectedShardSize(target, allocation, 0)); @@ -350,12 +352,9 @@ public void testSizeShrinkIndex() { .build(); allocationService.reroute(clusterState, "foo"); - RoutingAllocation allocationWithMissingSourceIndex = new RoutingAllocation(null, clusterStateWithMissingSourceIndex.getRoutingNodes(), clusterStateWithMissingSourceIndex, info, 0); - assertEquals(42L, DiskThresholdDecider.getExpectedShardSize(target, allocationWithMissingSourceIndex, 42L)); assertEquals(42L, DiskThresholdDecider.getExpectedShardSize(target2, allocationWithMissingSourceIndex, 42L)); } - } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/FilterAllocationDeciderTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/FilterAllocationDeciderTests.java index b1fa8346e2c5..ba6fe5b9a5a3 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/FilterAllocationDeciderTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/FilterAllocationDeciderTests.java @@ -42,8 +42,8 @@ import java.util.Arrays; import java.util.Collections; -import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_SHRINK_SOURCE_NAME; -import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_SHRINK_SOURCE_UUID; +import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_RESIZE_SOURCE_NAME; +import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_RESIZE_SOURCE_UUID; import static org.elasticsearch.cluster.routing.ShardRoutingState.INITIALIZING; import static org.elasticsearch.cluster.routing.ShardRoutingState.STARTED; import static org.elasticsearch.cluster.routing.ShardRoutingState.UNASSIGNED; @@ -151,8 +151,8 @@ private ClusterState createInitialClusterState(AllocationService service, Settin .putInSyncAllocationIds(1, Collections.singleton("aid1")) .build(); metaData.put(sourceIndex, false); - indexSettings.put(INDEX_SHRINK_SOURCE_UUID.getKey(), sourceIndex.getIndexUUID()); - indexSettings.put(INDEX_SHRINK_SOURCE_NAME.getKey(), sourceIndex.getIndex().getName()); + indexSettings.put(INDEX_RESIZE_SOURCE_UUID.getKey(), sourceIndex.getIndexUUID()); + indexSettings.put(INDEX_RESIZE_SOURCE_NAME.getKey(), sourceIndex.getIndex().getName()); } else { sourceIndex = null; } From 001b78f704d25d01d4c28a4a756c536321c8dc8a Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 31 Aug 2018 01:27:00 +0530 Subject: [PATCH 242/283] Replace IndexMetaData.Custom with Map-based custom metadata (#32749) This PR removes the deprecated `Custom` class in `IndexMetaData`, in favor of a `Map` that is used to store custom index metadata. As part of this, there is now no way to set this metadata in a template or create index request (since it's only set by plugins, or dedicated REST endpoints). The `Map` is intended to be a namespaced `Map` (`DiffableStringMap` implements `Map`, so the signature is more like `Map>`). This is so we can do things like: ``` java Map ccrMeta = indexMetaData.getCustom("ccr"); ``` And then have complete control over the metadata. This also means any plugin/feature that uses this has to manage its own BWC, as the map is just serialized as a map. It also means that if metadata is put in the map that isn't used (for instance, if a plugin were removed), it causes no failures the way an unregistered `Setting` would. The reason I use a custom `DiffableStringMap` here rather than a plain `Map` is so the map can be diffed with previous cluster state updates for serialization. Supersedes #32683 --- .../CreateIndexClusterStateUpdateRequest.java | 11 - .../indices/create/CreateIndexRequest.java | 53 ++--- .../create/CreateIndexRequestBuilder.java | 9 - .../create/TransportCreateIndexAction.java | 2 +- .../indices/shrink/TransportResizeAction.java | 1 - .../template/put/PutIndexTemplateRequest.java | 46 +---- .../put/TransportPutIndexTemplateAction.java | 1 - .../cluster/metadata/DiffableStringMap.java | 188 ++++++++++++++++++ .../cluster/metadata/IndexMetaData.java | 153 +++++--------- .../metadata/IndexTemplateMetaData.java | 79 ++------ .../metadata/MetaDataCreateIndexService.java | 21 +- .../MetaDataIndexTemplateService.java | 9 - .../metadata/DiffableStringMapTests.java | 103 ++++++++++ .../metadata/IndexCreationTaskTests.java | 41 +--- .../cluster/metadata/IndexMetaDataTests.java | 18 +- .../metadata/IndexTemplateMetaDataTests.java | 4 +- .../authz/store/NativeRolesStoreTests.java | 2 +- 17 files changed, 406 insertions(+), 335 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/cluster/metadata/DiffableStringMap.java create mode 100644 server/src/test/java/org/elasticsearch/cluster/metadata/DiffableStringMapTests.java diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java index f2e07e29bad6..25f7f33647c2 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java @@ -55,8 +55,6 @@ public class CreateIndexClusterStateUpdateRequest extends ClusterStateUpdateRequ private final Set aliases = new HashSet<>(); - private final Map customs = new HashMap<>(); - private final Set blocks = new HashSet<>(); private ActiveShardCount waitForActiveShards = ActiveShardCount.DEFAULT; @@ -83,11 +81,6 @@ public CreateIndexClusterStateUpdateRequest aliases(Set aliases) { return this; } - public CreateIndexClusterStateUpdateRequest customs(Map customs) { - this.customs.putAll(customs); - return this; - } - public CreateIndexClusterStateUpdateRequest blocks(Set blocks) { this.blocks.addAll(blocks); return this; @@ -146,10 +139,6 @@ public Set aliases() { return aliases; } - public Map customs() { - return customs; - } - public Set blocks() { return blocks; } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java index 875d17eb54bc..a186f9b50112 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java @@ -29,7 +29,6 @@ import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.master.AcknowledgedRequest; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -58,9 +57,9 @@ import java.util.Set; import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; import static org.elasticsearch.common.settings.Settings.readSettingsFromStream; import static org.elasticsearch.common.settings.Settings.writeSettingsToStream; -import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; /** * A request to create an index. Best created with {@link org.elasticsearch.client.Requests#createIndexRequest(String)}. @@ -87,8 +86,6 @@ public class CreateIndexRequest extends AcknowledgedRequest private final Set aliases = new HashSet<>(); - private final Map customs = new HashMap<>(); - private ActiveShardCount waitForActiveShards = ActiveShardCount.DEFAULT; public CreateIndexRequest() { @@ -388,18 +385,7 @@ public CreateIndexRequest source(Map source, DeprecationHandler depre } else if (ALIASES.match(name, deprecationHandler)) { aliases((Map) entry.getValue()); } else { - // maybe custom? - IndexMetaData.Custom proto = IndexMetaData.lookupPrototype(name); - if (proto != null) { - try { - customs.put(name, proto.fromMap((Map) entry.getValue())); - } catch (IOException e) { - throw new ElasticsearchParseException("failed to parse custom metadata for [{}]", name); - } - } else { - // found a key which is neither custom defined nor one of the supported ones - throw new ElasticsearchParseException("unknown key [{}] for create index", name); - } + throw new ElasticsearchParseException("unknown key [{}] for create index", name); } } return this; @@ -413,18 +399,6 @@ public Set aliases() { return this.aliases; } - /** - * Adds custom metadata to the index to be created. - */ - public CreateIndexRequest custom(IndexMetaData.Custom custom) { - customs.put(custom.type(), custom); - return this; - } - - public Map customs() { - return this.customs; - } - public ActiveShardCount waitForActiveShards() { return waitForActiveShards; } @@ -474,11 +448,13 @@ public void readFrom(StreamInput in) throws IOException { } mappings.put(type, source); } - int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - IndexMetaData.Custom customIndexMetaData = IndexMetaData.lookupPrototypeSafe(type).readFrom(in); - customs.put(type, customIndexMetaData); + if (in.getVersion().before(Version.V_7_0_0_alpha1)) { + // This used to be the size of custom metadata classes + int customSize = in.readVInt(); + assert customSize == 0 : "unexpected custom metadata when none is supported"; + if (customSize > 0) { + throw new IllegalStateException("unexpected custom metadata when none is supported"); + } } int aliasesSize = in.readVInt(); for (int i = 0; i < aliasesSize; i++) { @@ -501,10 +477,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(entry.getKey()); out.writeString(entry.getValue()); } - out.writeVInt(customs.size()); - for (Map.Entry entry : customs.entrySet()) { - out.writeString(entry.getKey()); - entry.getValue().writeTo(out); + if (out.getVersion().before(Version.V_7_0_0_alpha1)) { + // Size of custom index metadata, which is removed + out.writeVInt(0); } out.writeVInt(aliases.size()); for (Alias alias : aliases) { @@ -542,10 +517,6 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t alias.toXContent(builder, params); } builder.endObject(); - - for (Map.Entry entry : customs.entrySet()) { - builder.field(entry.getKey(), entry.getValue(), params); - } return builder; } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java index cc8fb2c32c37..d2593e7e94be 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestBuilder.java @@ -23,7 +23,6 @@ import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.action.support.master.AcknowledgedRequestBuilder; import org.elasticsearch.client.ElasticsearchClient; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; @@ -224,14 +223,6 @@ public CreateIndexRequestBuilder setSource(Map source) { return this; } - /** - * Adds custom metadata to the index to be created. - */ - public CreateIndexRequestBuilder addCustom(IndexMetaData.Custom custom) { - request.custom(custom); - return this; - } - /** * Sets the settings and mappings as a single source. */ diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/TransportCreateIndexAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/TransportCreateIndexAction.java index 4cf159c439cb..e4384745d36e 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/TransportCreateIndexAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/TransportCreateIndexAction.java @@ -75,7 +75,7 @@ protected void masterOperation(final CreateIndexRequest request, final ClusterSt final CreateIndexClusterStateUpdateRequest updateRequest = new CreateIndexClusterStateUpdateRequest(request, cause, indexName, request.index()) .ackTimeout(request.timeout()).masterNodeTimeout(request.masterNodeTimeout()) .settings(request.settings()).mappings(request.mappings()) - .aliases(request.aliases()).customs(request.customs()) + .aliases(request.aliases()) .waitForActiveShards(request.waitForActiveShards()); createIndexService.createIndex(updateRequest, ActionListener.wrap(response -> diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java index 7195fd78154c..5459805416e9 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/shrink/TransportResizeAction.java @@ -185,7 +185,6 @@ static CreateIndexClusterStateUpdateRequest prepareCreateIndexRequest(final Resi .masterNodeTimeout(targetIndex.masterNodeTimeout()) .settings(targetIndex.settings()) .aliases(targetIndex.aliases()) - .customs(targetIndex.customs()) .waitForActiveShards(targetIndex.waitForActiveShards()) .recoverFrom(metaData.getIndex()) .resizeType(resizeRequest.getResizeType()) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java index f9431a3ad02b..7b45709f2a00 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java @@ -27,7 +27,6 @@ import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.master.MasterNodeRequest; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -61,9 +60,9 @@ import java.util.stream.Collectors; import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; import static org.elasticsearch.common.settings.Settings.readSettingsFromStream; import static org.elasticsearch.common.settings.Settings.writeSettingsToStream; -import static org.elasticsearch.common.settings.Settings.Builder.EMPTY_SETTINGS; /** * A request to create an index template. @@ -88,8 +87,6 @@ public class PutIndexTemplateRequest extends MasterNodeRequest aliases = new HashSet<>(); - private Map customs = new HashMap<>(); - private Integer version; public PutIndexTemplateRequest() { @@ -353,15 +350,7 @@ public PutIndexTemplateRequest source(Map templateSource) { } else if (name.equals("aliases")) { aliases((Map) entry.getValue()); } else { - // maybe custom? - IndexMetaData.Custom proto = IndexMetaData.lookupPrototype(name); - if (proto != null) { - try { - customs.put(name, proto.fromMap((Map) entry.getValue())); - } catch (IOException e) { - throw new ElasticsearchParseException("failed to parse custom metadata for [{}]", name); - } - } + throw new ElasticsearchParseException("unknown key [{}] in the template ", name); } } return this; @@ -395,15 +384,6 @@ public PutIndexTemplateRequest source(BytesReference source, XContentType xConte return source(XContentHelper.convertToMap(source, true, xContentType).v2()); } - public PutIndexTemplateRequest custom(IndexMetaData.Custom custom) { - customs.put(custom.type(), custom); - return this; - } - - public Map customs() { - return this.customs; - } - public Set aliases() { return this.aliases; } @@ -494,11 +474,13 @@ public void readFrom(StreamInput in) throws IOException { String mappingSource = in.readString(); mappings.put(type, mappingSource); } - int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - IndexMetaData.Custom customIndexMetaData = IndexMetaData.lookupPrototypeSafe(type).readFrom(in); - customs.put(type, customIndexMetaData); + if (in.getVersion().before(Version.V_7_0_0_alpha1)) { + // Used to be used for custom index metadata + int customSize = in.readVInt(); + assert customSize == 0 : "expected not to have any custom metadata"; + if (customSize > 0) { + throw new IllegalStateException("unexpected custom metadata when none is supported"); + } } int aliasesSize = in.readVInt(); for (int i = 0; i < aliasesSize; i++) { @@ -525,10 +507,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(entry.getKey()); out.writeString(entry.getValue()); } - out.writeVInt(customs.size()); - for (Map.Entry entry : customs.entrySet()) { - out.writeString(entry.getKey()); - entry.getValue().writeTo(out); + if (out.getVersion().before(Version.V_7_0_0_alpha1)) { + out.writeVInt(0); } out.writeVInt(aliases.size()); for (Alias alias : aliases) { @@ -565,10 +545,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } builder.endObject(); - for (Map.Entry entry : customs.entrySet()) { - builder.field(entry.getKey(), entry.getValue(), params); - } - return builder; } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java index bd8621a1a7d6..34eccbf9d8a4 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/TransportPutIndexTemplateAction.java @@ -84,7 +84,6 @@ protected void masterOperation(final PutIndexTemplateRequest request, final Clus .settings(templateSettingsBuilder.build()) .mappings(request.mappings()) .aliases(request.aliases()) - .customs(request.customs()) .create(request.create()) .masterTimeout(request.masterNodeTimeout()) .version(request.version()), diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DiffableStringMap.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DiffableStringMap.java new file mode 100644 index 000000000000..4aa429f57049 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DiffableStringMap.java @@ -0,0 +1,188 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.cluster.metadata; + +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.Diffable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This is a {@code Map} that implements AbstractDiffable so it + * can be used for cluster state purposes + */ +public class DiffableStringMap extends AbstractMap implements Diffable { + + private final Map innerMap; + + DiffableStringMap(final Map map) { + this.innerMap = map; + } + + @SuppressWarnings("unchecked") + DiffableStringMap(final StreamInput in) throws IOException { + this.innerMap = (Map) (Map) in.readMap(); + } + + @Override + public String put(String key, String value) { + return innerMap.put(key, value); + } + + @Override + public Set> entrySet() { + return innerMap.entrySet(); + } + + @Override + @SuppressWarnings("unchecked") + public void writeTo(StreamOutput out) throws IOException { + out.writeMap((Map) (Map) innerMap); + } + + @Override + public Diff diff(DiffableStringMap previousState) { + return new DiffableStringMapDiff(previousState, this); + } + + public static Diff readDiffFrom(StreamInput in) throws IOException { + return new DiffableStringMapDiff(in); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj instanceof DiffableStringMap) { + DiffableStringMap other = (DiffableStringMap) obj; + return innerMap.equals(other.innerMap); + } else if (obj instanceof Map) { + Map other = (Map) obj; + return innerMap.equals(other); + } else { + return false; + } + } + + @Override + public int hashCode() { + return innerMap.hashCode(); + } + + @Override + public String toString() { + return "DiffableStringMap[" + innerMap.toString() + "]"; + } + + /** + * Represents differences between two DiffableStringMaps. + */ + public static class DiffableStringMapDiff implements Diff { + + private final List deletes; + private final Map upserts; // diffs also become upserts + + private DiffableStringMapDiff(DiffableStringMap before, DiffableStringMap after) { + final List tempDeletes = new ArrayList<>(); + final Map tempUpserts = new HashMap<>(); + for (String key : before.keySet()) { + if (after.containsKey(key) == false) { + tempDeletes.add(key); + } + } + + for (Map.Entry partIter : after.entrySet()) { + String beforePart = before.get(partIter.getKey()); + if (beforePart == null) { + tempUpserts.put(partIter.getKey(), partIter.getValue()); + } else if (partIter.getValue().equals(beforePart) == false) { + tempUpserts.put(partIter.getKey(), partIter.getValue()); + } + } + deletes = tempDeletes; + upserts = tempUpserts; + } + + private DiffableStringMapDiff(StreamInput in) throws IOException { + deletes = new ArrayList<>(); + upserts = new HashMap<>(); + int deletesCount = in.readVInt(); + for (int i = 0; i < deletesCount; i++) { + deletes.add(in.readString()); + } + int upsertsCount = in.readVInt(); + for (int i = 0; i < upsertsCount; i++) { + String key = in.readString(); + String newValue = in.readString(); + upserts.put(key, newValue); + } + } + + public List getDeletes() { + return deletes; + } + + public Map> getDiffs() { + return Collections.emptyMap(); + } + + public Map getUpserts() { + return upserts; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVInt(deletes.size()); + for (String delete : deletes) { + out.writeString(delete); + } + out.writeVInt(upserts.size()); + for (Map.Entry entry : upserts.entrySet()) { + out.writeString(entry.getKey()); + out.writeString(entry.getValue()); + } + } + + @Override + public DiffableStringMap apply(DiffableStringMap part) { + Map builder = new HashMap<>(part.innerMap); + List deletes = getDeletes(); + for (String delete : deletes) { + builder.remove(delete); + } + assert getDiffs().size() == 0 : "there should never be diffs for DiffableStringMap"; + + for (Map.Entry upsert : upserts.entrySet()) { + builder.put(upsert.getKey(), upsert.getValue()); + } + return new DiffableStringMap(builder); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index 117f663398f6..e3af709ec5f0 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -23,7 +23,6 @@ import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; - import org.elasticsearch.Assertions; import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.rollover.RolloverInfo; @@ -65,7 +64,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; @@ -81,59 +79,6 @@ public class IndexMetaData implements Diffable, ToXContentFragment { - /** - * This class will be removed in v7.0 - */ - @Deprecated - public interface Custom extends Diffable, ToXContent { - - String type(); - - Custom fromMap(Map map) throws IOException; - - Custom fromXContent(XContentParser parser) throws IOException; - - /** - * Reads the {@link org.elasticsearch.cluster.Diff} from StreamInput - */ - Diff readDiffFrom(StreamInput in) throws IOException; - - /** - * Reads an object of this type from the provided {@linkplain StreamInput}. The receiving instance remains unchanged. - */ - Custom readFrom(StreamInput in) throws IOException; - - /** - * Merges from this to another, with this being more important, i.e., if something exists in this and another, - * this will prevail. - */ - Custom mergeWith(Custom another); - } - - public static Map customPrototypes = new HashMap<>(); - - /** - * Register a custom index meta data factory. Make sure to call it from a static block. - */ - public static void registerPrototype(String type, Custom proto) { - customPrototypes.put(type, proto); - } - - @Nullable - public static T lookupPrototype(String type) { - //noinspection unchecked - return (T) customPrototypes.get(type); - } - - public static T lookupPrototypeSafe(String type) { - //noinspection unchecked - T proto = (T) customPrototypes.get(type); - if (proto == null) { - throw new IllegalArgumentException("No custom metadata prototype registered for type [" + type + "]"); - } - return proto; - } - public static final ClusterBlock INDEX_READ_ONLY_BLOCK = new ClusterBlock(5, "index read-only (api)", false, false, false, RestStatus.FORBIDDEN, EnumSet.of(ClusterBlockLevel.WRITE, ClusterBlockLevel.METADATA_WRITE)); public static final ClusterBlock INDEX_READ_BLOCK = new ClusterBlock(7, "index read (api)", false, false, false, RestStatus.FORBIDDEN, EnumSet.of(ClusterBlockLevel.READ)); public static final ClusterBlock INDEX_WRITE_BLOCK = new ClusterBlock(8, "index write (api)", false, false, false, RestStatus.FORBIDDEN, EnumSet.of(ClusterBlockLevel.WRITE)); @@ -324,7 +269,7 @@ public Iterator> settings() { private final ImmutableOpenMap mappings; - private final ImmutableOpenMap customs; + private final ImmutableOpenMap customData; private final ImmutableOpenIntMap> inSyncAllocationIds; @@ -343,7 +288,7 @@ public Iterator> settings() { private IndexMetaData(Index index, long version, long mappingVersion, long[] primaryTerms, State state, int numberOfShards, int numberOfReplicas, Settings settings, ImmutableOpenMap mappings, ImmutableOpenMap aliases, - ImmutableOpenMap customs, ImmutableOpenIntMap> inSyncAllocationIds, + ImmutableOpenMap customData, ImmutableOpenIntMap> inSyncAllocationIds, DiscoveryNodeFilters requireFilters, DiscoveryNodeFilters initialRecoveryFilters, DiscoveryNodeFilters includeFilters, DiscoveryNodeFilters excludeFilters, Version indexCreatedVersion, Version indexUpgradedVersion, int routingNumShards, int routingPartitionSize, ActiveShardCount waitForActiveShards, ImmutableOpenMap rolloverInfos) { @@ -360,7 +305,7 @@ private IndexMetaData(Index index, long version, long mappingVersion, long[] pri this.totalNumberOfShards = numberOfShards * (numberOfReplicas + 1); this.settings = settings; this.mappings = mappings; - this.customs = customs; + this.customData = customData; this.aliases = aliases; this.inSyncAllocationIds = inSyncAllocationIds; this.requireFilters = requireFilters; @@ -511,13 +456,12 @@ public MappingMetaData mappingOrDefault(String mappingType) { return mappings.get(MapperService.DEFAULT_MAPPING); } - public ImmutableOpenMap getCustoms() { - return this.customs; + ImmutableOpenMap getCustomData() { + return this.customData; } - @SuppressWarnings("unchecked") - public T custom(String type) { - return (T) customs.get(type); + public Map getCustomData(final String key) { + return Collections.unmodifiableMap(this.customData.get(key)); } public ImmutableOpenIntMap> getInSyncAllocationIds() { @@ -583,7 +527,7 @@ public boolean equals(Object o) { if (state != that.state) { return false; } - if (!customs.equals(that.customs)) { + if (!customData.equals(that.customData)) { return false; } if (routingNumShards != that.routingNumShards) { @@ -612,7 +556,7 @@ public int hashCode() { result = 31 * result + aliases.hashCode(); result = 31 * result + settings.hashCode(); result = 31 * result + mappings.hashCode(); - result = 31 * result + customs.hashCode(); + result = 31 * result + customData.hashCode(); result = 31 * result + Long.hashCode(routingFactor); result = 31 * result + Long.hashCode(routingNumShards); result = 31 * result + Arrays.hashCode(primaryTerms); @@ -652,7 +596,7 @@ private static class IndexMetaDataDiff implements Diff { private final Settings settings; private final Diff> mappings; private final Diff> aliases; - private final Diff> customs; + private final Diff> customData; private final Diff>> inSyncAllocationIds; private final Diff> rolloverInfos; @@ -666,7 +610,7 @@ private static class IndexMetaDataDiff implements Diff { primaryTerms = after.primaryTerms; mappings = DiffableUtils.diff(before.mappings, after.mappings, DiffableUtils.getStringKeySerializer()); aliases = DiffableUtils.diff(before.aliases, after.aliases, DiffableUtils.getStringKeySerializer()); - customs = DiffableUtils.diff(before.customs, after.customs, DiffableUtils.getStringKeySerializer()); + customData = DiffableUtils.diff(before.customData, after.customData, DiffableUtils.getStringKeySerializer()); inSyncAllocationIds = DiffableUtils.diff(before.inSyncAllocationIds, after.inSyncAllocationIds, DiffableUtils.getVIntKeySerializer(), DiffableUtils.StringSetValueSerializer.getInstance()); rolloverInfos = DiffableUtils.diff(before.rolloverInfos, after.rolloverInfos, DiffableUtils.getStringKeySerializer()); @@ -688,18 +632,8 @@ private static class IndexMetaDataDiff implements Diff { MappingMetaData::readDiffFrom); aliases = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), AliasMetaData::new, AliasMetaData::readDiffFrom); - customs = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), - new DiffableUtils.DiffableValueSerializer() { - @Override - public Custom read(StreamInput in, String key) throws IOException { - return lookupPrototypeSafe(key).readFrom(in); - } - - @Override - public Diff readDiff(StreamInput in, String key) throws IOException { - return lookupPrototypeSafe(key).readDiffFrom(in); - } - }); + customData = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), DiffableStringMap::new, + DiffableStringMap::readDiffFrom); inSyncAllocationIds = DiffableUtils.readImmutableOpenIntMapDiff(in, DiffableUtils.getVIntKeySerializer(), DiffableUtils.StringSetValueSerializer.getInstance()); if (in.getVersion().onOrAfter(Version.V_6_4_0)) { @@ -724,7 +658,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeVLongArray(primaryTerms); mappings.writeTo(out); aliases.writeTo(out); - customs.writeTo(out); + customData.writeTo(out); inSyncAllocationIds.writeTo(out); if (out.getVersion().onOrAfter(Version.V_6_4_0)) { rolloverInfos.writeTo(out); @@ -742,7 +676,7 @@ public IndexMetaData apply(IndexMetaData part) { builder.primaryTerms(primaryTerms); builder.mappings.putAll(mappings.apply(part.mappings)); builder.aliases.putAll(aliases.apply(part.aliases)); - builder.customs.putAll(customs.apply(part.customs)); + builder.customMetaData.putAll(customData.apply(part.customData)); builder.inSyncAllocationIds.putAll(inSyncAllocationIds.apply(part.inSyncAllocationIds)); builder.rolloverInfos.putAll(rolloverInfos.apply(part.rolloverInfos)); return builder.build(); @@ -772,10 +706,17 @@ public static IndexMetaData readFrom(StreamInput in) throws IOException { builder.putAlias(aliasMd); } int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - Custom customIndexMetaData = lookupPrototypeSafe(type).readFrom(in); - builder.putCustom(type, customIndexMetaData); + if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + for (int i = 0; i < customSize; i++) { + String key = in.readString(); + DiffableStringMap custom = new DiffableStringMap(in); + builder.putCustom(key, custom); + } + } else { + assert customSize == 0 : "expected no custom index metadata"; + if (customSize > 0) { + throw new IllegalStateException("unexpected custom metadata when none is supported"); + } } int inSyncAllocationIdsSize = in.readVInt(); for (int i = 0; i < inSyncAllocationIdsSize; i++) { @@ -811,10 +752,14 @@ public void writeTo(StreamOutput out) throws IOException { for (ObjectCursor cursor : aliases.values()) { cursor.value.writeTo(out); } - out.writeVInt(customs.size()); - for (ObjectObjectCursor cursor : customs) { - out.writeString(cursor.key); - cursor.value.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + out.writeVInt(customData.size()); + for (final ObjectObjectCursor cursor : customData) { + out.writeString(cursor.key); + cursor.value.writeTo(out); + } + } else { + out.writeVInt(0); } out.writeVInt(inSyncAllocationIds.size()); for (IntObjectCursor> cursor : inSyncAllocationIds) { @@ -847,7 +792,7 @@ public static class Builder { private Settings settings = Settings.Builder.EMPTY_SETTINGS; private final ImmutableOpenMap.Builder mappings; private final ImmutableOpenMap.Builder aliases; - private final ImmutableOpenMap.Builder customs; + private final ImmutableOpenMap.Builder customMetaData; private final ImmutableOpenIntMap.Builder> inSyncAllocationIds; private final ImmutableOpenMap.Builder rolloverInfos; private Integer routingNumShards; @@ -856,7 +801,7 @@ public Builder(String index) { this.index = index; this.mappings = ImmutableOpenMap.builder(); this.aliases = ImmutableOpenMap.builder(); - this.customs = ImmutableOpenMap.builder(); + this.customMetaData = ImmutableOpenMap.builder(); this.inSyncAllocationIds = ImmutableOpenIntMap.builder(); this.rolloverInfos = ImmutableOpenMap.builder(); } @@ -870,7 +815,7 @@ public Builder(IndexMetaData indexMetaData) { this.primaryTerms = indexMetaData.primaryTerms.clone(); this.mappings = ImmutableOpenMap.builder(indexMetaData.mappings); this.aliases = ImmutableOpenMap.builder(indexMetaData.aliases); - this.customs = ImmutableOpenMap.builder(indexMetaData.customs); + this.customMetaData = ImmutableOpenMap.builder(indexMetaData.customData); this.routingNumShards = indexMetaData.routingNumShards; this.inSyncAllocationIds = ImmutableOpenIntMap.builder(indexMetaData.inSyncAllocationIds); this.rolloverInfos = ImmutableOpenMap.builder(indexMetaData.rolloverInfos); @@ -1000,8 +945,8 @@ public Builder removeAllAliases() { return this; } - public Builder putCustom(String type, Custom customIndexMetaData) { - this.customs.put(type, customIndexMetaData); + public Builder putCustom(String type, Map customIndexMetaData) { + this.customMetaData.put(type, new DiffableStringMap(customIndexMetaData)); return this; } @@ -1169,7 +1114,7 @@ public IndexMetaData build() { final String uuid = settings.get(SETTING_INDEX_UUID, INDEX_UUID_NA_VALUE); return new IndexMetaData(new Index(index, uuid), version, mappingVersion, primaryTerms, state, numberOfShards, numberOfReplicas, tmpSettings, mappings.build(), - tmpAliases.build(), customs.build(), filledInSyncAllocationIds.build(), requireFilters, initialRecoveryFilters, includeFilters, excludeFilters, + tmpAliases.build(), customMetaData.build(), filledInSyncAllocationIds.build(), requireFilters, initialRecoveryFilters, includeFilters, excludeFilters, indexCreatedVersion, indexUpgradedVersion, getRoutingNumShards(), routingPartitionSize, waitForActiveShards, rolloverInfos.build()); } @@ -1197,10 +1142,9 @@ public static void toXContent(IndexMetaData indexMetaData, XContentBuilder build } builder.endArray(); - for (ObjectObjectCursor cursor : indexMetaData.getCustoms()) { - builder.startObject(cursor.key); - cursor.value.toXContent(builder, params); - builder.endObject(); + for (ObjectObjectCursor cursor : indexMetaData.customData) { + builder.field(cursor.key); + builder.map(cursor.value); } builder.startObject(KEY_ALIASES); @@ -1309,15 +1253,8 @@ public static IndexMetaData fromXContent(XContentParser parser) throws IOExcepti assert Version.CURRENT.major <= 5; parser.skipChildren(); } else { - // check if its a custom index metadata - Custom proto = lookupPrototype(currentFieldName); - if (proto == null) { - //TODO warn - parser.skipChildren(); - } else { - Custom custom = proto.fromXContent(parser); - builder.putCustom(custom.type(), custom); - } + // assume it's custom index metadata + builder.putCustom(currentFieldName, parser.mapStrings()); } } else if (token == XContentParser.Token.START_ARRAY) { if (KEY_MAPPINGS.equals(currentFieldName)) { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java index d35a4baa1e68..c3f0f86e3e9e 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java @@ -20,7 +20,7 @@ import com.carrotsearch.hppc.cursors.ObjectCursor; import com.carrotsearch.hppc.cursors.ObjectObjectCursor; - +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.Version; import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.Diff; @@ -87,13 +87,10 @@ public class IndexTemplateMetaData extends AbstractDiffable aliases; - private final ImmutableOpenMap customs; - public IndexTemplateMetaData(String name, int order, Integer version, List patterns, Settings settings, ImmutableOpenMap mappings, - ImmutableOpenMap aliases, - ImmutableOpenMap customs) { + ImmutableOpenMap aliases) { if (patterns == null || patterns.isEmpty()) { throw new IllegalArgumentException("Index patterns must not be null or empty; got " + patterns); } @@ -104,7 +101,6 @@ public IndexTemplateMetaData(String name, int order, Integer version, this.settings = settings; this.mappings = mappings; this.aliases = aliases; - this.customs = customs; } public String name() { @@ -165,19 +161,6 @@ public ImmutableOpenMap getAliases() { return this.aliases; } - public ImmutableOpenMap customs() { - return this.customs; - } - - public ImmutableOpenMap getCustoms() { - return this.customs; - } - - @SuppressWarnings("unchecked") - public T custom(String type) { - return (T) customs.get(type); - } - public static Builder builder(String name) { return new Builder(name); } @@ -227,11 +210,13 @@ public static IndexTemplateMetaData readFrom(StreamInput in) throws IOException AliasMetaData aliasMd = new AliasMetaData(in); builder.putAlias(aliasMd); } - int customSize = in.readVInt(); - for (int i = 0; i < customSize; i++) { - String type = in.readString(); - IndexMetaData.Custom customIndexMetaData = IndexMetaData.lookupPrototypeSafe(type).readFrom(in); - builder.putCustom(type, customIndexMetaData); + if (in.getVersion().before(Version.V_7_0_0_alpha1)) { + // Previously we allowed custom metadata + int customSize = in.readVInt(); + assert customSize == 0 : "expected no custom metadata"; + if (customSize > 0) { + throw new IllegalStateException("unexpected custom metadata when none is supported"); + } } builder.version(in.readOptionalVInt()); return builder.build(); @@ -260,10 +245,8 @@ public void writeTo(StreamOutput out) throws IOException { for (ObjectCursor cursor : aliases.values()) { cursor.value.writeTo(out); } - out.writeVInt(customs.size()); - for (ObjectObjectCursor cursor : customs) { - out.writeString(cursor.key); - cursor.value.writeTo(out); + if (out.getVersion().before(Version.V_7_0_0_alpha1)) { + out.writeVInt(0); } out.writeOptionalVInt(version); } @@ -272,9 +255,6 @@ public static class Builder { private static final Set VALID_FIELDS = Sets.newHashSet( "template", "order", "mappings", "settings", "index_patterns", "aliases", "version"); - static { - VALID_FIELDS.addAll(IndexMetaData.customPrototypes.keySet()); - } private String name; @@ -290,13 +270,10 @@ public static class Builder { private final ImmutableOpenMap.Builder aliases; - private final ImmutableOpenMap.Builder customs; - public Builder(String name) { this.name = name; mappings = ImmutableOpenMap.builder(); aliases = ImmutableOpenMap.builder(); - customs = ImmutableOpenMap.builder(); } public Builder(IndexTemplateMetaData indexTemplateMetaData) { @@ -308,7 +285,6 @@ public Builder(IndexTemplateMetaData indexTemplateMetaData) { mappings = ImmutableOpenMap.builder(indexTemplateMetaData.mappings()); aliases = ImmutableOpenMap.builder(indexTemplateMetaData.aliases()); - customs = ImmutableOpenMap.builder(indexTemplateMetaData.customs()); } public Builder order(int order) { @@ -362,23 +338,8 @@ public Builder putAlias(AliasMetaData.Builder aliasMetaData) { return this; } - public Builder putCustom(String type, IndexMetaData.Custom customIndexMetaData) { - this.customs.put(type, customIndexMetaData); - return this; - } - - public Builder removeCustom(String type) { - this.customs.remove(type); - return this; - } - - public IndexMetaData.Custom getCustom(String type) { - return this.customs.get(type); - } - public IndexTemplateMetaData build() { - return new IndexTemplateMetaData(name, order, version, indexPatterns, settings, mappings.build(), - aliases.build(), customs.build()); + return new IndexTemplateMetaData(name, order, version, indexPatterns, settings, mappings.build(), aliases.build()); } public static void toXContent(IndexTemplateMetaData indexTemplateMetaData, XContentBuilder builder, ToXContent.Params params) @@ -425,12 +386,6 @@ public static void toInnerXContent(IndexTemplateMetaData indexTemplateMetaData, builder.endArray(); } - for (ObjectObjectCursor cursor : indexTemplateMetaData.customs()) { - builder.startObject(cursor.key); - cursor.value.toXContent(builder, params); - builder.endObject(); - } - builder.startObject("aliases"); for (ObjectCursor cursor : indexTemplateMetaData.aliases().values()) { AliasMetaData.Builder.toXContent(cursor.value, builder, params); @@ -468,15 +423,7 @@ public static IndexTemplateMetaData fromXContent(XContentParser parser, String t builder.putAlias(AliasMetaData.Builder.fromXContent(parser)); } } else { - // check if its a custom index metadata - IndexMetaData.Custom proto = IndexMetaData.lookupPrototype(currentFieldName); - if (proto == null) { - //TODO warn - parser.skipChildren(); - } else { - IndexMetaData.Custom custom = proto.fromXContent(parser); - builder.putCustom(custom.type(), custom); - } + throw new ElasticsearchParseException("unknown key [{}] for index template", currentFieldName); } } else if (token == XContentParser.Token.START_ARRAY) { if ("mappings".equals(currentFieldName)) { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java index c07061cde97a..3fa77059ed06 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java @@ -38,7 +38,6 @@ import org.elasticsearch.cluster.block.ClusterBlock; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.block.ClusterBlocks; -import org.elasticsearch.cluster.metadata.IndexMetaData.Custom; import org.elasticsearch.cluster.metadata.IndexMetaData.State; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.IndexRoutingTable; @@ -287,7 +286,7 @@ public ClusterState execute(ClusterState currentState) throws Exception { List templates = MetaDataIndexTemplateService.findTemplates(currentState.metaData(), request.index()); - Map customs = new HashMap<>(); + Map> customs = new HashMap<>(); // add the request mapping Map> mappings = new HashMap<>(); @@ -300,10 +299,6 @@ public ClusterState execute(ClusterState currentState) throws Exception { mappings.put(entry.getKey(), MapperService.parseMapping(xContentRegistry, entry.getValue())); } - for (Map.Entry entry : request.customs().entrySet()) { - customs.put(entry.getKey(), entry.getValue()); - } - final Index recoverFromIndex = request.recoverFrom(); if (recoverFromIndex == null) { @@ -320,18 +315,6 @@ public ClusterState execute(ClusterState currentState) throws Exception { MapperService.parseMapping(xContentRegistry, mappingString)); } } - // handle custom - for (ObjectObjectCursor cursor : template.customs()) { - String type = cursor.key; - IndexMetaData.Custom custom = cursor.value; - IndexMetaData.Custom existing = customs.get(type); - if (existing == null) { - customs.put(type, custom); - } else { - IndexMetaData.Custom merged = existing.mergeWith(custom); - customs.put(type, merged); - } - } //handle aliases for (ObjectObjectCursor cursor : template.aliases()) { AliasMetaData aliasMetaData = cursor.value; @@ -519,7 +502,7 @@ public ClusterState execute(ClusterState currentState) throws Exception { indexMetaDataBuilder.putAlias(aliasMetaData); } - for (Map.Entry customEntry : customs.entrySet()) { + for (Map.Entry> customEntry : customs.entrySet()) { indexMetaDataBuilder.putCustom(customEntry.getKey(), customEntry.getValue()); } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateService.java index 507eaf412d5f..1baeb2459f09 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexTemplateService.java @@ -179,9 +179,6 @@ public ClusterState execute(ClusterState currentState) throws Exception { .indexRouting(alias.indexRouting()).searchRouting(alias.searchRouting()).build(); templateBuilder.putAlias(aliasMetaData); } - for (Map.Entry entry : request.customs.entrySet()) { - templateBuilder.putCustom(entry.getKey(), entry.getValue()); - } IndexTemplateMetaData template = templateBuilder.build(); MetaData.Builder builder = MetaData.builder(currentState.metaData()).put(template); @@ -339,7 +336,6 @@ public static class PutRequest { Settings settings = Settings.Builder.EMPTY_SETTINGS; Map mappings = new HashMap<>(); List aliases = new ArrayList<>(); - Map customs = new HashMap<>(); TimeValue masterTimeout = MasterNodeRequest.DEFAULT_MASTER_NODE_TIMEOUT; @@ -378,11 +374,6 @@ public PutRequest aliases(Set aliases) { return this; } - public PutRequest customs(Map customs) { - this.customs.putAll(customs); - return this; - } - public PutRequest putMapping(String mappingType, String mappingSource) { mappings.put(mappingType, mappingSource); return this; diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/DiffableStringMapTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/DiffableStringMapTests.java new file mode 100644 index 000000000000..341022030b37 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/DiffableStringMapTests.java @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.cluster.metadata; + +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class DiffableStringMapTests extends ESTestCase { + + public void testDiffableStringMapDiff() { + Map m = new HashMap<>(); + m.put("foo", "bar"); + m.put("baz", "eggplant"); + m.put("potato", "canon"); + DiffableStringMap dsm = new DiffableStringMap(m); + + Map m2 = new HashMap<>(); + m2.put("foo", "not-bar"); + m2.put("newkey", "yay"); + m2.put("baz", "eggplant"); + DiffableStringMap dsm2 = new DiffableStringMap(m2); + + Diff diff = dsm2.diff(dsm); + assertThat(diff, instanceOf(DiffableStringMap.DiffableStringMapDiff.class)); + DiffableStringMap.DiffableStringMapDiff dsmd = (DiffableStringMap.DiffableStringMapDiff) diff; + + assertThat(dsmd.getDeletes(), containsInAnyOrder("potato")); + assertThat(dsmd.getDiffs().size(), equalTo(0)); + Map upserts = new HashMap<>(); + upserts.put("foo", "not-bar"); + upserts.put("newkey", "yay"); + assertThat(dsmd.getUpserts(), equalTo(upserts)); + + DiffableStringMap dsm3 = diff.apply(dsm); + assertThat(dsm3.get("foo"), equalTo("not-bar")); + assertThat(dsm3.get("newkey"), equalTo("yay")); + assertThat(dsm3.get("baz"), equalTo("eggplant")); + assertThat(dsm3.get("potato"), equalTo(null)); + } + + public void testRandomDiffing() { + Map m = new HashMap<>(); + m.put("1", "1"); + m.put("2", "2"); + m.put("3", "3"); + DiffableStringMap dsm = new DiffableStringMap(m); + DiffableStringMap expected = new DiffableStringMap(m); + + for (int i = 0; i < randomIntBetween(5, 50); i++) { + if (randomBoolean() && expected.size() > 1) { + expected.remove(randomFrom(expected.keySet())); + } else if (randomBoolean()) { + expected.put(randomFrom(expected.keySet()), randomAlphaOfLength(4)); + } else { + expected.put(randomAlphaOfLength(2), randomAlphaOfLength(4)); + } + dsm = expected.diff(dsm).apply(dsm); + } + assertThat(expected, equalTo(dsm)); + } + + public void testSerialization() throws IOException { + Map m = new HashMap<>(); + // Occasionally have an empty map + if (frequently()) { + m.put("foo", "bar"); + m.put("baz", "eggplant"); + m.put("potato", "canon"); + } + DiffableStringMap dsm = new DiffableStringMap(m); + + BytesStreamOutput bso = new BytesStreamOutput(); + dsm.writeTo(bso); + DiffableStringMap deserialized = new DiffableStringMap(bso.bytes().streamInput()); + assertThat(deserialized, equalTo(dsm)); + } +} diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java index 744a29e843c4..1aaec0803075 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java @@ -56,11 +56,11 @@ import org.mockito.ArgumentCaptor; import java.io.IOException; -import java.util.Map; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Set; -import java.util.Collections; -import java.util.Arrays; import java.util.function.Supplier; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS; @@ -71,13 +71,13 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.mockito.Matchers.anyObject; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.anyMap; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class IndexCreationTaskTests extends ESTestCase { @@ -127,14 +127,12 @@ public void testApplyDataFromTemplate() throws Exception { addMatchingTemplate(builder -> builder .putAlias(AliasMetaData.builder("alias1")) .putMapping("mapping1", createMapping()) - .putCustom("custom1", createCustom()) .settings(Settings.builder().put("key1", "value1")) ); final ClusterState result = executeTask(); assertThat(result.metaData().index("test").getAliases(), hasKey("alias1")); - assertThat(result.metaData().index("test").getCustoms(), hasKey("custom1")); assertThat(result.metaData().index("test").getSettings().get("key1"), equalTo("value1")); assertThat(getMappingsFromResponse(), Matchers.hasKey("mapping1")); } @@ -142,41 +140,31 @@ public void testApplyDataFromTemplate() throws Exception { public void testApplyDataFromRequest() throws Exception { setupRequestAlias(new Alias("alias1")); setupRequestMapping("mapping1", createMapping()); - setupRequestCustom("custom1", createCustom()); reqSettings.put("key1", "value1"); final ClusterState result = executeTask(); assertThat(result.metaData().index("test").getAliases(), hasKey("alias1")); - assertThat(result.metaData().index("test").getCustoms(), hasKey("custom1")); assertThat(result.metaData().index("test").getSettings().get("key1"), equalTo("value1")); assertThat(getMappingsFromResponse(), Matchers.hasKey("mapping1")); } public void testRequestDataHavePriorityOverTemplateData() throws Exception { - final IndexMetaData.Custom tplCustom = createCustom(); - final IndexMetaData.Custom reqCustom = createCustom(); - final IndexMetaData.Custom mergedCustom = createCustom(); - when(reqCustom.mergeWith(tplCustom)).thenReturn(mergedCustom); - final CompressedXContent tplMapping = createMapping("text"); final CompressedXContent reqMapping = createMapping("keyword"); addMatchingTemplate(builder -> builder .putAlias(AliasMetaData.builder("alias1").searchRouting("fromTpl").build()) .putMapping("mapping1", tplMapping) - .putCustom("custom1", tplCustom) .settings(Settings.builder().put("key1", "tplValue")) ); setupRequestAlias(new Alias("alias1").searchRouting("fromReq")); setupRequestMapping("mapping1", reqMapping); - setupRequestCustom("custom1", reqCustom); reqSettings.put("key1", "reqValue"); final ClusterState result = executeTask(); - assertThat(result.metaData().index("test").getCustoms().get("custom1"), equalTo(mergedCustom)); assertThat(result.metaData().index("test").getAliases().get("alias1").getSearchRouting(), equalTo("fromReq")); assertThat(result.metaData().index("test").getSettings().get("key1"), equalTo("reqValue")); assertThat(getMappingsFromResponse().get("mapping1").toString(), equalTo("{type={properties={field={type=keyword}}}}")); @@ -272,14 +260,13 @@ public void testShrinkIndexIgnoresTemplates() throws Exception { addMatchingTemplate(builder -> builder .putAlias(AliasMetaData.builder("alias1").searchRouting("fromTpl").build()) .putMapping("mapping1", createMapping()) - .putCustom("custom1", createCustom()) .settings(Settings.builder().put("key1", "tplValue")) ); final ClusterState result = executeTask(); assertThat(result.metaData().index("test").getAliases(), not(hasKey("alias1"))); - assertThat(result.metaData().index("test").getCustoms(), not(hasKey("custom1"))); + assertThat(result.metaData().index("test").getCustomData(), not(hasKey("custom1"))); assertThat(result.metaData().index("test").getSettings().keySet(), not(Matchers.contains("key1"))); assertThat(getMappingsFromResponse(), not(Matchers.hasKey("mapping1"))); } @@ -296,7 +283,6 @@ public void testWriteIndex() throws Exception { Boolean writeIndex = randomBoolean() ? null : randomBoolean(); setupRequestAlias(new Alias("alias1").writeIndex(writeIndex)); setupRequestMapping("mapping1", createMapping()); - setupRequestCustom("custom1", createCustom()); reqSettings.put("key1", "value1"); final ClusterState result = executeTask(); @@ -310,7 +296,6 @@ public void testWriteIndexValidationException() throws Exception { .numberOfShards(1).numberOfReplicas(0).build(); idxBuilder.put("test2", existingWriteIndex); setupRequestMapping("mapping1", createMapping()); - setupRequestCustom("custom1", createCustom()); reqSettings.put("key1", "value1"); setupRequestAlias(new Alias("alias1").writeIndex(true)); @@ -342,8 +327,8 @@ private IndexMetaData.Builder createIndexMetaDataBuilder(String name, String uui .numberOfReplicas(numReplicas); } - private IndexMetaData.Custom createCustom() { - return mock(IndexMetaData.Custom.class); + private Map createCustom() { + return Collections.singletonMap("a", "b"); } private interface MetaDataBuilderConfigurator { @@ -372,10 +357,6 @@ private void setupRequestMapping(String mappingKey, CompressedXContent mapping) when(request.mappings()).thenReturn(Collections.singletonMap(mappingKey, mapping.string())); } - private void setupRequestCustom(String customKey, IndexMetaData.Custom custom) throws IOException { - when(request.customs()).thenReturn(Collections.singletonMap(customKey, custom)); - } - private CompressedXContent createMapping() throws IOException { return createMapping("text"); } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java index 9e8a5e04f43c..393f7f6b1d4a 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetaDataTests.java @@ -23,7 +23,9 @@ import org.elasticsearch.action.admin.indices.rollover.MaxDocsCondition; import org.elasticsearch.action.admin.indices.rollover.MaxSizeCondition; import org.elasticsearch.action.admin.indices.rollover.RolloverInfo; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -45,6 +47,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import static org.hamcrest.Matchers.is; @@ -71,6 +75,9 @@ protected NamedXContentRegistry xContentRegistry() { public void testIndexMetaDataSerialization() throws IOException { Integer numShard = randomFrom(1, 2, 4, 8, 16); int numberOfReplicas = randomIntBetween(0, 10); + Map customMap = new HashMap<>(); + customMap.put(randomAlphaOfLength(5), randomAlphaOfLength(10)); + customMap.put(randomAlphaOfLength(10), randomAlphaOfLength(15)); IndexMetaData metaData = IndexMetaData.builder("foo") .settings(Settings.builder() .put("index.version.created", 1) @@ -80,6 +87,7 @@ public void testIndexMetaDataSerialization() throws IOException { .creationDate(randomLong()) .primaryTerm(0, 2) .setRoutingNumShards(32) + .putCustom("my_custom", customMap) .putRolloverInfo( new RolloverInfo(randomAlphaOfLength(5), Arrays.asList(new MaxAgeCondition(TimeValue.timeValueMillis(randomNonNegativeLong())), @@ -93,7 +101,8 @@ public void testIndexMetaDataSerialization() throws IOException { builder.endObject(); XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(builder)); final IndexMetaData fromXContentMeta = IndexMetaData.fromXContent(parser); - assertEquals(metaData, fromXContentMeta); + assertEquals("expected: " + Strings.toString(metaData) + "\nactual : " + Strings.toString(fromXContentMeta), + metaData, fromXContentMeta); assertEquals(metaData.hashCode(), fromXContentMeta.hashCode()); assertEquals(metaData.getNumberOfReplicas(), fromXContentMeta.getNumberOfReplicas()); @@ -103,6 +112,11 @@ public void testIndexMetaDataSerialization() throws IOException { assertEquals(metaData.getCreationDate(), fromXContentMeta.getCreationDate()); assertEquals(metaData.getRoutingFactor(), fromXContentMeta.getRoutingFactor()); assertEquals(metaData.primaryTerm(0), fromXContentMeta.primaryTerm(0)); + ImmutableOpenMap.Builder expectedCustomBuilder = ImmutableOpenMap.builder(); + expectedCustomBuilder.put("my_custom", new DiffableStringMap(customMap)); + ImmutableOpenMap expectedCustom = expectedCustomBuilder.build(); + assertEquals(metaData.getCustomData(), expectedCustom); + assertEquals(metaData.getCustomData(), fromXContentMeta.getCustomData()); final BytesStreamOutput out = new BytesStreamOutput(); metaData.writeTo(out); @@ -119,6 +133,8 @@ public void testIndexMetaDataSerialization() throws IOException { assertEquals(metaData.getRoutingFactor(), deserialized.getRoutingFactor()); assertEquals(metaData.primaryTerm(0), deserialized.primaryTerm(0)); assertEquals(metaData.getRolloverInfos(), deserialized.getRolloverInfos()); + assertEquals(deserialized.getCustomData(), expectedCustom); + assertEquals(metaData.getCustomData(), deserialized.getCustomData()); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java index c98587c4cc63..5fc076423541 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaDataTests.java @@ -78,13 +78,13 @@ public void testIndexTemplateMetaDataXContentRoundTrip() throws Exception { public void testValidateInvalidIndexPatterns() throws Exception { final IllegalArgumentException emptyPatternError = expectThrows(IllegalArgumentException.class, () -> { new IndexTemplateMetaData(randomRealisticUnicodeOfLengthBetween(5, 10), randomInt(), randomInt(), - Collections.emptyList(), Settings.EMPTY, ImmutableOpenMap.of(), ImmutableOpenMap.of(), ImmutableOpenMap.of()); + Collections.emptyList(), Settings.EMPTY, ImmutableOpenMap.of(), ImmutableOpenMap.of()); }); assertThat(emptyPatternError.getMessage(), equalTo("Index patterns must not be null or empty; got []")); final IllegalArgumentException nullPatternError = expectThrows(IllegalArgumentException.class, () -> { new IndexTemplateMetaData(randomRealisticUnicodeOfLengthBetween(5, 10), randomInt(), randomInt(), - null, Settings.EMPTY, ImmutableOpenMap.of(), ImmutableOpenMap.of(), ImmutableOpenMap.of()); + null, Settings.EMPTY, ImmutableOpenMap.of(), ImmutableOpenMap.of()); }); assertThat(nullPatternError.getMessage(), equalTo("Index patterns must not be null or empty; got null")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStoreTests.java index a2c70db3b63e..400096918754 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/NativeRolesStoreTests.java @@ -259,7 +259,7 @@ private ClusterState getClusterStateWithSecurityIndex() { .put(IndexMetaData.builder(securityIndexName).settings(settings)) .put(new IndexTemplateMetaData(SecurityIndexManager.SECURITY_TEMPLATE_NAME, 0, 0, Collections.singletonList(securityIndexName), Settings.EMPTY, ImmutableOpenMap.of(), - ImmutableOpenMap.of(), ImmutableOpenMap.of())) + ImmutableOpenMap.of())) .build(); if (withAlias) { From 83c3d7a6cfbb469888bdf1429489a7dc07db6020 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Fri, 31 Aug 2018 00:13:03 +0300 Subject: [PATCH 243/283] SQL: prevent duplicate generation for repeated aggs (#33252) Prevent generation of duplicate aggs caused by repetitive functions, leading to invalid query. Fix #30287 --- .../org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java | 3 +++ x-pack/qa/sql/src/main/resources/agg.sql-spec | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java index 5fb8a754f0f5..b8faedec7187 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/agg/Aggs.java @@ -112,6 +112,9 @@ public Aggs addGroups(Collection groups) { } public Aggs addAgg(LeafAgg agg) { + if (metricAggs.contains(agg)) { + return this; + } return new Aggs(groups, combine(metricAggs, agg), pipelineAggs); } 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 f42ce0ef7a09..f1ab9160b1af 100644 --- a/x-pack/qa/sql/src/main/resources/agg.sql-spec +++ b/x-pack/qa/sql/src/main/resources/agg.sql-spec @@ -394,4 +394,12 @@ SELECT MIN(salary) min, MAX(salary) max, gender g, languages l, COUNT(*) c FROM aggMultiWithHavingOnCount SELECT MIN(salary) min, MAX(salary) max, gender g, COUNT(*) c FROM "test_emp" WHERE languages > 0 GROUP BY g HAVING c > 40 ORDER BY gender; aggMultiGroupByMultiWithHavingOnCount -SELECT MIN(salary) min, MAX(salary) max, gender g, languages l, COUNT(*) c FROM "test_emp" WHERE languages > 0 GROUP BY g, languages HAVING c > 40 ORDER BY gender, languages; \ No newline at end of file +SELECT MIN(salary) min, MAX(salary) max, gender g, languages l, COUNT(*) c FROM "test_emp" WHERE languages > 0 GROUP BY g, languages HAVING c > 40 ORDER BY gender, languages; + +// repetion of same aggs to check whether the generated query contains duplicates or not +aggRepeatFunctionAcrossFields +SELECT MIN(emp_no) AS a, 1 + MIN(emp_no) AS b, ABS(MIN(emp_no)) AS c FROM test_emp; +aggRepeatFunctionBetweenSelectAndHaving +SELECT gender, COUNT(DISTINCT languages) AS c FROM test_emp GROUP BY gender HAVING count(DISTINCT languages) > 0 ORDER BY gender; + + From bbbb11a698ed556de14cc3fcfb365a60493519b9 Mon Sep 17 00:00:00 2001 From: Jack Conradson Date: Thu, 30 Aug 2018 14:33:32 -0700 Subject: [PATCH 244/283] Painless: Fix Bindings Bug (#33274) When the change was made to the format for in the whitelist for bindings, parameters from both the constructor and the method were combined into a single list instead of separate lists. The check for method parameters was being executed from the start of the combined list rather than the correct position. The tests for bindings used a constructor and a method that only used the int types so this was not caught. The test has been changed to also use a double type and this issue is fixed. --- .../main/java/org/elasticsearch/painless/BindingTest.java | 4 ++-- .../painless/lookup/PainlessLookupBuilder.java | 2 +- .../org/elasticsearch/painless/spi/org.elasticsearch.txt | 2 +- .../test/java/org/elasticsearch/painless/BindingsTests.java | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java index 1dcbce037b26..fc2a10891f62 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/BindingTest.java @@ -26,7 +26,7 @@ public BindingTest(int state0, int state1) { this.state = state0 + state1; } - public int testAddWithState(int stateless) { - return stateless + state; + public int testAddWithState(int istateless, double dstateless) { + return istateless + state + (int)dstateless; } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index 7adc81625205..a64814f86611 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -908,7 +908,7 @@ public void addPainlessBinding(Class targetClass, String methodName, Class int methodTypeParametersSize = javaMethod.getParameterCount(); for (int typeParameterIndex = 0; typeParameterIndex < methodTypeParametersSize; ++typeParameterIndex) { - Class typeParameter = typeParameters.get(typeParameterIndex); + Class typeParameter = typeParameters.get(constructorTypeParametersSize + typeParameterIndex); if (isValidType(typeParameter) == false) { throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " + diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt index 65f50bbc3834..444234384c6d 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.txt @@ -177,5 +177,5 @@ class org.elasticsearch.painless.FeatureTest no_import { # for testing static { - int testAddWithState(int, int, int) bound_to org.elasticsearch.painless.BindingTest + int testAddWithState(int, int, int, double) bound_to org.elasticsearch.painless.BindingTest } \ No newline at end of file diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java index c6d4e1974c14..4bcc557d3dcf 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java @@ -28,11 +28,11 @@ public class BindingsTests extends ScriptTestCase { public void testBasicBinding() { - assertEquals(15, exec("testAddWithState(4, 5, 6)")); + assertEquals(15, exec("testAddWithState(4, 5, 6, 0.0)")); } public void testRepeatedBinding() { - String script = "testAddWithState(4, 5, params.test)"; + String script = "testAddWithState(4, 5, params.test, 0.0)"; Map params = new HashMap<>(); ExecutableScript.Factory factory = scriptEngine.compile(null, script, ExecutableScript.CONTEXT, Collections.emptyMap()); ExecutableScript executableScript = factory.newInstance(params); @@ -48,7 +48,7 @@ public void testRepeatedBinding() { } public void testBoundBinding() { - String script = "testAddWithState(4, params.bound, params.test)"; + String script = "testAddWithState(4, params.bound, params.test, 0.0)"; Map params = new HashMap<>(); ExecutableScript.Factory factory = scriptEngine.compile(null, script, ExecutableScript.CONTEXT, Collections.emptyMap()); ExecutableScript executableScript = factory.newInstance(params); From 8a2d154bad8976af3c1282b7d5dd0b2ceb1c2454 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Thu, 30 Aug 2018 15:05:38 -0600 Subject: [PATCH 245/283] Update serialization versions for custom IndexMetaData backport --- .../action/admin/indices/create/CreateIndexRequest.java | 4 ++-- .../admin/indices/template/put/PutIndexTemplateRequest.java | 4 ++-- .../org/elasticsearch/cluster/metadata/IndexMetaData.java | 4 ++-- .../elasticsearch/cluster/metadata/IndexTemplateMetaData.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java index a186f9b50112..fa2a395f2c9e 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequest.java @@ -448,7 +448,7 @@ public void readFrom(StreamInput in) throws IOException { } mappings.put(type, source); } - if (in.getVersion().before(Version.V_7_0_0_alpha1)) { + if (in.getVersion().before(Version.V_6_5_0)) { // This used to be the size of custom metadata classes int customSize = in.readVInt(); assert customSize == 0 : "unexpected custom metadata when none is supported"; @@ -477,7 +477,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(entry.getKey()); out.writeString(entry.getValue()); } - if (out.getVersion().before(Version.V_7_0_0_alpha1)) { + if (out.getVersion().before(Version.V_6_5_0)) { // Size of custom index metadata, which is removed out.writeVInt(0); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java index 7b45709f2a00..d254f989d4a2 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/template/put/PutIndexTemplateRequest.java @@ -474,7 +474,7 @@ public void readFrom(StreamInput in) throws IOException { String mappingSource = in.readString(); mappings.put(type, mappingSource); } - if (in.getVersion().before(Version.V_7_0_0_alpha1)) { + if (in.getVersion().before(Version.V_6_5_0)) { // Used to be used for custom index metadata int customSize = in.readVInt(); assert customSize == 0 : "expected not to have any custom metadata"; @@ -507,7 +507,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(entry.getKey()); out.writeString(entry.getValue()); } - if (out.getVersion().before(Version.V_7_0_0_alpha1)) { + if (out.getVersion().before(Version.V_6_5_0)) { out.writeVInt(0); } out.writeVInt(aliases.size()); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java index e3af709ec5f0..a88ad4418e0c 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetaData.java @@ -706,7 +706,7 @@ public static IndexMetaData readFrom(StreamInput in) throws IOException { builder.putAlias(aliasMd); } int customSize = in.readVInt(); - if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (in.getVersion().onOrAfter(Version.V_6_5_0)) { for (int i = 0; i < customSize; i++) { String key = in.readString(); DiffableStringMap custom = new DiffableStringMap(in); @@ -752,7 +752,7 @@ public void writeTo(StreamOutput out) throws IOException { for (ObjectCursor cursor : aliases.values()) { cursor.value.writeTo(out); } - if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { + if (out.getVersion().onOrAfter(Version.V_6_5_0)) { out.writeVInt(customData.size()); for (final ObjectObjectCursor cursor : customData) { out.writeString(cursor.key); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java index c3f0f86e3e9e..7e2d92563035 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexTemplateMetaData.java @@ -210,7 +210,7 @@ public static IndexTemplateMetaData readFrom(StreamInput in) throws IOException AliasMetaData aliasMd = new AliasMetaData(in); builder.putAlias(aliasMd); } - if (in.getVersion().before(Version.V_7_0_0_alpha1)) { + if (in.getVersion().before(Version.V_6_5_0)) { // Previously we allowed custom metadata int customSize = in.readVInt(); assert customSize == 0 : "expected no custom metadata"; @@ -245,7 +245,7 @@ public void writeTo(StreamOutput out) throws IOException { for (ObjectCursor cursor : aliases.values()) { cursor.value.writeTo(out); } - if (out.getVersion().before(Version.V_7_0_0_alpha1)) { + if (out.getVersion().before(Version.V_6_5_0)) { out.writeVInt(0); } out.writeOptionalVInt(version); From 39839f97ef8fa4562e2c2e0566867943b0e0de96 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 30 Aug 2018 19:37:20 -0400 Subject: [PATCH 246/283] TEST: Access cluster state directly in assertSeqNos (#33277) Some AbstractDisruptionTestCase tests start failing since we enabled assertSeqNos (in #33130). They fail because the assertSeqNos assertion queries cluster stats while the cluster is disrupted or not formed yet. This commit switches to use the cluster state and shard stats directly from the test cluster. Closes #33251 --- .../elasticsearch/test/ESIntegTestCase.java | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) 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 1f51ad495e18..322e2a128c97 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -20,12 +20,15 @@ package org.elasticsearch.test; import com.carrotsearch.hppc.ObjectLongMap; +import com.carrotsearch.hppc.cursors.IntObjectCursor; +import com.carrotsearch.hppc.cursors.ObjectObjectCursor; import com.carrotsearch.randomizedtesting.RandomizedContext; import com.carrotsearch.randomizedtesting.annotations.TestGroup; import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import com.carrotsearch.randomizedtesting.generators.RandomPicks; import org.apache.http.HttpHost; import org.apache.lucene.search.Sort; +import org.apache.lucene.store.AlreadyClosedException; import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; @@ -48,10 +51,6 @@ import org.elasticsearch.action.admin.indices.segments.IndexShardSegments; import org.elasticsearch.action.admin.indices.segments.IndicesSegmentResponse; import org.elasticsearch.action.admin.indices.segments.ShardSegments; -import org.elasticsearch.action.admin.indices.stats.IndexShardStats; -import org.elasticsearch.action.admin.indices.stats.IndexStats; -import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; -import org.elasticsearch.action.admin.indices.stats.ShardStats; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequestBuilder; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; @@ -187,7 +186,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; @@ -2328,40 +2326,48 @@ public static Index resolveIndex(String index) { protected void assertSeqNos() throws Exception { assertBusy(() -> { - IndicesStatsResponse stats = client().admin().indices().prepareStats().clear().get(); - for (IndexStats indexStats : stats.getIndices().values()) { - for (IndexShardStats indexShardStats : indexStats.getIndexShards().values()) { - Optional maybePrimary = Stream.of(indexShardStats.getShards()) - .filter(s -> s.getShardRouting().active() && s.getShardRouting().primary()) - .findFirst(); - if (maybePrimary.isPresent() == false) { + final ClusterState state = clusterService().state(); + for (ObjectObjectCursor indexRoutingTable : state.routingTable().indicesRouting()) { + for (IntObjectCursor indexShardRoutingTable : indexRoutingTable.value.shards()) { + ShardRouting primaryShardRouting = indexShardRoutingTable.value.primaryShard(); + if (primaryShardRouting == null || primaryShardRouting.assignedToNode() == false) { continue; } - ShardStats primary = maybePrimary.get(); - final SeqNoStats primarySeqNoStats = primary.getSeqNoStats(); - final ShardRouting primaryShardRouting = primary.getShardRouting(); + DiscoveryNode primaryNode = state.nodes().get(primaryShardRouting.currentNodeId()); + IndexShard primaryShard = internalCluster().getInstance(IndicesService.class, primaryNode.getName()) + .indexServiceSafe(primaryShardRouting.index()).getShard(primaryShardRouting.id()); + final SeqNoStats primarySeqNoStats; + final ObjectLongMap syncGlobalCheckpoints; + try { + primarySeqNoStats = primaryShard.seqNoStats(); + syncGlobalCheckpoints = primaryShard.getInSyncGlobalCheckpoints(); + } catch (AlreadyClosedException ex) { + continue; // shard is closed - just ignore + } assertThat(primaryShardRouting + " should have set the global checkpoint", - primarySeqNoStats.getGlobalCheckpoint(), not(equalTo(SequenceNumbers.UNASSIGNED_SEQ_NO))); - final DiscoveryNode node = clusterService().state().nodes().get(primaryShardRouting.currentNodeId()); - final IndicesService indicesService = - internalCluster().getInstance(IndicesService.class, node.getName()); - final IndexShard indexShard = indicesService.getShardOrNull(primaryShardRouting.shardId()); - final ObjectLongMap globalCheckpoints = indexShard.getInSyncGlobalCheckpoints(); - for (ShardStats shardStats : indexShardStats) { - final SeqNoStats seqNoStats = shardStats.getSeqNoStats(); - if (seqNoStats == null) { - continue; // this shard was closed + primarySeqNoStats.getGlobalCheckpoint(), not(equalTo(SequenceNumbers.UNASSIGNED_SEQ_NO))); + 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 SeqNoStats seqNoStats; + try { + seqNoStats = replicaShard.seqNoStats(); + } catch (AlreadyClosedException e) { + continue; // shard is closed - just ignore } - assertThat(shardStats.getShardRouting() + " local checkpoint mismatch", - seqNoStats.getLocalCheckpoint(), equalTo(primarySeqNoStats.getLocalCheckpoint())); - assertThat(shardStats.getShardRouting() + " global checkpoint mismatch", - seqNoStats.getGlobalCheckpoint(), equalTo(primarySeqNoStats.getGlobalCheckpoint())); - assertThat(shardStats.getShardRouting() + " max seq no mismatch", - seqNoStats.getMaxSeqNo(), equalTo(primarySeqNoStats.getMaxSeqNo())); + assertThat(replicaShardRouting + " local checkpoint mismatch", + seqNoStats.getLocalCheckpoint(), equalTo(primarySeqNoStats.getLocalCheckpoint())); + assertThat(replicaShardRouting + " global checkpoint mismatch", + seqNoStats.getGlobalCheckpoint(), equalTo(primarySeqNoStats.getGlobalCheckpoint())); + assertThat(replicaShardRouting + " max seq no mismatch", + seqNoStats.getMaxSeqNo(), equalTo(primarySeqNoStats.getMaxSeqNo())); // the local knowledge on the primary of the global checkpoint equals the global checkpoint on the shard - assertThat( - seqNoStats.getGlobalCheckpoint(), - equalTo(globalCheckpoints.get(shardStats.getShardRouting().allocationId().getId()))); + assertThat(replicaShardRouting + " global checkpoint syncs mismatch", seqNoStats.getGlobalCheckpoint(), + equalTo(syncGlobalCheckpoints.get(replicaShardRouting.allocationId().getId()))); } } } From 86feb7713b1903c2f1aca1a1de44c5477bbe40ae Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Thu, 30 Aug 2018 18:13:50 -0700 Subject: [PATCH 247/283] [MUTE] SmokeTestWatcherWithSecurityIT flaky tests --- .../elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java index 665b92bbc0e3..538d54416bf6 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java @@ -158,6 +158,7 @@ public void testSearchInputHasPermissions() throws Exception { assertThat(conditionMet, is(true)); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/29893") public void testSearchInputWithInsufficientPrivileges() throws Exception { String indexName = "index_not_allowed_to_read"; try (XContentBuilder builder = jsonBuilder()) { @@ -213,6 +214,7 @@ public void testSearchTransformHasPermissions() throws Exception { assertThat(value, is("15")); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33291") public void testSearchTransformInsufficientPermissions() throws Exception { try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); From 6dd0aa54f6d0ad44219b3b416438b1be57cb37e5 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 30 Aug 2018 22:11:23 -0400 Subject: [PATCH 248/283] Integrates soft-deletes into Elasticsearch (#33222) This PR integrates Lucene soft-deletes(LUCENE-8200) into Elasticsearch. Highlight works in this PR include: - Replace hard-deletes by soft-deletes in InternalEngine - Use _recovery_source if _source is disabled or modified (#31106) - Soft-deletes retention policy based on the global checkpoint (#30335) - Read operation history from Lucene instead of translog (#30120) - Use Lucene history in peer-recovery (#30522) Relates #30086 Closes #29530 --- These works have been done by the whole team; however, these individuals (lexical order) have significant contribution in coding and reviewing: Co-authored-by: Adrien Grand jpountz@gmail.com Co-authored-by: Boaz Leskes b.leskes@gmail.com Co-authored-by: Jason Tedor jason@tedor.me Co-authored-by: Martijn van Groningen martijn.v.groningen@gmail.com Co-authored-by: Nhat Nguyen nhat.nguyen@elastic.co Co-authored-by: Simon Willnauer simonw@apache.org --- .../percolator/CandidateQueryTests.java | 8 +- .../PercolatorFieldMapperTests.java | 30 +- .../elasticsearch/common/lucene/Lucene.java | 86 ++- .../uid/PerThreadIDVersionAndSeqNoLookup.java | 21 +- .../common/settings/IndexScopedSettings.java | 2 + .../elasticsearch/index/IndexSettings.java | 38 ++ .../index/engine/CombinedDeletionPolicy.java | 12 +- .../elasticsearch/index/engine/Engine.java | 28 +- .../index/engine/EngineConfig.java | 27 +- .../index/engine/InternalEngine.java | 388 +++++++++-- .../index/engine/LuceneChangesSnapshot.java | 368 +++++++++++ .../RecoverySourcePruneMergePolicy.java | 292 +++++++++ .../index/engine/SoftDeletesPolicy.java | 120 ++++ .../index/fieldvisitor/FieldsVisitor.java | 10 +- .../index/mapper/DocumentMapper.java | 34 +- .../index/mapper/DocumentParser.java | 33 +- .../index/mapper/FieldNamesFieldMapper.java | 5 +- .../index/mapper/ParseContext.java | 20 +- .../index/mapper/ParsedDocument.java | 11 + .../index/mapper/SeqNoFieldMapper.java | 7 +- .../index/mapper/SourceFieldMapper.java | 16 +- .../elasticsearch/index/shard/IndexShard.java | 47 +- .../index/shard/PrimaryReplicaSyncer.java | 2 +- .../index/shard/StoreRecovery.java | 1 + .../org/elasticsearch/index/store/Store.java | 2 +- .../index/translog/Translog.java | 3 + .../index/translog/TranslogWriter.java | 20 +- .../translog/TruncateTranslogCommand.java | 2 + .../recovery/RecoverySourceHandler.java | 59 +- .../blobstore/BlobStoreRepository.java | 1 + .../snapshots/RestoreService.java | 4 +- .../cluster/routing/PrimaryAllocationIT.java | 1 + .../common/lucene/LuceneTests.java | 91 +++ .../discovery/AbstractDisruptionTestCase.java | 1 + .../gateway/RecoveryFromGatewayIT.java | 13 +- .../index/IndexServiceTests.java | 3 +- .../index/IndexSettingsTests.java | 8 + .../engine/CombinedDeletionPolicyTests.java | 69 +- .../index/engine/InternalEngineTests.java | 620 ++++++++++++------ .../engine/LuceneChangesSnapshotTests.java | 289 ++++++++ .../RecoverySourcePruneMergePolicyTests.java | 161 +++++ .../index/engine/SoftDeletesPolicyTests.java | 75 +++ .../index/mapper/DocumentParserTests.java | 10 +- .../index/mapper/DynamicMappingTests.java | 6 +- .../IndexLevelReplicationTests.java | 29 +- .../RecoveryDuringReplicationTests.java | 11 +- .../index/shard/IndexShardTests.java | 58 +- .../shard/PrimaryReplicaSyncerTests.java | 21 +- .../index/shard/RefreshListenersTests.java | 4 +- .../indices/recovery/IndexRecoveryIT.java | 6 + .../PeerRecoveryTargetServiceTests.java | 2 + .../recovery/RecoverySourceHandlerTests.java | 6 - .../indices/recovery/RecoveryTests.java | 80 ++- .../indices/stats/IndexStatsIT.java | 37 +- .../AbstractSnapshotIntegTestCase.java | 6 + .../SharedClusterSnapshotRestoreIT.java | 13 +- .../versioning/SimpleVersioningIT.java | 23 + .../index/engine/EngineTestCase.java | 400 ++++++++++- .../ESIndexLevelReplicationTestCase.java | 27 +- .../index/shard/IndexShardTestCase.java | 131 ++-- .../elasticsearch/test/ESIntegTestCase.java | 4 + .../test/ESSingleNodeTestCase.java | 9 + .../test/InternalTestCluster.java | 20 + 63 files changed, 3432 insertions(+), 499 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java create mode 100644 server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java create mode 100644 server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java create mode 100644 server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java index e6d637aabb14..9c8979601e8d 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java @@ -77,6 +77,7 @@ import org.apache.lucene.store.RAMDirectory; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -87,6 +88,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; @@ -1109,7 +1111,11 @@ private void duelRun(PercolateQuery.QueryStore queryStore, MemoryIndex memoryInd } private void addQuery(Query query, List docs) { - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(query, parseContext); ParseContext.Document queryDocument = parseContext.doc(); diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java index ecff48b344c5..80524a2f862f 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java @@ -42,6 +42,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -58,6 +59,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.DocumentMapperParser; import org.elasticsearch.index.mapper.MapperParsingException; @@ -182,7 +184,11 @@ public void testExtractTerms() throws Exception { DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); ParseContext.Document document = parseContext.doc(); @@ -204,7 +210,7 @@ public void testExtractTerms() throws Exception { bq.add(termQuery1, Occur.MUST); bq.add(termQuery2, Occur.MUST); - parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), + parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); document = parseContext.doc(); @@ -232,8 +238,12 @@ public void testExtractRanges() throws Exception { bq.add(rangeQuery2, Occur.MUST); DocumentMapper documentMapper = mapperService.documentMapper("doc"); + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); ParseContext.Document document = parseContext.doc(); @@ -259,7 +269,7 @@ public void testExtractRanges() throws Exception { .rangeQuery(15, 20, true, true, null, null, null, null); bq.add(rangeQuery2, Occur.MUST); - parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); document = parseContext.doc(); @@ -283,7 +293,11 @@ public void testExtractTermsAndRanges_failed() throws Exception { TermRangeQuery query = new TermRangeQuery("field1", new BytesRef("a"), new BytesRef("z"), true, true); DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(query, parseContext); ParseContext.Document document = parseContext.doc(); @@ -298,7 +312,11 @@ public void testExtractTermsAndRanges_partial() throws Exception { PhraseQuery phraseQuery = new PhraseQuery("field", "term"); DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(phraseQuery, parseContext); ParseContext.Document document = parseContext.doc(); diff --git a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java index a24a6aea07fc..1c1e56878932 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java @@ -27,8 +27,10 @@ import org.apache.lucene.codecs.DocValuesFormat; import org.apache.lucene.codecs.PostingsFormat; import org.apache.lucene.document.LatLonDocValuesField; +import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FilterDirectoryReader; import org.apache.lucene.index.FilterLeafReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexFileNames; @@ -96,6 +98,8 @@ public class Lucene { assert annotation == null : "DocValuesFormat " + LATEST_DOC_VALUES_FORMAT + " is deprecated" ; } + public static final String SOFT_DELETES_FIELD = "__soft_deletes"; + public static final NamedAnalyzer STANDARD_ANALYZER = new NamedAnalyzer("_standard", AnalyzerScope.GLOBAL, new StandardAnalyzer()); public static final NamedAnalyzer KEYWORD_ANALYZER = new NamedAnalyzer("_keyword", AnalyzerScope.GLOBAL, new KeywordAnalyzer()); @@ -140,7 +144,7 @@ public static Iterable files(SegmentInfos infos) throws IOException { public static int getNumDocs(SegmentInfos info) { int numDocs = 0; for (SegmentCommitInfo si : info) { - numDocs += si.info.maxDoc() - si.getDelCount(); + numDocs += si.info.maxDoc() - si.getDelCount() - si.getSoftDelCount(); } return numDocs; } @@ -197,6 +201,7 @@ public static SegmentInfos pruneUnreferencedFiles(String segmentsFileName, Direc } final CommitPoint cp = new CommitPoint(si, directory); try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(Lucene.STANDARD_ANALYZER) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setIndexCommit(cp) .setCommitOnClose(false) .setMergePolicy(NoMergePolicy.INSTANCE) @@ -220,6 +225,7 @@ public static void cleanLuceneIndex(Directory directory) throws IOException { } } try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(Lucene.STANDARD_ANALYZER) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setMergePolicy(NoMergePolicy.INSTANCE) // no merges .setCommitOnClose(false) // no commits .setOpenMode(IndexWriterConfig.OpenMode.CREATE))) // force creation - don't append... @@ -829,4 +835,82 @@ public int length() { } }; } + + /** + * Wraps a directory reader to make all documents live except those were rolled back + * or hard-deleted due to non-aborting exceptions during indexing. + * The wrapped reader can be used to query all documents. + * + * @param in the input directory reader + * @return the wrapped reader + */ + public static DirectoryReader wrapAllDocsLive(DirectoryReader in) throws IOException { + return new DirectoryReaderWithAllLiveDocs(in); + } + + private static final class DirectoryReaderWithAllLiveDocs extends FilterDirectoryReader { + static final class LeafReaderWithLiveDocs extends FilterLeafReader { + final Bits liveDocs; + final int numDocs; + LeafReaderWithLiveDocs(LeafReader in, Bits liveDocs, int numDocs) { + super(in); + this.liveDocs = liveDocs; + this.numDocs = numDocs; + } + @Override + public Bits getLiveDocs() { + return liveDocs; + } + @Override + public int numDocs() { + return numDocs; + } + @Override + public CacheHelper getCoreCacheHelper() { + return in.getCoreCacheHelper(); + } + @Override + public CacheHelper getReaderCacheHelper() { + return null; // Modifying liveDocs + } + } + + DirectoryReaderWithAllLiveDocs(DirectoryReader in) throws IOException { + super(in, new SubReaderWrapper() { + @Override + public LeafReader wrap(LeafReader leaf) { + SegmentReader segmentReader = segmentReader(leaf); + Bits hardLiveDocs = segmentReader.getHardLiveDocs(); + if (hardLiveDocs == null) { + return new LeafReaderWithLiveDocs(leaf, null, leaf.maxDoc()); + } + // TODO: Can we avoid calculate numDocs by using SegmentReader#getSegmentInfo with LUCENE-8458? + int numDocs = 0; + for (int i = 0; i < hardLiveDocs.length(); i++) { + if (hardLiveDocs.get(i)) { + numDocs++; + } + } + return new LeafReaderWithLiveDocs(segmentReader, hardLiveDocs, numDocs); + } + }); + } + + @Override + protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException { + return wrapAllDocsLive(in); + } + + @Override + public CacheHelper getReaderCacheHelper() { + return null; // Modifying liveDocs + } + } + + /** + * Returns a numeric docvalues which can be used to soft-delete documents. + */ + public static NumericDocValuesField newSoftDeletesField() { + return new NumericDocValuesField(SOFT_DELETES_FIELD, 1); + } } diff --git a/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java b/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java index 38fcdfe5f1b6..3a037bed62b7 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java @@ -28,6 +28,7 @@ import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndSeqNo; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndVersion; import org.elasticsearch.index.mapper.SeqNoFieldMapper; @@ -66,15 +67,22 @@ final class PerThreadIDVersionAndSeqNoLookup { */ PerThreadIDVersionAndSeqNoLookup(LeafReader reader, String uidField) throws IOException { this.uidField = uidField; - Terms terms = reader.terms(uidField); + final Terms terms = reader.terms(uidField); if (terms == null) { - throw new IllegalArgumentException("reader misses the [" + uidField + "] field"); + // If a segment contains only no-ops, it does not have _uid but has both _soft_deletes and _tombstone fields. + final NumericDocValues softDeletesDV = reader.getNumericDocValues(Lucene.SOFT_DELETES_FIELD); + final NumericDocValues tombstoneDV = reader.getNumericDocValues(SeqNoFieldMapper.TOMBSTONE_NAME); + if (softDeletesDV == null || tombstoneDV == null) { + throw new IllegalArgumentException("reader does not have _uid terms but not a no-op segment; " + + "_soft_deletes [" + softDeletesDV + "], _tombstone [" + tombstoneDV + "]"); + } + termsEnum = null; + } else { + termsEnum = terms.iterator(); } - termsEnum = terms.iterator(); if (reader.getNumericDocValues(VersionFieldMapper.NAME) == null) { - throw new IllegalArgumentException("reader misses the [" + VersionFieldMapper.NAME + "] field"); + throw new IllegalArgumentException("reader misses the [" + VersionFieldMapper.NAME + "] field; _uid terms [" + terms + "]"); } - Object readerKey = null; assert (readerKey = reader.getCoreCacheHelper().getKey()) != null; this.readerKey = readerKey; @@ -111,7 +119,8 @@ public DocIdAndVersion lookupVersion(BytesRef id, LeafReaderContext context) * {@link DocIdSetIterator#NO_MORE_DOCS} is returned if not found * */ private int getDocID(BytesRef id, Bits liveDocs) throws IOException { - if (termsEnum.seekExact(id)) { + // termsEnum can possibly be null here if this leaf contains only no-ops. + if (termsEnum != null && termsEnum.seekExact(id)) { int docID = DocIdSetIterator.NO_MORE_DOCS; // there may be more than one matching docID, in the case of nested docs, so we want the last one: docsEnum = termsEnum.postings(docsEnum, 0); diff --git a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java index 46e3867f7aea..f3de294046c4 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java @@ -129,6 +129,8 @@ public final class IndexScopedSettings extends AbstractScopedSettings { IndexSettings.MAX_REGEX_LENGTH_SETTING, ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING, IndexSettings.INDEX_GC_DELETES_SETTING, + IndexSettings.INDEX_SOFT_DELETES_SETTING, + IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING, IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED_SETTING, UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING, EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING, diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 44cd743bbd42..3ea022bbebd4 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -237,6 +237,21 @@ public final class IndexSettings { public static final Setting INDEX_GC_DELETES_SETTING = Setting.timeSetting("index.gc_deletes", DEFAULT_GC_DELETES, new TimeValue(-1, TimeUnit.MILLISECONDS), Property.Dynamic, Property.IndexScope); + + /** + * Specifies if the index should use soft-delete instead of hard-delete for update/delete operations. + */ + public static final Setting INDEX_SOFT_DELETES_SETTING = + Setting.boolSetting("index.soft_deletes.enabled", false, Property.IndexScope, Property.Final); + + /** + * Controls how many soft-deleted documents will be kept around before being merged away. Keeping more deleted + * documents increases the chance of operation-based recoveries and allows querying a longer history of documents. + * If soft-deletes is enabled, an engine by default will retain all operations up to the global checkpoint. + **/ + public static final Setting INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING = + Setting.longSetting("index.soft_deletes.retention.operations", 0, 0, Property.IndexScope, Property.Dynamic); + /** * The maximum number of refresh listeners allows on this shard. */ @@ -289,6 +304,8 @@ public final class IndexSettings { private final IndexSortConfig indexSortConfig; private final IndexScopedSettings scopedSettings; private long gcDeletesInMillis = DEFAULT_GC_DELETES.millis(); + private final boolean softDeleteEnabled; + private volatile long softDeleteRetentionOperations; private volatile boolean warmerEnabled; private volatile int maxResultWindow; private volatile int maxInnerResultWindow; @@ -400,6 +417,8 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti generationThresholdSize = scopedSettings.get(INDEX_TRANSLOG_GENERATION_THRESHOLD_SIZE_SETTING); mergeSchedulerConfig = new MergeSchedulerConfig(this); gcDeletesInMillis = scopedSettings.get(INDEX_GC_DELETES_SETTING).getMillis(); + softDeleteEnabled = version.onOrAfter(Version.V_7_0_0_alpha1) && scopedSettings.get(INDEX_SOFT_DELETES_SETTING); + softDeleteRetentionOperations = scopedSettings.get(INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING); warmerEnabled = scopedSettings.get(INDEX_WARMER_ENABLED_SETTING); maxResultWindow = scopedSettings.get(MAX_RESULT_WINDOW_SETTING); maxInnerResultWindow = scopedSettings.get(MAX_INNER_RESULT_WINDOW_SETTING); @@ -458,6 +477,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti scopedSettings.addSettingsUpdateConsumer(INDEX_SEARCH_IDLE_AFTER, this::setSearchIdleAfter); scopedSettings.addSettingsUpdateConsumer(MAX_REGEX_LENGTH_SETTING, this::setMaxRegexLength); scopedSettings.addSettingsUpdateConsumer(DEFAULT_PIPELINE, this::setDefaultPipeline); + scopedSettings.addSettingsUpdateConsumer(INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING, this::setSoftDeleteRetentionOperations); } private void setSearchIdleAfter(TimeValue searchIdleAfter) { this.searchIdleAfter = searchIdleAfter; } @@ -841,4 +861,22 @@ public String getDefaultPipeline() { public void setDefaultPipeline(String defaultPipeline) { this.defaultPipeline = defaultPipeline; } + + /** + * Returns true if soft-delete is enabled. + */ + public boolean isSoftDeleteEnabled() { + return softDeleteEnabled; + } + + private void setSoftDeleteRetentionOperations(long ops) { + this.softDeleteRetentionOperations = ops; + } + + /** + * Returns the number of extra operations (i.e. soft-deleted documents) to be kept for recoveries and history purpose. + */ + public long getSoftDeleteRetentionOperations() { + return this.softDeleteRetentionOperations; + } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java index d0575c8a8c97..d10690379edd 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java +++ b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java @@ -46,14 +46,17 @@ public final class CombinedDeletionPolicy extends IndexDeletionPolicy { private final Logger logger; private final TranslogDeletionPolicy translogDeletionPolicy; + private final SoftDeletesPolicy softDeletesPolicy; private final LongSupplier globalCheckpointSupplier; private final ObjectIntHashMap snapshottedCommits; // Number of snapshots held against each commit point. private volatile IndexCommit safeCommit; // the most recent safe commit point - its max_seqno at most the persisted global checkpoint. private volatile IndexCommit lastCommit; // the most recent commit point - CombinedDeletionPolicy(Logger logger, TranslogDeletionPolicy translogDeletionPolicy, LongSupplier globalCheckpointSupplier) { + CombinedDeletionPolicy(Logger logger, TranslogDeletionPolicy translogDeletionPolicy, + SoftDeletesPolicy softDeletesPolicy, LongSupplier globalCheckpointSupplier) { this.logger = logger; this.translogDeletionPolicy = translogDeletionPolicy; + this.softDeletesPolicy = softDeletesPolicy; this.globalCheckpointSupplier = globalCheckpointSupplier; this.snapshottedCommits = new ObjectIntHashMap<>(); } @@ -80,7 +83,7 @@ public synchronized void onCommit(List commits) throws IO deleteCommit(commits.get(i)); } } - updateTranslogDeletionPolicy(); + updateRetentionPolicy(); } private void deleteCommit(IndexCommit commit) throws IOException { @@ -90,7 +93,7 @@ private void deleteCommit(IndexCommit commit) throws IOException { assert commit.isDeleted() : "Deletion commit [" + commitDescription(commit) + "] was suppressed"; } - private void updateTranslogDeletionPolicy() throws IOException { + private void updateRetentionPolicy() throws IOException { assert Thread.holdsLock(this); logger.debug("Safe commit [{}], last commit [{}]", commitDescription(safeCommit), commitDescription(lastCommit)); assert safeCommit.isDeleted() == false : "The safe commit must not be deleted"; @@ -101,6 +104,9 @@ private void updateTranslogDeletionPolicy() throws IOException { assert minRequiredGen <= lastGen : "minRequiredGen must not be greater than lastGen"; translogDeletionPolicy.setTranslogGenerationOfLastCommit(lastGen); translogDeletionPolicy.setMinTranslogGenerationForRecovery(minRequiredGen); + + softDeletesPolicy.setLocalCheckpointOfSafeCommit( + Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY))); } /** 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 4d95cf89ef00..08724d6e7942 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -58,6 +58,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.index.mapper.ParsedDocument; @@ -97,6 +98,7 @@ public abstract class Engine implements Closeable { public static final String SYNC_COMMIT_ID = "sync_id"; public static final String HISTORY_UUID_KEY = "history_uuid"; + public static final String MIN_RETAINED_SEQNO = "min_retained_seq_no"; protected final ShardId shardId; protected final String allocationId; @@ -585,18 +587,32 @@ public enum SearcherScope { public abstract void syncTranslog() throws IOException; - public abstract Closeable acquireTranslogRetentionLock(); + /** + * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed + */ + public abstract Closeable acquireRetentionLockForPeerRecovery(); + + /** + * Creates a new history snapshot from Lucene for reading operations whose seqno in the requesting seqno range (both inclusive) + */ + public abstract Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, + long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException; + + /** + * Creates a new history snapshot for reading operations since {@code startingSeqNo} (inclusive). + * The returned snapshot can be retrieved from either Lucene index or translog files. + */ + public abstract Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException; /** - * Creates a new translog snapshot from this engine for reading translog operations whose seq# at least the provided seq#. - * The caller has to close the returned snapshot after finishing the reading. + * Returns the estimated number of history operations whose seq# at least {@code startingSeqNo}(inclusive) in this engine. */ - public abstract Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException; + public abstract int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException; /** - * Returns the estimated number of translog operations in this engine whose seq# at least the provided seq#. + * Checks if this engine has every operations since {@code startingSeqNo}(inclusive) in its history (either Lucene or translog) */ - public abstract int estimateTranslogOperationsFromMinSeq(long minSeqNo); + public abstract boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException; public abstract TranslogStats getTranslogStats(); diff --git a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java index 2deae61bd52e..23a90553f60a 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java +++ b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.codec.CodecService; +import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.store.Store; import org.elasticsearch.index.translog.Translog; @@ -80,6 +81,7 @@ public final class EngineConfig { private final CircuitBreakerService circuitBreakerService; private final LongSupplier globalCheckpointSupplier; private final LongSupplier primaryTermSupplier; + private final TombstoneDocSupplier tombstoneDocSupplier; /** * Index setting to change the low level lucene codec used for writing new segments. @@ -126,7 +128,8 @@ public EngineConfig(ShardId shardId, String allocationId, ThreadPool threadPool, List externalRefreshListener, List internalRefreshListener, Sort indexSort, TranslogRecoveryRunner translogRecoveryRunner, CircuitBreakerService circuitBreakerService, - LongSupplier globalCheckpointSupplier, LongSupplier primaryTermSupplier) { + LongSupplier globalCheckpointSupplier, LongSupplier primaryTermSupplier, + TombstoneDocSupplier tombstoneDocSupplier) { this.shardId = shardId; this.allocationId = allocationId; this.indexSettings = indexSettings; @@ -164,6 +167,7 @@ public EngineConfig(ShardId shardId, String allocationId, ThreadPool threadPool, this.circuitBreakerService = circuitBreakerService; this.globalCheckpointSupplier = globalCheckpointSupplier; this.primaryTermSupplier = primaryTermSupplier; + this.tombstoneDocSupplier = tombstoneDocSupplier; } /** @@ -373,4 +377,25 @@ public CircuitBreakerService getCircuitBreakerService() { public LongSupplier getPrimaryTermSupplier() { return primaryTermSupplier; } + + /** + * A supplier supplies tombstone documents which will be used in soft-update methods. + * The returned document consists only _uid, _seqno, _term and _version fields; other metadata fields are excluded. + */ + public interface TombstoneDocSupplier { + /** + * Creates a tombstone document for a delete operation. + */ + ParsedDocument newDeleteTombstoneDoc(String type, String id); + + /** + * Creates a tombstone document for a noop operation. + * @param reason the reason of an a noop + */ + ParsedDocument newNoopTombstoneDoc(String reason); + } + + public TombstoneDocSupplier getTombstoneDocSupplier() { + return tombstoneDocSupplier; + } } 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 023e659ffabe..da4decc93b1c 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -21,16 +21,20 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LiveIndexWriterConfig; import org.apache.lucene.index.MergePolicy; import org.apache.lucene.index.SegmentCommitInfo; import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ReferenceManager; @@ -42,6 +46,7 @@ import org.apache.lucene.store.LockObtainFailedException; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.InfoStream; +import org.elasticsearch.Assertions; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.common.Nullable; @@ -61,7 +66,11 @@ import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.mapper.IdFieldMapper; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.ParseContext; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.merge.MergeStats; import org.elasticsearch.index.merge.OnGoingMerge; import org.elasticsearch.index.seqno.LocalCheckpointTracker; @@ -140,6 +149,10 @@ public class InternalEngine extends Engine { private final CounterMetric numDocDeletes = new CounterMetric(); private final CounterMetric numDocAppends = new CounterMetric(); private final CounterMetric numDocUpdates = new CounterMetric(); + private final NumericDocValuesField softDeletesField = Lucene.newSoftDeletesField(); + private final boolean softDeleteEnabled; + private final SoftDeletesPolicy softDeletesPolicy; + private final LastRefreshedCheckpointListener lastRefreshedCheckpointListener; /** * How many bytes we are currently moving to disk, via either IndexWriter.flush or refresh. IndexingMemoryController polls this @@ -184,8 +197,10 @@ public InternalEngine(EngineConfig engineConfig) { assert translog.getGeneration() != null; this.translog = translog; this.localCheckpointTracker = createLocalCheckpointTracker(localCheckpointTrackerSupplier); + this.softDeleteEnabled = engineConfig.getIndexSettings().isSoftDeleteEnabled(); + this.softDeletesPolicy = newSoftDeletesPolicy(); this.combinedDeletionPolicy = - new CombinedDeletionPolicy(logger, translogDeletionPolicy, translog::getLastSyncedGlobalCheckpoint); + new CombinedDeletionPolicy(logger, translogDeletionPolicy, softDeletesPolicy, translog::getLastSyncedGlobalCheckpoint); writer = createWriter(); bootstrapAppendOnlyInfoFromWriter(writer); historyUUID = loadHistoryUUID(writer); @@ -215,6 +230,8 @@ public InternalEngine(EngineConfig engineConfig) { for (ReferenceManager.RefreshListener listener: engineConfig.getInternalRefreshListener()) { this.internalSearcherManager.addListener(listener); } + this.lastRefreshedCheckpointListener = new LastRefreshedCheckpointListener(localCheckpointTracker.getCheckpoint()); + this.internalSearcherManager.addListener(lastRefreshedCheckpointListener); success = true; } finally { if (success == false) { @@ -240,6 +257,18 @@ private LocalCheckpointTracker createLocalCheckpointTracker( return localCheckpointTrackerSupplier.apply(maxSeqNo, localCheckpoint); } + private SoftDeletesPolicy newSoftDeletesPolicy() throws IOException { + final Map commitUserData = store.readLastCommittedSegmentsInfo().userData; + final long lastMinRetainedSeqNo; + if (commitUserData.containsKey(Engine.MIN_RETAINED_SEQNO)) { + lastMinRetainedSeqNo = Long.parseLong(commitUserData.get(Engine.MIN_RETAINED_SEQNO)); + } else { + lastMinRetainedSeqNo = Long.parseLong(commitUserData.get(SequenceNumbers.MAX_SEQ_NO)) + 1; + } + return new SoftDeletesPolicy(translog::getLastSyncedGlobalCheckpoint, lastMinRetainedSeqNo, + engineConfig.getIndexSettings().getSoftDeleteRetentionOperations()); + } + /** * This reference manager delegates all it's refresh calls to another (internal) SearcherManager * The main purpose for this is that if we have external refreshes happening we don't issue extra @@ -451,19 +480,31 @@ public void syncTranslog() throws IOException { revisitIndexDeletionPolicyOnTranslogSynced(); } + /** + * Creates a new history snapshot for reading operations since the provided seqno. + * The returned snapshot can be retrieved from either Lucene index or translog files. + */ @Override - public Closeable acquireTranslogRetentionLock() { - return getTranslog().acquireRetentionLock(); - } - - @Override - public Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException { - return getTranslog().newSnapshotFromMinSeqNo(minSeqNo); + public Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + return newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false); + } else { + return getTranslog().newSnapshotFromMinSeqNo(startingSeqNo); + } } + /** + * Returns the estimated number of history operations whose seq# at least the provided seq# in this engine. + */ @Override - public int estimateTranslogOperationsFromMinSeq(long minSeqNo) { - return getTranslog().estimateTotalOperationsFromMinSeq(minSeqNo); + public int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + try (Translog.Snapshot snapshot = newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false)) { + return snapshot.totalOperations(); + } + } else { + return getTranslog().estimateTotalOperationsFromMinSeq(startingSeqNo); + } } @Override @@ -790,7 +831,7 @@ public IndexResult index(Index index) throws IOException { if (plan.earlyResultOnPreFlightError.isPresent()) { indexResult = plan.earlyResultOnPreFlightError.get(); assert indexResult.getResultType() == Result.Type.FAILURE : indexResult.getResultType(); - } else if (plan.indexIntoLucene) { + } else if (plan.indexIntoLucene || plan.addStaleOpToLucene) { indexResult = indexIntoLucene(index, plan); } else { indexResult = new IndexResult( @@ -801,8 +842,10 @@ public IndexResult index(Index index) throws IOException { if (indexResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Index(index, indexResult)); } else if (indexResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - // if we have document failure, record it as a no-op in the translog with the generated seq_no - location = translog.add(new Translog.NoOp(indexResult.getSeqNo(), index.primaryTerm(), indexResult.getFailure().toString())); + // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no + final NoOp noOp = new NoOp(indexResult.getSeqNo(), index.primaryTerm(), index.origin(), + index.startTime(), indexResult.getFailure().toString()); + location = innerNoOp(noOp).getTranslogLocation(); } else { location = null; } @@ -854,7 +897,6 @@ private IndexingStrategy planIndexingAsNonPrimary(Index index) throws IOExceptio // unlike the primary, replicas don't really care to about creation status of documents // this allows to ignore the case where a document was found in the live version maps in // a delete state and return false for the created flag in favor of code simplicity - final OpVsLuceneDocStatus opVsLucene; if (index.seqNo() <= localCheckpointTracker.getCheckpoint()){ // the operation seq# is lower then the current local checkpoint and thus was already put into lucene // this can happen during recovery where older operations are sent from the translog that are already @@ -863,16 +905,15 @@ private IndexingStrategy planIndexingAsNonPrimary(Index index) throws IOExceptio // question may have been deleted in an out of order op that is not replayed. // See testRecoverFromStoreWithOutOfOrderDelete for an example of local recovery // See testRecoveryWithOutOfOrderDelete for an example of peer recovery - opVsLucene = OpVsLuceneDocStatus.OP_STALE_OR_EQUAL; - } else { - opVsLucene = compareOpToLuceneDocBasedOnSeqNo(index); - } - if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { plan = IndexingStrategy.processButSkipLucene(false, index.seqNo(), index.version()); } else { - plan = IndexingStrategy.processNormally( - opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, index.seqNo(), index.version() - ); + final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(index); + if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { + plan = IndexingStrategy.processAsStaleOp(softDeleteEnabled, index.seqNo(), index.version()); + } else { + plan = IndexingStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, + index.seqNo(), index.version()); + } } } return plan; @@ -921,7 +962,7 @@ private IndexResult indexIntoLucene(Index index, IndexingStrategy plan) throws IOException { assert plan.seqNoForIndexing >= 0 : "ops should have an assigned seq no.; origin: " + index.origin(); assert plan.versionForIndexing >= 0 : "version must be set. got " + plan.versionForIndexing; - assert plan.indexIntoLucene; + assert plan.indexIntoLucene || plan.addStaleOpToLucene; /* Update the document's sequence number and primary term; the sequence number here is derived here from either the sequence * number service if this is on the primary, or the existing document's sequence number if this is on the replica. The * primary term here has already been set, see IndexShard#prepareIndex where the Engine$Index operation is created. @@ -929,7 +970,9 @@ private IndexResult indexIntoLucene(Index index, IndexingStrategy plan) index.parsedDoc().updateSeqID(plan.seqNoForIndexing, index.primaryTerm()); index.parsedDoc().version().setLongValue(plan.versionForIndexing); try { - if (plan.useLuceneUpdateDocument) { + if (plan.addStaleOpToLucene) { + addStaleDocs(index.docs(), indexWriter); + } else if (plan.useLuceneUpdateDocument) { updateDocs(index.uid(), index.docs(), indexWriter); } else { // document does not exists, we can optimize for create, but double check if assertions are running @@ -993,16 +1036,29 @@ private void addDocs(final List docs, final IndexWriter i numDocAppends.inc(docs.size()); } - private static final class IndexingStrategy { + private void addStaleDocs(final List docs, final IndexWriter indexWriter) throws IOException { + assert softDeleteEnabled : "Add history documents but soft-deletes is disabled"; + for (ParseContext.Document doc : docs) { + doc.add(softDeletesField); // soft-deleted every document before adding to Lucene + } + if (docs.size() > 1) { + indexWriter.addDocuments(docs); + } else { + indexWriter.addDocument(docs.get(0)); + } + } + + protected static final class IndexingStrategy { final boolean currentNotFoundOrDeleted; final boolean useLuceneUpdateDocument; final long seqNoForIndexing; final long versionForIndexing; final boolean indexIntoLucene; + final boolean addStaleOpToLucene; final Optional earlyResultOnPreFlightError; private IndexingStrategy(boolean currentNotFoundOrDeleted, boolean useLuceneUpdateDocument, - boolean indexIntoLucene, long seqNoForIndexing, + boolean indexIntoLucene, boolean addStaleOpToLucene, long seqNoForIndexing, long versionForIndexing, IndexResult earlyResultOnPreFlightError) { assert useLuceneUpdateDocument == false || indexIntoLucene : "use lucene update is set to true, but we're not indexing into lucene"; @@ -1015,37 +1071,40 @@ private IndexingStrategy(boolean currentNotFoundOrDeleted, boolean useLuceneUpda this.seqNoForIndexing = seqNoForIndexing; this.versionForIndexing = versionForIndexing; this.indexIntoLucene = indexIntoLucene; + this.addStaleOpToLucene = addStaleOpToLucene; this.earlyResultOnPreFlightError = earlyResultOnPreFlightError == null ? Optional.empty() : Optional.of(earlyResultOnPreFlightError); } static IndexingStrategy optimizedAppendOnly(long seqNoForIndexing) { - return new IndexingStrategy(true, false, true, seqNoForIndexing, 1, null); + return new IndexingStrategy(true, false, true, false, seqNoForIndexing, 1, null); } static IndexingStrategy skipDueToVersionConflict( VersionConflictEngineException e, boolean currentNotFoundOrDeleted, long currentVersion, long term) { final IndexResult result = new IndexResult(e, currentVersion, term); return new IndexingStrategy( - currentNotFoundOrDeleted, false, false, SequenceNumbers.UNASSIGNED_SEQ_NO, Versions.NOT_FOUND, result); + currentNotFoundOrDeleted, false, false, false, SequenceNumbers.UNASSIGNED_SEQ_NO, Versions.NOT_FOUND, result); } static IndexingStrategy processNormally(boolean currentNotFoundOrDeleted, long seqNoForIndexing, long versionForIndexing) { return new IndexingStrategy(currentNotFoundOrDeleted, currentNotFoundOrDeleted == false, - true, seqNoForIndexing, versionForIndexing, null); + true, false, seqNoForIndexing, versionForIndexing, null); } static IndexingStrategy overrideExistingAsIfNotThere( long seqNoForIndexing, long versionForIndexing) { - return new IndexingStrategy(true, true, true, seqNoForIndexing, versionForIndexing, null); + return new IndexingStrategy(true, true, true, false, seqNoForIndexing, versionForIndexing, null); + } + + static IndexingStrategy processButSkipLucene(boolean currentNotFoundOrDeleted, long seqNoForIndexing, long versionForIndexing) { + return new IndexingStrategy(currentNotFoundOrDeleted, false, false, false, seqNoForIndexing, versionForIndexing, null); } - static IndexingStrategy processButSkipLucene(boolean currentNotFoundOrDeleted, - long seqNoForIndexing, long versionForIndexing) { - return new IndexingStrategy(currentNotFoundOrDeleted, false, - false, seqNoForIndexing, versionForIndexing, null); + static IndexingStrategy processAsStaleOp(boolean addStaleOpToLucene, long seqNoForIndexing, long versionForIndexing) { + return new IndexingStrategy(false, false, false, addStaleOpToLucene, seqNoForIndexing, versionForIndexing, null); } } @@ -1072,10 +1131,18 @@ private boolean assertDocDoesNotExist(final Index index, final boolean allowDele } private void updateDocs(final Term uid, final List docs, final IndexWriter indexWriter) throws IOException { - if (docs.size() > 1) { - indexWriter.updateDocuments(uid, docs); + if (softDeleteEnabled) { + if (docs.size() > 1) { + indexWriter.softUpdateDocuments(uid, docs, softDeletesField); + } else { + indexWriter.softUpdateDocument(uid, docs.get(0), softDeletesField); + } } else { - indexWriter.updateDocument(uid, docs.get(0)); + if (docs.size() > 1) { + indexWriter.updateDocuments(uid, docs); + } else { + indexWriter.updateDocument(uid, docs.get(0)); + } } numDocUpdates.inc(docs.size()); } @@ -1099,7 +1166,7 @@ public DeleteResult delete(Delete delete) throws IOException { if (plan.earlyResultOnPreflightError.isPresent()) { deleteResult = plan.earlyResultOnPreflightError.get(); - } else if (plan.deleteFromLucene) { + } else if (plan.deleteFromLucene || plan.addStaleOpToLucene) { deleteResult = deleteInLucene(delete, plan); } else { deleteResult = new DeleteResult( @@ -1110,8 +1177,10 @@ public DeleteResult delete(Delete delete) throws IOException { if (deleteResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Delete(delete, deleteResult)); } else if (deleteResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - location = translog.add(new Translog.NoOp(deleteResult.getSeqNo(), - delete.primaryTerm(), deleteResult.getFailure().toString())); + // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no + final NoOp noOp = new NoOp(deleteResult.getSeqNo(), delete.primaryTerm(), delete.origin(), + delete.startTime(), deleteResult.getFailure().toString()); + location = innerNoOp(noOp).getTranslogLocation(); } else { location = null; } @@ -1142,7 +1211,7 @@ private DeletionStrategy planDeletionAsNonPrimary(Delete delete) throws IOExcept // unlike the primary, replicas don't really care to about found status of documents // this allows to ignore the case where a document was found in the live version maps in // a delete state and return true for the found flag in favor of code simplicity - final OpVsLuceneDocStatus opVsLucene; + final DeletionStrategy plan; if (delete.seqNo() <= localCheckpointTracker.getCheckpoint()) { // the operation seq# is lower then the current local checkpoint and thus was already put into lucene // this can happen during recovery where older operations are sent from the translog that are already @@ -1151,18 +1220,15 @@ private DeletionStrategy planDeletionAsNonPrimary(Delete delete) throws IOExcept // question may have been deleted in an out of order op that is not replayed. // See testRecoverFromStoreWithOutOfOrderDelete for an example of local recovery // See testRecoveryWithOutOfOrderDelete for an example of peer recovery - opVsLucene = OpVsLuceneDocStatus.OP_STALE_OR_EQUAL; - } else { - opVsLucene = compareOpToLuceneDocBasedOnSeqNo(delete); - } - - final DeletionStrategy plan; - if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { plan = DeletionStrategy.processButSkipLucene(false, delete.seqNo(), delete.version()); } else { - plan = DeletionStrategy.processNormally( - opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, - delete.seqNo(), delete.version()); + final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(delete); + if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { + plan = DeletionStrategy.processAsStaleOp(softDeleteEnabled, false, delete.seqNo(), delete.version()); + } else { + plan = DeletionStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, + delete.seqNo(), delete.version()); + } } return plan; } @@ -1197,15 +1263,31 @@ private DeletionStrategy planDeletionAsPrimary(Delete delete) throws IOException private DeleteResult deleteInLucene(Delete delete, DeletionStrategy plan) throws IOException { try { - if (plan.currentlyDeleted == false) { + if (softDeleteEnabled) { + final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newDeleteTombstoneDoc(delete.type(), delete.id()); + assert tombstone.docs().size() == 1 : "Tombstone doc should have single doc [" + tombstone + "]"; + tombstone.updateSeqID(plan.seqNoOfDeletion, delete.primaryTerm()); + tombstone.version().setLongValue(plan.versionOfDeletion); + final ParseContext.Document doc = tombstone.docs().get(0); + assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null : + "Delete tombstone document but _tombstone field is not set [" + doc + " ]"; + doc.add(softDeletesField); + if (plan.addStaleOpToLucene || plan.currentlyDeleted) { + indexWriter.addDocument(doc); + } else { + indexWriter.softUpdateDocument(delete.uid(), doc, softDeletesField); + } + } else if (plan.currentlyDeleted == false) { // any exception that comes from this is a either an ACE or a fatal exception there // can't be any document failures coming from this indexWriter.deleteDocuments(delete.uid()); + } + if (plan.deleteFromLucene) { numDocDeletes.inc(); + versionMap.putDeleteUnderLock(delete.uid().bytes(), + new DeleteVersionValue(plan.versionOfDeletion, plan.seqNoOfDeletion, delete.primaryTerm(), + engineConfig.getThreadPool().relativeTimeInMillis())); } - versionMap.putDeleteUnderLock(delete.uid().bytes(), - new DeleteVersionValue(plan.versionOfDeletion, plan.seqNoOfDeletion, delete.primaryTerm(), - engineConfig.getThreadPool().relativeTimeInMillis())); return new DeleteResult( plan.versionOfDeletion, getPrimaryTerm(), plan.seqNoOfDeletion, plan.currentlyDeleted == false); } catch (Exception ex) { @@ -1219,15 +1301,16 @@ private DeleteResult deleteInLucene(Delete delete, DeletionStrategy plan) } } - private static final class DeletionStrategy { + protected static final class DeletionStrategy { // of a rare double delete final boolean deleteFromLucene; + final boolean addStaleOpToLucene; final boolean currentlyDeleted; final long seqNoOfDeletion; final long versionOfDeletion; final Optional earlyResultOnPreflightError; - private DeletionStrategy(boolean deleteFromLucene, boolean currentlyDeleted, + private DeletionStrategy(boolean deleteFromLucene, boolean addStaleOpToLucene, boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion, DeleteResult earlyResultOnPreflightError) { assert (deleteFromLucene && earlyResultOnPreflightError != null) == false : @@ -1235,6 +1318,7 @@ private DeletionStrategy(boolean deleteFromLucene, boolean currentlyDeleted, "deleteFromLucene: " + deleteFromLucene + " earlyResultOnPreFlightError:" + earlyResultOnPreflightError; this.deleteFromLucene = deleteFromLucene; + this.addStaleOpToLucene = addStaleOpToLucene; this.currentlyDeleted = currentlyDeleted; this.seqNoOfDeletion = seqNoOfDeletion; this.versionOfDeletion = versionOfDeletion; @@ -1246,16 +1330,22 @@ static DeletionStrategy skipDueToVersionConflict( VersionConflictEngineException e, long currentVersion, long term, boolean currentlyDeleted) { final long unassignedSeqNo = SequenceNumbers.UNASSIGNED_SEQ_NO; final DeleteResult deleteResult = new DeleteResult(e, currentVersion, term, unassignedSeqNo, currentlyDeleted == false); - return new DeletionStrategy(false, currentlyDeleted, unassignedSeqNo, Versions.NOT_FOUND, deleteResult); + return new DeletionStrategy(false, false, currentlyDeleted, unassignedSeqNo, Versions.NOT_FOUND, deleteResult); } static DeletionStrategy processNormally(boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion) { - return new DeletionStrategy(true, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); + return new DeletionStrategy(true, false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); + + } + public static DeletionStrategy processButSkipLucene(boolean currentlyDeleted, + long seqNoOfDeletion, long versionOfDeletion) { + return new DeletionStrategy(false, false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); } - public static DeletionStrategy processButSkipLucene(boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion) { - return new DeletionStrategy(false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); + static DeletionStrategy processAsStaleOp(boolean addStaleOpToLucene, boolean currentlyDeleted, + long seqNoOfDeletion, long versionOfDeletion) { + return new DeletionStrategy(false, addStaleOpToLucene, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); } } @@ -1284,7 +1374,28 @@ private NoOpResult innerNoOp(final NoOp noOp) throws IOException { assert noOp.seqNo() > SequenceNumbers.NO_OPS_PERFORMED; final long seqNo = noOp.seqNo(); try { - final NoOpResult noOpResult = new NoOpResult(getPrimaryTerm(), noOp.seqNo()); + Exception failure = null; + if (softDeleteEnabled) { + try { + final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newNoopTombstoneDoc(noOp.reason()); + tombstone.updateSeqID(noOp.seqNo(), noOp.primaryTerm()); + // A noop tombstone does not require a _version but it's added to have a fully dense docvalues for the version field. + // 1L is selected to optimize the compression because it might probably be the most common value in version field. + tombstone.version().setLongValue(1L); + assert tombstone.docs().size() == 1 : "Tombstone should have a single doc [" + tombstone + "]"; + final ParseContext.Document doc = tombstone.docs().get(0); + assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null + : "Noop tombstone document but _tombstone field is not set [" + doc + " ]"; + doc.add(softDeletesField); + indexWriter.addDocument(doc); + } catch (Exception ex) { + if (maybeFailEngine("noop", ex)) { + throw ex; + } + failure = ex; + } + } + final NoOpResult noOpResult = failure != null ? new NoOpResult(getPrimaryTerm(), noOp.seqNo(), failure) : new NoOpResult(getPrimaryTerm(), noOp.seqNo()); if (noOp.origin() != Operation.Origin.LOCAL_TRANSLOG_RECOVERY) { final Translog.Location location = translog.add(new Translog.NoOp(noOp.seqNo(), noOp.primaryTerm(), noOp.reason())); noOpResult.setTranslogLocation(location); @@ -1309,6 +1420,7 @@ final void refresh(String source, SearcherScope scope) throws EngineException { // since it flushes the index as well (though, in terms of concurrency, we are allowed to do it) // both refresh types will result in an internal refresh but only the external will also // 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(); @@ -1334,6 +1446,7 @@ final void refresh(String source, SearcherScope scope) throws EngineException { } finally { store.decRef(); } + lastRefreshedCheckpointListener.updateRefreshedCheckpoint(localCheckpointBeforeRefresh); } } catch (AlreadyClosedException e) { failOnTragicEvent(e); @@ -1348,7 +1461,8 @@ final void refresh(String source, SearcherScope scope) throws EngineException { } finally { writingBytes.addAndGet(-bytes); } - + assert lastRefreshedCheckpoint() >= localCheckpointBeforeRefresh : "refresh checkpoint was not advanced; " + + "local_checkpoint=" + localCheckpointBeforeRefresh + " refresh_checkpoint=" + lastRefreshedCheckpoint(); // TODO: maybe we should just put a scheduled job in threadPool? // We check for pruning in each delete request, but we also prune here e.g. in case a delete burst comes in and then no more deletes // for a long time: @@ -1930,7 +2044,11 @@ private IndexWriter createWriter() throws IOException { // pkg-private for testing IndexWriter createWriter(Directory directory, IndexWriterConfig iwc) throws IOException { - return new IndexWriter(directory, iwc); + if (Assertions.ENABLED) { + return new AssertingIndexWriter(directory, iwc); + } else { + return new IndexWriter(directory, iwc); + } } private IndexWriterConfig getIndexWriterConfig() { @@ -1946,11 +2064,15 @@ private IndexWriterConfig getIndexWriterConfig() { } iwc.setInfoStream(verbose ? InfoStream.getDefault() : new LoggerInfoStream(logger)); iwc.setMergeScheduler(mergeScheduler); - MergePolicy mergePolicy = config().getMergePolicy(); // Give us the opportunity to upgrade old segments while performing // background merges - mergePolicy = new ElasticsearchMergePolicy(mergePolicy); - iwc.setMergePolicy(mergePolicy); + MergePolicy mergePolicy = config().getMergePolicy(); + if (softDeleteEnabled) { + iwc.setSoftDeletesField(Lucene.SOFT_DELETES_FIELD); + mergePolicy = new RecoverySourcePruneMergePolicy(SourceFieldMapper.RECOVERY_SOURCE_NAME, softDeletesPolicy::getRetentionQuery, + new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, softDeletesPolicy::getRetentionQuery, mergePolicy)); + } + iwc.setMergePolicy(new ElasticsearchMergePolicy(mergePolicy)); iwc.setSimilarity(engineConfig.getSimilarity()); iwc.setRAMBufferSizeMB(engineConfig.getIndexingBufferSize().getMbFrac()); iwc.setCodec(engineConfig.getCodec()); @@ -2147,6 +2269,9 @@ protected void commitIndexWriter(final IndexWriter writer, final Translog transl commitData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(localCheckpointTracker.getMaxSeqNo())); commitData.put(MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID, Long.toString(maxUnsafeAutoIdTimestamp.get())); commitData.put(HISTORY_UUID_KEY, historyUUID); + if (softDeleteEnabled) { + commitData.put(Engine.MIN_RETAINED_SEQNO, Long.toString(softDeletesPolicy.getMinRetainedSeqNo())); + } logger.trace("committing writer with commit data [{}]", commitData); return commitData.entrySet().iterator(); }); @@ -2202,6 +2327,7 @@ public void onSettingsChanged() { final IndexSettings indexSettings = engineConfig.getIndexSettings(); translogDeletionPolicy.setRetentionAgeInMillis(indexSettings.getTranslogRetentionAge().getMillis()); translogDeletionPolicy.setRetentionSizeInBytes(indexSettings.getTranslogRetentionSize().getBytes()); + softDeletesPolicy.setRetentionOperations(indexSettings.getSoftDeleteRetentionOperations()); } public MergeStats getMergeStats() { @@ -2296,6 +2422,69 @@ long getNumDocUpdates() { return numDocUpdates.count(); } + @Override + public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, + long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { + // TODO: Should we defer the refresh until we really need it? + ensureOpen(); + if (lastRefreshedCheckpoint() < toSeqNo) { + refresh(source, SearcherScope.INTERNAL); + } + Searcher searcher = acquireSearcher(source, SearcherScope.INTERNAL); + try { + LuceneChangesSnapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE, fromSeqNo, toSeqNo, requiredFullRange); + searcher = null; + return snapshot; + } catch (Exception e) { + try { + maybeFailEngine("acquire changes snapshot", e); + } catch (Exception inner) { + e.addSuppressed(inner); + } + throw e; + } finally { + IOUtils.close(searcher); + } + } + + @Override + public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException { + if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + return getMinRetainedSeqNo() <= startingSeqNo; + } else { + final long currentLocalCheckpoint = getLocalCheckpointTracker().getCheckpoint(); + final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); + try (Translog.Snapshot snapshot = getTranslog().newSnapshotFromMinSeqNo(startingSeqNo)) { + Translog.Operation operation; + while ((operation = snapshot.next()) != null) { + if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { + tracker.markSeqNoAsCompleted(operation.seqNo()); + } + } + } + return tracker.getCheckpoint() >= currentLocalCheckpoint; + } + } + + /** + * Returns the minimum seqno that is retained in the Lucene index. + * Operations whose seq# are at least this value should exist in the Lucene index. + */ + final long getMinRetainedSeqNo() { + assert softDeleteEnabled : Thread.currentThread().getName(); + return softDeletesPolicy.getMinRetainedSeqNo(); + } + + @Override + public Closeable acquireRetentionLockForPeerRecovery() { + if (softDeleteEnabled) { + return softDeletesPolicy.acquireRetentionLock(); + } else { + return translog.acquireRetentionLock(); + } + } + @Override public boolean isRecovering() { return pendingTranslogRecovery.get(); @@ -2311,4 +2500,69 @@ private static Map commitDataAsMap(final IndexWriter indexWriter } return commitData; } + + private final class AssertingIndexWriter extends IndexWriter { + AssertingIndexWriter(Directory d, IndexWriterConfig conf) throws IOException { + super(d, conf); + } + @Override + public long updateDocument(Term term, Iterable doc) throws IOException { + assert softDeleteEnabled == false : "Call #updateDocument but soft-deletes is enabled"; + return super.updateDocument(term, doc); + } + @Override + public long updateDocuments(Term delTerm, Iterable> docs) throws IOException { + assert softDeleteEnabled == false : "Call #updateDocuments but soft-deletes is enabled"; + return super.updateDocuments(delTerm, docs); + } + @Override + public long deleteDocuments(Term... terms) throws IOException { + assert softDeleteEnabled == false : "Call #deleteDocuments but soft-deletes is enabled"; + return super.deleteDocuments(terms); + } + @Override + public long softUpdateDocument(Term term, Iterable doc, Field... softDeletes) throws IOException { + assert softDeleteEnabled : "Call #softUpdateDocument but soft-deletes is disabled"; + return super.softUpdateDocument(term, doc, softDeletes); + } + @Override + public long softUpdateDocuments(Term term, Iterable> docs, Field... softDeletes) throws IOException { + assert softDeleteEnabled : "Call #softUpdateDocuments but soft-deletes is disabled"; + return super.softUpdateDocuments(term, docs, softDeletes); + } + } + + /** + * Returned the last local checkpoint value has been refreshed internally. + */ + final long lastRefreshedCheckpoint() { + return lastRefreshedCheckpointListener.refreshedCheckpoint.get(); + } + + private final class LastRefreshedCheckpointListener implements ReferenceManager.RefreshListener { + final AtomicLong refreshedCheckpoint; + private long pendingCheckpoint; + + LastRefreshedCheckpointListener(long initialLocalCheckpoint) { + this.refreshedCheckpoint = new AtomicLong(initialLocalCheckpoint); + } + + @Override + public void beforeRefresh() { + // all changes until this point should be visible after refresh + pendingCheckpoint = localCheckpointTracker.getCheckpoint(); + } + + @Override + public void afterRefresh(boolean didRefresh) { + if (didRefresh) { + updateRefreshedCheckpoint(pendingCheckpoint); + } + } + + void updateRefreshedCheckpoint(long checkpoint) { + refreshedCheckpoint.updateAndGet(curr -> Math.max(curr, checkpoint)); + assert refreshedCheckpoint.get() >= checkpoint : refreshedCheckpoint.get() + " < " + checkpoint; + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java new file mode 100644 index 000000000000..deebfba9ed42 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java @@ -0,0 +1,368 @@ +/* + * 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.document.LongPoint; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.util.ArrayUtil; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.fieldvisitor.FieldsVisitor; +import org.elasticsearch.index.mapper.IdFieldMapper; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.SourceFieldMapper; +import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.mapper.VersionFieldMapper; +import org.elasticsearch.index.translog.Translog; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A {@link Translog.Snapshot} from changes in a Lucene index + */ +final class LuceneChangesSnapshot implements Translog.Snapshot { + static final int DEFAULT_BATCH_SIZE = 1024; + + private final int searchBatchSize; + private final long fromSeqNo, toSeqNo; + private long lastSeenSeqNo; + private int skippedOperations; + private final boolean requiredFullRange; + + private final IndexSearcher indexSearcher; + private final MapperService mapperService; + private int docIndex = 0; + private final int totalHits; + private ScoreDoc[] scoreDocs; + private final ParallelArray parallelArray; + private final Closeable onClose; + + /** + * Creates a new "translog" snapshot from Lucene for reading operations whose seq# in the specified range. + * + * @param engineSearcher the internal engine searcher which will be taken over if the snapshot is opened successfully + * @param mapperService the mapper service which will be mainly used to resolve the document's type and uid + * @param searchBatchSize the number of documents should be returned by each search + * @param fromSeqNo the min requesting seq# - inclusive + * @param toSeqNo the maximum requesting seq# - inclusive + * @param requiredFullRange if true, the snapshot will strictly check for the existence of operations between fromSeqNo and toSeqNo + */ + LuceneChangesSnapshot(Engine.Searcher engineSearcher, MapperService mapperService, int searchBatchSize, + long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { + if (fromSeqNo < 0 || toSeqNo < 0 || fromSeqNo > toSeqNo) { + throw new IllegalArgumentException("Invalid range; from_seqno [" + fromSeqNo + "], to_seqno [" + toSeqNo + "]"); + } + if (searchBatchSize <= 0) { + throw new IllegalArgumentException("Search_batch_size must be positive [" + searchBatchSize + "]"); + } + final AtomicBoolean closed = new AtomicBoolean(); + this.onClose = () -> { + if (closed.compareAndSet(false, true)) { + IOUtils.close(engineSearcher); + } + }; + this.mapperService = mapperService; + this.searchBatchSize = searchBatchSize; + this.fromSeqNo = fromSeqNo; + this.toSeqNo = toSeqNo; + this.lastSeenSeqNo = fromSeqNo - 1; + this.requiredFullRange = requiredFullRange; + this.indexSearcher = new IndexSearcher(Lucene.wrapAllDocsLive(engineSearcher.getDirectoryReader())); + this.indexSearcher.setQueryCache(null); + this.parallelArray = new ParallelArray(searchBatchSize); + final TopDocs topDocs = searchOperations(null); + this.totalHits = Math.toIntExact(topDocs.totalHits); + this.scoreDocs = topDocs.scoreDocs; + fillParallelArray(scoreDocs, parallelArray); + } + + @Override + public void close() throws IOException { + onClose.close(); + } + + @Override + public int totalOperations() { + return totalHits; + } + + @Override + public int skippedOperations() { + return skippedOperations; + } + + @Override + public Translog.Operation next() throws IOException { + Translog.Operation op = null; + for (int idx = nextDocIndex(); idx != -1; idx = nextDocIndex()) { + op = readDocAsOp(idx); + if (op != null) { + break; + } + } + if (requiredFullRange) { + rangeCheck(op); + } + if (op != null) { + lastSeenSeqNo = op.seqNo(); + } + return op; + } + + private void rangeCheck(Translog.Operation op) { + if (op == null) { + if (lastSeenSeqNo < toSeqNo) { + throw new IllegalStateException("Not all operations between from_seqno [" + fromSeqNo + "] " + + "and to_seqno [" + toSeqNo + "] found; prematurely terminated last_seen_seqno [" + lastSeenSeqNo + "]"); + } + } else { + final long expectedSeqNo = lastSeenSeqNo + 1; + if (op.seqNo() != expectedSeqNo) { + throw new IllegalStateException("Not all operations between from_seqno [" + fromSeqNo + "] " + + "and to_seqno [" + toSeqNo + "] found; expected seqno [" + expectedSeqNo + "]; found [" + op + "]"); + } + } + } + + private int nextDocIndex() throws IOException { + // we have processed all docs in the current search - fetch the next batch + if (docIndex == scoreDocs.length && docIndex > 0) { + final ScoreDoc prev = scoreDocs[scoreDocs.length - 1]; + scoreDocs = searchOperations(prev).scoreDocs; + fillParallelArray(scoreDocs, parallelArray); + docIndex = 0; + } + if (docIndex < scoreDocs.length) { + int idx = docIndex; + docIndex++; + return idx; + } + return -1; + } + + private void fillParallelArray(ScoreDoc[] scoreDocs, ParallelArray parallelArray) throws IOException { + if (scoreDocs.length > 0) { + for (int i = 0; i < scoreDocs.length; i++) { + scoreDocs[i].shardIndex = i; + } + // for better loading performance we sort the array by docID and + // then visit all leaves in order. + ArrayUtil.introSort(scoreDocs, Comparator.comparingInt(i -> i.doc)); + int docBase = -1; + int maxDoc = 0; + List leaves = indexSearcher.getIndexReader().leaves(); + int readerIndex = 0; + CombinedDocValues combinedDocValues = null; + LeafReaderContext leaf = null; + for (int i = 0; i < scoreDocs.length; i++) { + ScoreDoc scoreDoc = scoreDocs[i]; + if (scoreDoc.doc >= docBase + maxDoc) { + do { + leaf = leaves.get(readerIndex++); + docBase = leaf.docBase; + maxDoc = leaf.reader().maxDoc(); + } while (scoreDoc.doc >= docBase + maxDoc); + combinedDocValues = new CombinedDocValues(leaf.reader()); + } + final int segmentDocID = scoreDoc.doc - docBase; + final int index = scoreDoc.shardIndex; + parallelArray.leafReaderContexts[index] = leaf; + parallelArray.seqNo[index] = combinedDocValues.docSeqNo(segmentDocID); + parallelArray.primaryTerm[index] = combinedDocValues.docPrimaryTerm(segmentDocID); + parallelArray.version[index] = combinedDocValues.docVersion(segmentDocID); + parallelArray.isTombStone[index] = combinedDocValues.isTombstone(segmentDocID); + parallelArray.hasRecoverySource[index] = combinedDocValues.hasRecoverySource(segmentDocID); + } + // now sort back based on the shardIndex. we use this to store the previous index + ArrayUtil.introSort(scoreDocs, Comparator.comparingInt(i -> i.shardIndex)); + } + } + + private TopDocs searchOperations(ScoreDoc after) throws IOException { + final Query rangeQuery = LongPoint.newRangeQuery(SeqNoFieldMapper.NAME, lastSeenSeqNo + 1, toSeqNo); + final Sort sortedBySeqNoThenByTerm = new Sort( + new SortField(SeqNoFieldMapper.NAME, SortField.Type.LONG), + new SortField(SeqNoFieldMapper.PRIMARY_TERM_NAME, SortField.Type.LONG, true) + ); + return indexSearcher.searchAfter(after, rangeQuery, searchBatchSize, sortedBySeqNoThenByTerm); + } + + private Translog.Operation readDocAsOp(int docIndex) throws IOException { + final LeafReaderContext leaf = parallelArray.leafReaderContexts[docIndex]; + final int segmentDocID = scoreDocs[docIndex].doc - leaf.docBase; + final long primaryTerm = parallelArray.primaryTerm[docIndex]; + // We don't have to read the nested child documents - those docs don't have primary terms. + if (primaryTerm == -1) { + skippedOperations++; + return null; + } + final long seqNo = parallelArray.seqNo[docIndex]; + // Only pick the first seen seq# + if (seqNo == lastSeenSeqNo) { + skippedOperations++; + return null; + } + final long version = parallelArray.version[docIndex]; + final String sourceField = parallelArray.hasRecoverySource[docIndex] ? SourceFieldMapper.RECOVERY_SOURCE_NAME : + SourceFieldMapper.NAME; + final FieldsVisitor fields = new FieldsVisitor(true, sourceField); + leaf.reader().document(segmentDocID, fields); + fields.postProcess(mapperService); + + final Translog.Operation op; + final boolean isTombstone = parallelArray.isTombStone[docIndex]; + if (isTombstone && fields.uid() == null) { + op = new Translog.NoOp(seqNo, primaryTerm, fields.source().utf8ToString()); + assert version == 1L : "Noop tombstone should have version 1L; actual version [" + version + "]"; + assert assertDocSoftDeleted(leaf.reader(), segmentDocID) : "Noop but soft_deletes field is not set [" + op + "]"; + } else { + final String id = fields.uid().id(); + final String type = fields.uid().type(); + final Term uid = new Term(IdFieldMapper.NAME, Uid.encodeId(id)); + if (isTombstone) { + op = new Translog.Delete(type, id, uid, seqNo, primaryTerm, version); + assert assertDocSoftDeleted(leaf.reader(), segmentDocID) : "Delete op but soft_deletes field is not set [" + op + "]"; + } else { + final BytesReference source = fields.source(); + if (source == null) { + // TODO: Callers should ask for the range that source should be retained. Thus we should always + // check for the existence source once we make peer-recovery to send ops after the local checkpoint. + if (requiredFullRange) { + throw new IllegalStateException("source not found for seqno=" + seqNo + + " from_seqno=" + fromSeqNo + " to_seqno=" + toSeqNo); + } else { + skippedOperations++; + return null; + } + } + // TODO: pass the latest timestamp from engine. + final long autoGeneratedIdTimestamp = -1; + op = new Translog.Index(type, id, seqNo, primaryTerm, version, + source.toBytesRef().bytes, fields.routing(), autoGeneratedIdTimestamp); + } + } + assert fromSeqNo <= op.seqNo() && op.seqNo() <= toSeqNo && lastSeenSeqNo < op.seqNo() : "Unexpected operation; " + + "last_seen_seqno [" + lastSeenSeqNo + "], from_seqno [" + fromSeqNo + "], to_seqno [" + toSeqNo + "], op [" + op + "]"; + return op; + } + + private boolean assertDocSoftDeleted(LeafReader leafReader, int segmentDocId) throws IOException { + final NumericDocValues ndv = leafReader.getNumericDocValues(Lucene.SOFT_DELETES_FIELD); + if (ndv == null || ndv.advanceExact(segmentDocId) == false) { + throw new IllegalStateException("DocValues for field [" + Lucene.SOFT_DELETES_FIELD + "] is not found"); + } + return ndv.longValue() == 1; + } + + private static final class ParallelArray { + final LeafReaderContext[] leafReaderContexts; + final long[] version; + final long[] seqNo; + final long[] primaryTerm; + final boolean[] isTombStone; + final boolean[] hasRecoverySource; + + ParallelArray(int size) { + version = new long[size]; + seqNo = new long[size]; + primaryTerm = new long[size]; + isTombStone = new boolean[size]; + hasRecoverySource = new boolean[size]; + leafReaderContexts = new LeafReaderContext[size]; + } + } + + private static final class CombinedDocValues { + private final NumericDocValues versionDV; + private final NumericDocValues seqNoDV; + private final NumericDocValues primaryTermDV; + private final NumericDocValues tombstoneDV; + private final NumericDocValues recoverySource; + + CombinedDocValues(LeafReader leafReader) throws IOException { + this.versionDV = Objects.requireNonNull(leafReader.getNumericDocValues(VersionFieldMapper.NAME), "VersionDV is missing"); + this.seqNoDV = Objects.requireNonNull(leafReader.getNumericDocValues(SeqNoFieldMapper.NAME), "SeqNoDV is missing"); + this.primaryTermDV = Objects.requireNonNull( + leafReader.getNumericDocValues(SeqNoFieldMapper.PRIMARY_TERM_NAME), "PrimaryTermDV is missing"); + this.tombstoneDV = leafReader.getNumericDocValues(SeqNoFieldMapper.TOMBSTONE_NAME); + this.recoverySource = leafReader.getNumericDocValues(SourceFieldMapper.RECOVERY_SOURCE_NAME); + } + + long docVersion(int segmentDocId) throws IOException { + assert versionDV.docID() < segmentDocId; + if (versionDV.advanceExact(segmentDocId) == false) { + throw new IllegalStateException("DocValues for field [" + VersionFieldMapper.NAME + "] is not found"); + } + return versionDV.longValue(); + } + + long docSeqNo(int segmentDocId) throws IOException { + assert seqNoDV.docID() < segmentDocId; + if (seqNoDV.advanceExact(segmentDocId) == false) { + throw new IllegalStateException("DocValues for field [" + SeqNoFieldMapper.NAME + "] is not found"); + } + return seqNoDV.longValue(); + } + + long docPrimaryTerm(int segmentDocId) throws IOException { + if (primaryTermDV == null) { + return -1L; + } + assert primaryTermDV.docID() < segmentDocId; + // Use -1 for docs which don't have primary term. The caller considers those docs as nested docs. + if (primaryTermDV.advanceExact(segmentDocId) == false) { + return -1; + } + return primaryTermDV.longValue(); + } + + boolean isTombstone(int segmentDocId) throws IOException { + if (tombstoneDV == null) { + return false; + } + assert tombstoneDV.docID() < segmentDocId; + return tombstoneDV.advanceExact(segmentDocId) && tombstoneDV.longValue() > 0; + } + + boolean hasRecoverySource(int segmentDocId) throws IOException { + if (recoverySource == null) { + return false; + } + assert recoverySource.docID() < segmentDocId; + return recoverySource.advanceExact(segmentDocId); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java b/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java new file mode 100644 index 000000000000..fde97562de8f --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java @@ -0,0 +1,292 @@ +/* + * 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.codecs.DocValuesProducer; +import org.apache.lucene.codecs.StoredFieldsReader; +import org.apache.lucene.index.BinaryDocValues; +import org.apache.lucene.index.CodecReader; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FilterCodecReader; +import org.apache.lucene.index.FilterNumericDocValues; +import org.apache.lucene.index.MergePolicy; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.OneMergeWrappingMergePolicy; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.index.StoredFieldVisitor; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.ConjunctionDISI; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.DocValuesFieldExistsQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.BitSet; +import org.apache.lucene.util.BitSetIterator; + +import java.io.IOException; +import java.util.Arrays; +import java.util.function.Supplier; + +final class RecoverySourcePruneMergePolicy extends OneMergeWrappingMergePolicy { + RecoverySourcePruneMergePolicy(String recoverySourceField, Supplier retainSourceQuerySupplier, MergePolicy in) { + super(in, toWrap -> new OneMerge(toWrap.segments) { + @Override + public CodecReader wrapForMerge(CodecReader reader) throws IOException { + CodecReader wrapped = toWrap.wrapForMerge(reader); + return wrapReader(recoverySourceField, wrapped, retainSourceQuerySupplier); + } + }); + } + + // pkg private for testing + static CodecReader wrapReader(String recoverySourceField, CodecReader reader, Supplier retainSourceQuerySupplier) + throws IOException { + NumericDocValues recoverySource = reader.getNumericDocValues(recoverySourceField); + if (recoverySource == null || recoverySource.nextDoc() == DocIdSetIterator.NO_MORE_DOCS) { + return reader; // early terminate - nothing to do here since non of the docs has a recovery source anymore. + } + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new DocValuesFieldExistsQuery(recoverySourceField), BooleanClause.Occur.FILTER); + builder.add(retainSourceQuerySupplier.get(), BooleanClause.Occur.FILTER); + IndexSearcher s = new IndexSearcher(reader); + s.setQueryCache(null); + Weight weight = s.createWeight(s.rewrite(builder.build()), false, 1.0f); + Scorer scorer = weight.scorer(reader.getContext()); + if (scorer != null) { + return new SourcePruningFilterCodecReader(recoverySourceField, reader, BitSet.of(scorer.iterator(), reader.maxDoc())); + } else { + return new SourcePruningFilterCodecReader(recoverySourceField, reader, null); + } + } + + private static class SourcePruningFilterCodecReader extends FilterCodecReader { + private final BitSet recoverySourceToKeep; + private final String recoverySourceField; + + SourcePruningFilterCodecReader(String recoverySourceField, CodecReader reader, BitSet recoverySourceToKeep) { + super(reader); + this.recoverySourceField = recoverySourceField; + this.recoverySourceToKeep = recoverySourceToKeep; + } + + @Override + public DocValuesProducer getDocValuesReader() { + DocValuesProducer docValuesReader = super.getDocValuesReader(); + return new FilterDocValuesProducer(docValuesReader) { + @Override + public NumericDocValues getNumeric(FieldInfo field) throws IOException { + NumericDocValues numeric = super.getNumeric(field); + if (recoverySourceField.equals(field.name)) { + assert numeric != null : recoverySourceField + " must have numeric DV but was null"; + final DocIdSetIterator intersection; + if (recoverySourceToKeep == null) { + // we can't return null here lucenes DocIdMerger expects an instance + intersection = DocIdSetIterator.empty(); + } else { + intersection = ConjunctionDISI.intersectIterators(Arrays.asList(numeric, + new BitSetIterator(recoverySourceToKeep, recoverySourceToKeep.length()))); + } + return new FilterNumericDocValues(numeric) { + @Override + public int nextDoc() throws IOException { + return intersection.nextDoc(); + } + + @Override + public int advance(int target) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean advanceExact(int target) { + throw new UnsupportedOperationException(); + } + }; + + } + return numeric; + } + }; + } + + @Override + public StoredFieldsReader getFieldsReader() { + StoredFieldsReader fieldsReader = super.getFieldsReader(); + return new FilterStoredFieldsReader(fieldsReader) { + @Override + public void visitDocument(int docID, StoredFieldVisitor visitor) throws IOException { + if (recoverySourceToKeep != null && recoverySourceToKeep.get(docID)) { + super.visitDocument(docID, visitor); + } else { + super.visitDocument(docID, new FilterStoredFieldVisitor(visitor) { + @Override + public Status needsField(FieldInfo fieldInfo) throws IOException { + if (recoverySourceField.equals(fieldInfo.name)) { + return Status.NO; + } + return super.needsField(fieldInfo); + } + }); + } + } + }; + } + + @Override + public CacheHelper getCoreCacheHelper() { + return null; + } + + @Override + public CacheHelper getReaderCacheHelper() { + return null; + } + + private static class FilterDocValuesProducer extends DocValuesProducer { + private final DocValuesProducer in; + + FilterDocValuesProducer(DocValuesProducer in) { + this.in = in; + } + + @Override + public NumericDocValues getNumeric(FieldInfo field) throws IOException { + return in.getNumeric(field); + } + + @Override + public BinaryDocValues getBinary(FieldInfo field) throws IOException { + return in.getBinary(field); + } + + @Override + public SortedDocValues getSorted(FieldInfo field) throws IOException { + return in.getSorted(field); + } + + @Override + public SortedNumericDocValues getSortedNumeric(FieldInfo field) throws IOException { + return in.getSortedNumeric(field); + } + + @Override + public SortedSetDocValues getSortedSet(FieldInfo field) throws IOException { + return in.getSortedSet(field); + } + + @Override + public void checkIntegrity() throws IOException { + in.checkIntegrity(); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public long ramBytesUsed() { + return in.ramBytesUsed(); + } + } + + private static class FilterStoredFieldsReader extends StoredFieldsReader { + + private final StoredFieldsReader fieldsReader; + + FilterStoredFieldsReader(StoredFieldsReader fieldsReader) { + this.fieldsReader = fieldsReader; + } + + @Override + public long ramBytesUsed() { + return fieldsReader.ramBytesUsed(); + } + + @Override + public void close() throws IOException { + fieldsReader.close(); + } + + @Override + public void visitDocument(int docID, StoredFieldVisitor visitor) throws IOException { + fieldsReader.visitDocument(docID, visitor); + } + + @Override + public StoredFieldsReader clone() { + return fieldsReader.clone(); + } + + @Override + public void checkIntegrity() throws IOException { + fieldsReader.checkIntegrity(); + } + } + + private static class FilterStoredFieldVisitor extends StoredFieldVisitor { + private final StoredFieldVisitor visitor; + + FilterStoredFieldVisitor(StoredFieldVisitor visitor) { + this.visitor = visitor; + } + + @Override + public void binaryField(FieldInfo fieldInfo, byte[] value) throws IOException { + visitor.binaryField(fieldInfo, value); + } + + @Override + public void stringField(FieldInfo fieldInfo, byte[] value) throws IOException { + visitor.stringField(fieldInfo, value); + } + + @Override + public void intField(FieldInfo fieldInfo, int value) throws IOException { + visitor.intField(fieldInfo, value); + } + + @Override + public void longField(FieldInfo fieldInfo, long value) throws IOException { + visitor.longField(fieldInfo, value); + } + + @Override + public void floatField(FieldInfo fieldInfo, float value) throws IOException { + visitor.floatField(fieldInfo, value); + } + + @Override + public void doubleField(FieldInfo fieldInfo, double value) throws IOException { + visitor.doubleField(fieldInfo, value); + } + + @Override + public Status needsField(FieldInfo fieldInfo) throws IOException { + return visitor.needsField(fieldInfo); + } + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java new file mode 100644 index 000000000000..af2ded8c4662 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java @@ -0,0 +1,120 @@ +/* + * 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.document.LongPoint; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.index.translog.Translog; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.LongSupplier; + +/** + * A policy that controls how many soft-deleted documents should be retained for peer-recovery and querying history changes purpose. + */ +final class SoftDeletesPolicy { + private final LongSupplier globalCheckpointSupplier; + private long localCheckpointOfSafeCommit; + // This lock count is used to prevent `minRetainedSeqNo` from advancing. + private int retentionLockCount; + // The extra number of operations before the global checkpoint are retained + private long retentionOperations; + // The min seq_no value that is retained - ops after this seq# should exist in the Lucene index. + private long minRetainedSeqNo; + + SoftDeletesPolicy(LongSupplier globalCheckpointSupplier, long minRetainedSeqNo, long retentionOperations) { + this.globalCheckpointSupplier = globalCheckpointSupplier; + this.retentionOperations = retentionOperations; + this.minRetainedSeqNo = minRetainedSeqNo; + this.localCheckpointOfSafeCommit = SequenceNumbers.NO_OPS_PERFORMED; + this.retentionLockCount = 0; + } + + /** + * Updates the number of soft-deleted documents prior to the global checkpoint to be retained + * See {@link org.elasticsearch.index.IndexSettings#INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING} + */ + synchronized void setRetentionOperations(long retentionOperations) { + this.retentionOperations = retentionOperations; + } + + /** + * Sets the local checkpoint of the current safe commit + */ + synchronized void setLocalCheckpointOfSafeCommit(long newCheckpoint) { + if (newCheckpoint < this.localCheckpointOfSafeCommit) { + throw new IllegalArgumentException("Local checkpoint can't go backwards; " + + "new checkpoint [" + newCheckpoint + "]," + "current checkpoint [" + localCheckpointOfSafeCommit + "]"); + } + this.localCheckpointOfSafeCommit = newCheckpoint; + } + + /** + * Acquires a lock on soft-deleted documents to prevent them from cleaning up in merge processes. This is necessary to + * make sure that all operations that are being retained will be retained until the lock is released. + * This is a analogy to the translog's retention lock; see {@link Translog#acquireRetentionLock()} + */ + synchronized Releasable acquireRetentionLock() { + assert retentionLockCount >= 0 : "Invalid number of retention locks [" + retentionLockCount + "]"; + retentionLockCount++; + final AtomicBoolean released = new AtomicBoolean(); + return () -> { + if (released.compareAndSet(false, true)) { + releaseRetentionLock(); + } + }; + } + + private synchronized void releaseRetentionLock() { + assert retentionLockCount > 0 : "Invalid number of retention locks [" + retentionLockCount + "]"; + retentionLockCount--; + } + + /** + * Returns the min seqno that is retained in the Lucene index. + * Operations whose seq# is least this value should exist in the Lucene index. + */ + synchronized long getMinRetainedSeqNo() { + // Do not advance if the retention lock is held + if (retentionLockCount == 0) { + // This policy retains operations for two purposes: peer-recovery and querying changes history. + // - Peer-recovery is driven by the local checkpoint of the safe commit. In peer-recovery, the primary transfers a safe commit, + // then sends ops after the local checkpoint of that commit. This requires keeping all ops after localCheckpointOfSafeCommit; + // - Changes APIs are driven the combination of the global checkpoint and retention ops. Here we prefer using the global + // checkpoint instead of max_seqno because only operations up to the global checkpoint are exposed in the the changes APIs. + final long minSeqNoForQueryingChanges = globalCheckpointSupplier.getAsLong() - retentionOperations; + final long minSeqNoToRetain = Math.min(minSeqNoForQueryingChanges, localCheckpointOfSafeCommit) + 1; + // This can go backward as the retentionOperations value can be changed in settings. + minRetainedSeqNo = Math.max(minRetainedSeqNo, minSeqNoToRetain); + } + return minRetainedSeqNo; + } + + /** + * Returns a soft-deletes retention query that will be used in {@link org.apache.lucene.index.SoftDeletesRetentionMergePolicy} + * Documents including tombstones are soft-deleted and matched this query will be retained and won't cleaned up by merges. + */ + Query getRetentionQuery() { + return LongPoint.newRangeQuery(SeqNoFieldMapper.NAME, getMinRetainedSeqNo(), Long.MAX_VALUE); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java b/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java index 4c65635c61b3..462f8ce8e68b 100644 --- a/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java +++ b/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java @@ -54,13 +54,19 @@ public class FieldsVisitor extends StoredFieldVisitor { RoutingFieldMapper.NAME)); private final boolean loadSource; + private final String sourceFieldName; private final Set requiredFields; protected BytesReference source; protected String type, id; protected Map> fieldsValues; public FieldsVisitor(boolean loadSource) { + this(loadSource, SourceFieldMapper.NAME); + } + + public FieldsVisitor(boolean loadSource, String sourceFieldName) { this.loadSource = loadSource; + this.sourceFieldName = sourceFieldName; requiredFields = new HashSet<>(); reset(); } @@ -103,7 +109,7 @@ public void postProcess(MapperService mapperService) { @Override public void binaryField(FieldInfo fieldInfo, byte[] value) throws IOException { - if (SourceFieldMapper.NAME.equals(fieldInfo.name)) { + if (sourceFieldName.equals(fieldInfo.name)) { source = new BytesArray(value); } else if (IdFieldMapper.NAME.equals(fieldInfo.name)) { id = Uid.decodeId(value); @@ -175,7 +181,7 @@ public void reset() { requiredFields.addAll(BASE_REQUIRED_FIELDS); if (loadSource) { - requiredFields.add(SourceFieldMapper.NAME); + requiredFields.add(sourceFieldName); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java index a0640ac68a99..663aa7e6f9e1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java @@ -19,11 +19,14 @@ package org.elasticsearch.index.mapper; +import org.apache.lucene.document.StoredField; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.Weight; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchGenerationException; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.text.Text; @@ -39,12 +42,15 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Stream; public class DocumentMapper implements ToXContentFragment { @@ -121,6 +127,8 @@ public DocumentMapper build(MapperService mapperService) { private final Map objectMappers; private final boolean hasNestedObjects; + private final MetadataFieldMapper[] deleteTombstoneMetadataFieldMappers; + private final MetadataFieldMapper[] noopTombstoneMetadataFieldMappers; public DocumentMapper(MapperService mapperService, Mapping mapping) { this.mapperService = mapperService; @@ -171,6 +179,15 @@ public DocumentMapper(MapperService mapperService, Mapping mapping) { } catch (Exception e) { throw new ElasticsearchGenerationException("failed to serialize source for type [" + type + "]", e); } + + final Collection deleteTombstoneMetadataFields = Arrays.asList(VersionFieldMapper.NAME, IdFieldMapper.NAME, + TypeFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME); + this.deleteTombstoneMetadataFieldMappers = Stream.of(mapping.metadataMappers) + .filter(field -> deleteTombstoneMetadataFields.contains(field.name())).toArray(MetadataFieldMapper[]::new); + final Collection noopTombstoneMetadataFields = Arrays.asList( + VersionFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME); + this.noopTombstoneMetadataFieldMappers = Stream.of(mapping.metadataMappers) + .filter(field -> noopTombstoneMetadataFields.contains(field.name())).toArray(MetadataFieldMapper[]::new); } public Mapping mapping() { @@ -242,7 +259,22 @@ public Map objectMappers() { } public ParsedDocument parse(SourceToParse source) throws MapperParsingException { - return documentParser.parseDocument(source); + return documentParser.parseDocument(source, mapping.metadataMappers); + } + + public ParsedDocument createDeleteTombstoneDoc(String index, String type, String id) throws MapperParsingException { + final SourceToParse emptySource = SourceToParse.source(index, type, id, new BytesArray("{}"), XContentType.JSON); + return documentParser.parseDocument(emptySource, deleteTombstoneMetadataFieldMappers).toTombstone(); + } + + public ParsedDocument createNoopTombstoneDoc(String index, String reason) throws MapperParsingException { + final String id = ""; // _id won't be used. + final SourceToParse sourceToParse = SourceToParse.source(index, type, id, new BytesArray("{}"), XContentType.JSON); + final ParsedDocument parsedDoc = documentParser.parseDocument(sourceToParse, noopTombstoneMetadataFieldMappers).toTombstone(); + // Store the reason of a noop as a raw string in the _source field + final BytesRef byteRef = new BytesRef(reason); + parsedDoc.rootDoc().add(new StoredField(SourceFieldMapper.NAME, byteRef.bytes, byteRef.offset, byteRef.length)); + return parsedDoc; } /** diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java index 0fd156c09053..85123f602edf 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java @@ -55,7 +55,7 @@ final class DocumentParser { this.docMapper = docMapper; } - ParsedDocument parseDocument(SourceToParse source) throws MapperParsingException { + ParsedDocument parseDocument(SourceToParse source, MetadataFieldMapper[] metadataFieldsMappers) throws MapperParsingException { validateType(source); final Mapping mapping = docMapper.mapping(); @@ -64,9 +64,9 @@ ParsedDocument parseDocument(SourceToParse source) throws MapperParsingException try (XContentParser parser = XContentHelper.createParser(docMapperParser.getXContentRegistry(), LoggingDeprecationHandler.INSTANCE, source.source(), xContentType)) { - context = new ParseContext.InternalParseContext(indexSettings.getSettings(), docMapperParser, docMapper, source, parser); + context = new ParseContext.InternalParseContext(indexSettings, docMapperParser, docMapper, source, parser); validateStart(parser); - internalParseDocument(mapping, context, parser); + internalParseDocument(mapping, metadataFieldsMappers, context, parser); validateEnd(parser); } catch (Exception e) { throw wrapInMapperParsingException(source, e); @@ -81,10 +81,11 @@ ParsedDocument parseDocument(SourceToParse source) throws MapperParsingException return parsedDocument(source, context, createDynamicUpdate(mapping, docMapper, context.getDynamicMappers())); } - private static void internalParseDocument(Mapping mapping, ParseContext.InternalParseContext context, XContentParser parser) throws IOException { + private static void internalParseDocument(Mapping mapping, MetadataFieldMapper[] metadataFieldsMappers, + ParseContext.InternalParseContext context, XContentParser parser) throws IOException { final boolean emptyDoc = isEmptyDoc(mapping, parser); - for (MetadataFieldMapper metadataMapper : mapping.metadataMappers) { + for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) { metadataMapper.preParse(context); } @@ -95,7 +96,7 @@ private static void internalParseDocument(Mapping mapping, ParseContext.Internal parseObjectOrNested(context, mapping.root); } - for (MetadataFieldMapper metadataMapper : mapping.metadataMappers) { + for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) { metadataMapper.postParse(context); } } @@ -495,7 +496,7 @@ private static void parseObject(final ParseContext context, ObjectMapper mapper, if (builder == null) { builder = new ObjectMapper.Builder(currentFieldName).enabled(true); } - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); objectMapper = builder.build(builderContext); context.addDynamicMapper(objectMapper); context.path().add(currentFieldName); @@ -538,7 +539,7 @@ private static void parseArray(ParseContext context, ObjectMapper parentMapper, if (builder == null) { parseNonDynamicArray(context, parentMapper, lastFieldName, arrayFieldName); } else { - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); mapper = builder.build(builderContext); assert mapper != null; if (mapper instanceof ArrayValueMapperParser) { @@ -696,13 +697,13 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont if (parseableAsLong && context.root().numericDetection()) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.LONG); if (builder == null) { - builder = newLongBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newLongBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } else if (parseableAsDouble && context.root().numericDetection()) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.DOUBLE); if (builder == null) { - builder = newFloatBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newFloatBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } else if (parseableAsLong == false && parseableAsDouble == false && context.root().dateDetection()) { @@ -718,7 +719,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont } Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.DATE); if (builder == null) { - builder = newDateBuilder(currentFieldName, dateTimeFormatter, Version.indexCreated(context.indexSettings())); + builder = newDateBuilder(currentFieldName, dateTimeFormatter, context.indexSettings().getIndexVersionCreated()); } if (builder instanceof DateFieldMapper.Builder) { DateFieldMapper.Builder dateBuilder = (DateFieldMapper.Builder) builder; @@ -741,7 +742,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont if (numberType == XContentParser.NumberType.INT || numberType == XContentParser.NumberType.LONG) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.LONG); if (builder == null) { - builder = newLongBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newLongBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } else if (numberType == XContentParser.NumberType.FLOAT || numberType == XContentParser.NumberType.DOUBLE) { @@ -750,7 +751,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont // no templates are defined, we use float by default instead of double // since this is much more space-efficient and should be enough most of // the time - builder = newFloatBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newFloatBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } @@ -785,7 +786,7 @@ private static void parseDynamicValue(final ParseContext context, ObjectMapper p return; } final String path = context.path().pathAsText(currentFieldName); - final Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + final Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); final MappedFieldType existingFieldType = context.mapperService().fullName(path); final Mapper.Builder builder; if (existingFieldType != null) { @@ -883,8 +884,8 @@ private static Tuple getDynamicParentMapper(ParseContext if (builder == null) { builder = new ObjectMapper.Builder(paths[i]).enabled(true); } - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); - mapper = (ObjectMapper) builder.build(builderContext); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), + context.path()); mapper = (ObjectMapper) builder.build(builderContext); if (mapper.nested() != ObjectMapper.Nested.NO) { throw new MapperParsingException("It is forbidden to create dynamic nested objects ([" + context.path().pathAsText(paths[i]) + "]) through `copy_to` or dots in field names"); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java index 606777392dec..8389a3062701 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java @@ -24,7 +24,6 @@ import org.apache.lucene.index.IndexableField; import org.apache.lucene.search.Query; import org.elasticsearch.Version; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.lucene.Lucene; @@ -205,12 +204,12 @@ public FieldNamesFieldType fieldType() { } @Override - public void preParse(ParseContext context) throws IOException { + public void preParse(ParseContext context) { } @Override public void postParse(ParseContext context) throws IOException { - if (context.indexSettings().getAsVersion(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).before(Version.V_6_1_0)) { + if (context.indexSettings().getIndexVersionCreated().before(Version.V_6_1_0)) { super.parse(context); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java b/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java index b77ffee05caf..cf8cc4022fd8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java @@ -24,9 +24,8 @@ import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexableField; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.IndexSettings; import java.util.ArrayList; import java.util.Collection; @@ -196,7 +195,7 @@ public boolean isWithinMultiFields() { } @Override - public Settings indexSettings() { + public IndexSettings indexSettings() { return in.indexSettings(); } @@ -315,8 +314,7 @@ public static class InternalParseContext extends ParseContext { private final List documents; - @Nullable - private final Settings indexSettings; + private final IndexSettings indexSettings; private final SourceToParse sourceToParse; @@ -334,8 +332,8 @@ public static class InternalParseContext extends ParseContext { private final Set ignoredFields = new HashSet<>(); - public InternalParseContext(@Nullable Settings indexSettings, DocumentMapperParser docMapperParser, DocumentMapper docMapper, - SourceToParse source, XContentParser parser) { + public InternalParseContext(IndexSettings indexSettings, DocumentMapperParser docMapperParser, DocumentMapper docMapper, + SourceToParse source, XContentParser parser) { this.indexSettings = indexSettings; this.docMapper = docMapper; this.docMapperParser = docMapperParser; @@ -347,7 +345,7 @@ public InternalParseContext(@Nullable Settings indexSettings, DocumentMapperPars this.version = null; this.sourceToParse = source; this.dynamicMappers = new ArrayList<>(); - this.maxAllowedNumNestedDocs = MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING.get(indexSettings); + this.maxAllowedNumNestedDocs = indexSettings.getValue(MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING); this.numNestedDocs = 0L; } @@ -357,8 +355,7 @@ public DocumentMapperParser docMapperParser() { } @Override - @Nullable - public Settings indexSettings() { + public IndexSettings indexSettings() { return this.indexSettings; } @@ -565,8 +562,7 @@ public boolean isWithinMultiFields() { return false; } - @Nullable - public abstract Settings indexSettings(); + public abstract IndexSettings indexSettings(); public abstract SourceToParse sourceToParse(); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java index 414cb3a98eca..d2cf17ddd350 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java @@ -83,6 +83,17 @@ public void updateSeqID(long sequenceNumber, long primaryTerm) { this.seqID.primaryTerm.setLongValue(primaryTerm); } + /** + * Makes the processing document as a tombstone document rather than a regular document. + * Tombstone documents are stored in Lucene index to represent delete operations or Noops. + */ + ParsedDocument toTombstone() { + assert docs().size() == 1 : "Tombstone should have a single doc [" + docs() + "]"; + this.seqID.tombstoneField.setLongValue(1); + rootDoc().add(this.seqID.tombstoneField); + return this; + } + public String routing() { return this.routing; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java index ac3ffe462723..5a0db4163bf2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java @@ -69,26 +69,29 @@ public static class SequenceIDFields { public final Field seqNo; public final Field seqNoDocValue; public final Field primaryTerm; + public final Field tombstoneField; - public SequenceIDFields(Field seqNo, Field seqNoDocValue, Field primaryTerm) { + public SequenceIDFields(Field seqNo, Field seqNoDocValue, Field primaryTerm, Field tombstoneField) { Objects.requireNonNull(seqNo, "sequence number field cannot be null"); Objects.requireNonNull(seqNoDocValue, "sequence number dv field cannot be null"); Objects.requireNonNull(primaryTerm, "primary term field cannot be null"); this.seqNo = seqNo; this.seqNoDocValue = seqNoDocValue; this.primaryTerm = primaryTerm; + this.tombstoneField = tombstoneField; } public static SequenceIDFields emptySeqID() { return new SequenceIDFields(new LongPoint(NAME, SequenceNumbers.UNASSIGNED_SEQ_NO), new NumericDocValuesField(NAME, SequenceNumbers.UNASSIGNED_SEQ_NO), - new NumericDocValuesField(PRIMARY_TERM_NAME, 0)); + new NumericDocValuesField(PRIMARY_TERM_NAME, 0), new NumericDocValuesField(TOMBSTONE_NAME, 0)); } } public static final String NAME = "_seq_no"; public static final String CONTENT_TYPE = "_seq_no"; public static final String PRIMARY_TERM_NAME = "_primary_term"; + public static final String TOMBSTONE_NAME = "_tombstone"; public static class SeqNoDefaults { public static final String NAME = SeqNoFieldMapper.NAME; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java index f2090613c096..7bfe793bba4a 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.mapper; +import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.StoredField; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexableField; @@ -49,6 +50,7 @@ public class SourceFieldMapper extends MetadataFieldMapper { public static final String NAME = "_source"; + public static final String RECOVERY_SOURCE_NAME = "_recovery_source"; public static final String CONTENT_TYPE = "_source"; private final Function, Map> filter; @@ -224,7 +226,8 @@ public Mapper parse(ParseContext context) throws IOException { @Override protected void parseCreateField(ParseContext context, List fields) throws IOException { - BytesReference source = context.sourceToParse().source(); + BytesReference originalSource = context.sourceToParse().source(); + BytesReference source = originalSource; if (enabled && fieldType().stored() && source != null) { // Percolate and tv APIs may not set the source and that is ok, because these APIs will not index any data if (filter != null) { @@ -240,8 +243,17 @@ protected void parseCreateField(ParseContext context, List field } BytesRef ref = source.toBytesRef(); fields.add(new StoredField(fieldType().name(), ref.bytes, ref.offset, ref.length)); + } else { + source = null; } - } + + if (originalSource != null && source != originalSource && context.indexSettings().isSoftDeleteEnabled()) { + // if we omitted source or modified it we add the _recovery_source to ensure we have it for ops based recovery + BytesRef ref = originalSource.toBytesRef(); + fields.add(new StoredField(RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); + fields.add(new NumericDocValuesField(RECOVERY_SOURCE_NAME, 1)); + } + } @Override protected String contentType() { 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 e030c95b56e3..ef5f9ab0ef3e 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -92,12 +92,14 @@ import org.elasticsearch.index.flush.FlushStats; import org.elasticsearch.index.get.GetStats; import org.elasticsearch.index.get.ShardGetService; +import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.DocumentMapperForType; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.RootObjectMapper; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.merge.MergeStats; @@ -1620,25 +1622,33 @@ public void onSettingsChanged() { } /** - * Acquires a lock on the translog files, preventing them from being trimmed. + * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed */ - public Closeable acquireTranslogRetentionLock() { - return getEngine().acquireTranslogRetentionLock(); + public Closeable acquireRetentionLockForPeerRecovery() { + return getEngine().acquireRetentionLockForPeerRecovery(); } /** - * Creates a new translog snapshot for reading translog operations whose seq# at least the provided seq#. - * The caller has to close the returned snapshot after finishing the reading. + * Returns the estimated number of history operations whose seq# at least the provided seq# in this shard. */ - public Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException { - return getEngine().newTranslogSnapshotFromMinSeqNo(minSeqNo); + public int estimateNumberOfHistoryOperations(String source, long startingSeqNo) throws IOException { + return getEngine().estimateNumberOfHistoryOperations(source, mapperService, startingSeqNo); } /** - * Returns the estimated number of operations in translog whose seq# at least the provided seq#. + * Creates a new history snapshot for reading operations since the provided starting seqno (inclusive). + * The returned snapshot can be retrieved from either Lucene index or translog files. */ - public int estimateTranslogOperationsFromMinSeq(long minSeqNo) { - return getEngine().estimateTranslogOperationsFromMinSeq(minSeqNo); + public Translog.Snapshot getHistoryOperations(String source, long startingSeqNo) throws IOException { + return getEngine().readHistoryOperations(source, mapperService, startingSeqNo); + } + + /** + * Checks if we have a completed history of operations since the given starting seqno (inclusive). + * This method should be called after acquiring the retention lock; See {@link #acquireRetentionLockForPeerRecovery()} + */ + public boolean hasCompleteHistoryOperations(String source, long startingSeqNo) throws IOException { + return getEngine().hasCompleteOperationHistory(source, mapperService, startingSeqNo); } public List segments(boolean verbose) { @@ -2209,7 +2219,7 @@ private EngineConfig newEngineConfig() { IndexingMemoryController.SHARD_INACTIVE_TIME_SETTING.get(indexSettings.getSettings()), Collections.singletonList(refreshListeners), Collections.singletonList(new RefreshMetricUpdater(refreshMetric)), - indexSort, this::runTranslogRecovery, circuitBreakerService, replicationTracker, () -> operationPrimaryTerm); + indexSort, this::runTranslogRecovery, circuitBreakerService, replicationTracker, () -> operationPrimaryTerm, tombstoneDocSupplier()); } /** @@ -2648,4 +2658,19 @@ public void afterRefresh(boolean didRefresh) throws IOException { refreshMetric.inc(System.nanoTime() - currentRefreshStartTime); } } + + private EngineConfig.TombstoneDocSupplier tombstoneDocSupplier() { + final RootObjectMapper.Builder noopRootMapper = new RootObjectMapper.Builder("__noop"); + final DocumentMapper noopDocumentMapper = new DocumentMapper.Builder(noopRootMapper, mapperService).build(mapperService); + return new EngineConfig.TombstoneDocSupplier() { + @Override + public ParsedDocument newDeleteTombstoneDoc(String type, String id) { + return docMapper(type).getDocumentMapper().createDeleteTombstoneDoc(shardId.getIndexName(), type, id); + } + @Override + public ParsedDocument newNoopTombstoneDoc(String reason) { + return noopDocumentMapper.createNoopTombstoneDoc(shardId.getIndexName(), reason); + } + }; + } } diff --git a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java index 1edc0eb5dcaf..016a8afff696 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java +++ b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java @@ -89,7 +89,7 @@ public void resync(final IndexShard indexShard, final ActionListener // Wrap translog snapshot to make it synchronized as it is accessed by different threads through SnapshotSender. // Even though those calls are not concurrent, snapshot.next() uses non-synchronized state and is not multi-thread-compatible // Also fail the resync early if the shard is shutting down - snapshot = indexShard.newTranslogSnapshotFromMinSeqNo(startingSeqNo); + snapshot = indexShard.getHistoryOperations("resync", startingSeqNo); final Translog.Snapshot originalSnapshot = snapshot; final Translog.Snapshot wrappedSnapshot = new Translog.Snapshot() { @Override diff --git a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java index e9acfe3d8b06..ae3f90e63e7d 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java +++ b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java @@ -156,6 +156,7 @@ void addIndices(final RecoveryState.Index indexRecoveryStats, final Directory ta final Directory hardLinkOrCopyTarget = new org.apache.lucene.store.HardlinkCopyDirectoryWrapper(target); IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) // we don't want merges to happen here - we call maybe merge on the engine // later once we stared it up otherwise we would need to wait for it here 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 001e263ea8ff..85975bc68c85 100644 --- a/server/src/main/java/org/elasticsearch/index/store/Store.java +++ b/server/src/main/java/org/elasticsearch/index/store/Store.java @@ -1009,7 +1009,6 @@ public RecoveryDiff recoveryDiff(MetadataSnapshot recoveryTargetSnapshot) { } final String segmentId = IndexFileNames.parseSegmentName(meta.name()); final String extension = IndexFileNames.getExtension(meta.name()); - assert FIELD_INFOS_FILE_EXTENSION.equals(extension) == false || IndexFileNames.stripExtension(IndexFileNames.stripSegmentName(meta.name())).isEmpty() : "FieldInfos are generational but updateable DV are not supported in elasticsearch"; if (IndexFileNames.SEGMENTS.equals(segmentId) || DEL_FILE_EXTENSION.equals(extension) || LIV_FILE_EXTENSION.equals(extension)) { // only treat del files as per-commit files fnm files are generational but only for upgradable DV perCommitStoreFiles.add(meta); @@ -1595,6 +1594,7 @@ private static IndexWriter newIndexWriter(final IndexWriterConfig.OpenMode openM throws IOException { assert openMode == IndexWriterConfig.OpenMode.APPEND || commit == null : "can't specify create flag with a commit"; IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) .setIndexCommit(commit) // we don't want merges to happen here - we call maybe merge on the engine diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index 618aa546e425..f17acac37896 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -1261,6 +1261,8 @@ public String toString() { ", type='" + type + '\'' + ", seqNo=" + seqNo + ", primaryTerm=" + primaryTerm + + ", version=" + version + + ", autoGeneratedIdTimestamp=" + autoGeneratedIdTimestamp + '}'; } @@ -1403,6 +1405,7 @@ public String toString() { "uid=" + uid + ", seqNo=" + seqNo + ", primaryTerm=" + primaryTerm + + ", version=" + version + '}'; } } diff --git a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java index f48f2ceb7927..e0cfe9eaaff0 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java @@ -40,6 +40,7 @@ import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.LongSupplier; @@ -192,7 +193,24 @@ private synchronized boolean assertNoSeqNumberConflict(long seqNo, BytesReferenc new BufferedChecksumStreamInput(data.streamInput(), "assertion")); Translog.Operation prvOp = Translog.readOperation( new BufferedChecksumStreamInput(previous.v1().streamInput(), "assertion")); - if (newOp.equals(prvOp) == false) { + // TODO: We haven't had timestamp for Index operations in Lucene yet, we need to loosen this check without timestamp. + final boolean sameOp; + if (newOp instanceof Translog.Index && prvOp instanceof Translog.Index) { + final Translog.Index o1 = (Translog.Index) prvOp; + final Translog.Index o2 = (Translog.Index) newOp; + sameOp = Objects.equals(o1.id(), o2.id()) && Objects.equals(o1.type(), o2.type()) + && Objects.equals(o1.source(), o2.source()) && Objects.equals(o1.routing(), o2.routing()) + && o1.primaryTerm() == o2.primaryTerm() && o1.seqNo() == o2.seqNo() + && o1.version() == o2.version(); + } else if (newOp instanceof Translog.Delete && prvOp instanceof Translog.Delete) { + final Translog.Delete o1 = (Translog.Delete) newOp; + final Translog.Delete o2 = (Translog.Delete) prvOp; + sameOp = Objects.equals(o1.id(), o2.id()) && Objects.equals(o1.type(), o2.type()) + && o1.primaryTerm() == o2.primaryTerm() && o1.seqNo() == o2.seqNo() && o1.version() == o2.version(); + } else { + sameOp = false; + } + if (sameOp == false) { throw new AssertionError( "seqNo [" + seqNo + "] was processed twice in generation [" + generation + "], with different data. " + "prvOp [" + prvOp + "], newOp [" + newOp + "]", previous.v2()); diff --git a/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java b/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java index 86995ae7c5a9..a90f8af0af42 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java @@ -32,6 +32,7 @@ import org.apache.lucene.store.Lock; import org.apache.lucene.store.LockObtainFailedException; import org.apache.lucene.store.NativeFSLockFactory; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cli.EnvironmentAwareCommand; @@ -177,6 +178,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th terminal.println("Marking index with the new history uuid"); // commit the new histroy id IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) // we don't want merges to happen here - we call maybe merge on the engine // later once we stared it up otherwise we would need to wait for it here diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 352f07d57649..10f796e5e155 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -146,11 +146,11 @@ public RecoveryResponse recoverToTarget() throws IOException { assert targetShardRouting.initializing() : "expected recovery target to be initializing but was " + targetShardRouting; }, shardId + " validating recovery target ["+ request.targetAllocationId() + "] registered ", shard, cancellableThreads, logger); - try (Closeable ignored = shard.acquireTranslogRetentionLock()) { + try (Closeable ignored = shard.acquireRetentionLockForPeerRecovery()) { final long startingSeqNo; final long requiredSeqNoRangeStart; final boolean isSequenceNumberBasedRecovery = request.startingSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && - isTargetSameHistory() && isTranslogReadyForSequenceNumberBasedRecovery(); + isTargetSameHistory() && shard.hasCompleteHistoryOperations("peer-recovery", request.startingSeqNo()); if (isSequenceNumberBasedRecovery) { logger.trace("performing sequence numbers based recovery. starting at [{}]", request.startingSeqNo()); startingSeqNo = request.startingSeqNo(); @@ -162,14 +162,16 @@ public RecoveryResponse recoverToTarget() throws IOException { } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "snapshot failed", e); } - // we set this to 0 to create a translog roughly according to the retention policy - // on the target. Note that it will still filter out legacy operations with no sequence numbers - startingSeqNo = 0; - // but we must have everything above the local checkpoint in the commit + // We must have everything above the local checkpoint in the commit requiredSeqNoRangeStart = Long.parseLong(phase1Snapshot.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + // If soft-deletes enabled, we need to transfer only operations after the local_checkpoint of the commit to have + // the same history on the target. However, with translog, we need to set this to 0 to create a translog roughly + // according to the retention policy on the target. Note that it will still filter out legacy operations without seqNo. + startingSeqNo = shard.indexSettings().isSoftDeleteEnabled() ? requiredSeqNoRangeStart : 0; try { - phase1(phase1Snapshot.getIndexCommit(), () -> shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); + final int estimateNumOps = shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo); + phase1(phase1Snapshot.getIndexCommit(), () -> estimateNumOps); } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "phase1 failed", e); } finally { @@ -186,7 +188,8 @@ public RecoveryResponse recoverToTarget() throws IOException { try { // For a sequence based recovery, the target can keep its local translog - prepareTargetForTranslog(isSequenceNumberBasedRecovery == false, shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); + prepareTargetForTranslog(isSequenceNumberBasedRecovery == false, + shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo)); } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "prepare target for translog failed", e); } @@ -207,11 +210,13 @@ public RecoveryResponse recoverToTarget() throws IOException { */ cancellableThreads.execute(() -> shard.waitForOpsToComplete(endingSeqNo)); - logger.trace("all operations up to [{}] completed, which will be used as an ending sequence number", endingSeqNo); - - logger.trace("snapshot translog for recovery; current size is [{}]", shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); + if (logger.isTraceEnabled()) { + logger.trace("all operations up to [{}] completed, which will be used as an ending sequence number", endingSeqNo); + logger.trace("snapshot translog for recovery; current size is [{}]", + shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo)); + } final long targetLocalCheckpoint; - try(Translog.Snapshot snapshot = shard.newTranslogSnapshotFromMinSeqNo(startingSeqNo)) { + try (Translog.Snapshot snapshot = shard.getHistoryOperations("peer-recovery", startingSeqNo)) { targetLocalCheckpoint = phase2(startingSeqNo, requiredSeqNoRangeStart, endingSeqNo, snapshot); } catch (Exception e) { throw new RecoveryEngineException(shard.shardId(), 2, "phase2 failed", e); @@ -268,36 +273,6 @@ public void onFailure(Exception e) { }); } - /** - * Determines if the source translog is ready for a sequence-number-based peer recovery. The main condition here is that the source - * translog contains all operations above the local checkpoint on the target. We already know the that translog contains or will contain - * all ops above the source local checkpoint, so we can stop check there. - * - * @return {@code true} if the source is ready for a sequence-number-based recovery - * @throws IOException if an I/O exception occurred reading the translog snapshot - */ - boolean isTranslogReadyForSequenceNumberBasedRecovery() throws IOException { - final long startingSeqNo = request.startingSeqNo(); - assert startingSeqNo >= 0; - final long localCheckpoint = shard.getLocalCheckpoint(); - logger.trace("testing sequence numbers in range: [{}, {}]", startingSeqNo, localCheckpoint); - // the start recovery request is initialized with the starting sequence number set to the target shard's local checkpoint plus one - if (startingSeqNo - 1 <= localCheckpoint) { - final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); - try (Translog.Snapshot snapshot = shard.newTranslogSnapshotFromMinSeqNo(startingSeqNo)) { - Translog.Operation operation; - while ((operation = snapshot.next()) != null) { - if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - tracker.markSeqNoAsCompleted(operation.seqNo()); - } - } - } - return tracker.getCheckpoint() >= localCheckpoint; - } else { - return false; - } - } - /** * Perform phase1 of the recovery operations. Once this {@link IndexCommit} * snapshot has been performed no commit operations (files being fsync'd) 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 a4d6518e9af9..9469f657c96b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -1492,6 +1492,7 @@ public void restore() throws IOException { // empty shard would cause exceptions to be thrown. Since there is no data to restore from an empty // shard anyway, we just create the empty shard here and then exit. IndexWriter writer = new IndexWriter(store.directory(), new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setOpenMode(IndexWriterConfig.OpenMode.CREATE) .setCommitOnClose(true)); writer.close(); diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java index 702d63d0d940..6acdbad2ccec 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java @@ -64,6 +64,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.repositories.IndexId; @@ -120,7 +121,8 @@ public class RestoreService extends AbstractComponent implements ClusterStateApp SETTING_NUMBER_OF_SHARDS, SETTING_VERSION_CREATED, SETTING_INDEX_UUID, - SETTING_CREATION_DATE)); + SETTING_CREATION_DATE, + IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey())); // It's OK to change some settings, but we shouldn't allow simply removing them private static final Set UNREMOVABLE_SETTINGS; diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java b/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java index 90173455c3be..9786c0eaf529 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java @@ -392,6 +392,7 @@ public void testPrimaryReplicaResyncFailed() throws Exception { assertThat(shard.getLocalCheckpoint(), equalTo(numDocs + moreDocs)); } }, 30, TimeUnit.SECONDS); + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); } } diff --git a/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java b/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java index 753aedea01e0..890f6ef163b3 100644 --- a/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java +++ b/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java @@ -33,18 +33,23 @@ import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.Weight; import org.apache.lucene.store.Directory; import org.apache.lucene.store.MMapDirectory; import org.apache.lucene.store.MockDirectoryWrapper; import org.apache.lucene.util.Bits; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.test.ESTestCase; import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -53,6 +58,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import static org.hamcrest.Matchers.equalTo; + public class LuceneTests extends ESTestCase { public void testWaitForIndex() throws Exception { final MockDirectoryWrapper dir = newMockDirectory(); @@ -406,4 +413,88 @@ public void testMMapHackSupported() throws Exception { // add assume's here if needed for certain platforms, but we should know if it does not work. assertTrue("MMapDirectory does not support unmapping: " + MMapDirectory.UNMAP_NOT_SUPPORTED_REASON, MMapDirectory.UNMAP_SUPPORTED); } + + public void testWrapAllDocsLive() throws Exception { + Directory dir = newDirectory(); + IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) + .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())); + IndexWriter writer = new IndexWriter(dir, config); + int numDocs = between(1, 10); + Set liveDocs = new HashSet<>(); + for (int i = 0; i < numDocs; i++) { + String id = Integer.toString(i); + Document doc = new Document(); + doc.add(new StringField("id", id, Store.YES)); + writer.addDocument(doc); + liveDocs.add(id); + } + for (int i = 0; i < numDocs; i++) { + if (randomBoolean()) { + String id = Integer.toString(i); + Document doc = new Document(); + doc.add(new StringField("id", "v2-" + id, Store.YES)); + if (randomBoolean()) { + doc.add(Lucene.newSoftDeletesField()); + } + writer.softUpdateDocument(new Term("id", id), doc, Lucene.newSoftDeletesField()); + liveDocs.add("v2-" + id); + } + } + try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { + DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); + assertThat(reader.numDocs(), equalTo(liveDocs.size())); + IndexSearcher searcher = new IndexSearcher(reader); + Set actualDocs = new HashSet<>(); + TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); + for (ScoreDoc scoreDoc : topDocs.scoreDocs) { + actualDocs.add(reader.document(scoreDoc.doc).get("id")); + } + assertThat(actualDocs, equalTo(liveDocs)); + } + IOUtils.close(writer, dir); + } + + public void testWrapLiveDocsNotExposeAbortedDocuments() throws Exception { + Directory dir = newDirectory(); + IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) + .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())); + IndexWriter writer = new IndexWriter(dir, config); + int numDocs = between(1, 10); + List liveDocs = new ArrayList<>(); + for (int i = 0; i < numDocs; i++) { + String id = Integer.toString(i); + Document doc = new Document(); + doc.add(new StringField("id", id, Store.YES)); + if (randomBoolean()) { + doc.add(Lucene.newSoftDeletesField()); + } + writer.addDocument(doc); + liveDocs.add(id); + } + int abortedDocs = between(1, 10); + for (int i = 0; i < abortedDocs; i++) { + try { + Document doc = new Document(); + doc.add(new StringField("id", "aborted-" + i, Store.YES)); + StringReader reader = new StringReader(""); + doc.add(new TextField("other", reader)); + reader.close(); // mark the indexing hit non-aborting error + writer.addDocument(doc); + fail("index should have failed"); + } catch (Exception ignored) { } + } + try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { + DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); + assertThat(reader.maxDoc(), equalTo(numDocs + abortedDocs)); + assertThat(reader.numDocs(), equalTo(liveDocs.size())); + IndexSearcher searcher = new IndexSearcher(reader); + List actualDocs = new ArrayList<>(); + TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); + for (ScoreDoc scoreDoc : topDocs.scoreDocs) { + actualDocs.add(reader.document(scoreDoc.doc).get("id")); + } + assertThat(actualDocs, equalTo(liveDocs)); + } + IOUtils.close(writer, dir); + } } diff --git a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java index 6bdd8ea3f2e0..ac2f2b0d4f32 100644 --- a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java +++ b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java @@ -109,6 +109,7 @@ public void setDisruptionScheme(ServiceDisruptionScheme scheme) { protected void beforeIndexDeletion() throws Exception { if (disableBeforeIndexDeletion == false) { super.beforeIndexDeletion(); + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); assertSeqNos(); } } diff --git a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java index d098c4918a76..b0b6c35f92a1 100644 --- a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java @@ -40,6 +40,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardPath; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; @@ -397,7 +398,8 @@ public void testReuseInFileBasedPeerRecovery() throws Exception { .get(); logger.info("--> indexing docs"); - for (int i = 0; i < randomIntBetween(1, 1024); i++) { + int numDocs = randomIntBetween(1, 1024); + for (int i = 0; i < numDocs; i++) { client(primaryNode).prepareIndex("test", "type").setSource("field", "value").execute().actionGet(); } @@ -419,12 +421,15 @@ public void testReuseInFileBasedPeerRecovery() throws Exception { } logger.info("--> restart replica node"); + boolean softDeleteEnabled = internalCluster().getInstance(IndicesService.class, primaryNode) + .indexServiceSafe(resolveIndex("test")).getShard(0).indexSettings().isSoftDeleteEnabled(); + int moreDocs = randomIntBetween(1, 1024); internalCluster().restartNode(replicaNode, new RestartCallback() { @Override public Settings onNodeStopped(String nodeName) throws Exception { // index some more documents; we expect to reuse the files that already exist on the replica - for (int i = 0; i < randomIntBetween(1, 1024); i++) { + for (int i = 0; i < moreDocs; i++) { client(primaryNode).prepareIndex("test", "type").setSource("field", "value").execute().actionGet(); } @@ -432,8 +437,12 @@ public Settings onNodeStopped(String nodeName) throws Exception { client(primaryNode).admin().indices().prepareUpdateSettings("test").setSettings(Settings.builder() .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), "-1") .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1") + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) ).get(); client(primaryNode).admin().indices().prepareFlush("test").setForce(true).get(); + if (softDeleteEnabled) { // We need an extra flush to advance the min_retained_seqno of the SoftDeletesPolicy + client(primaryNode).admin().indices().prepareFlush("test").setForce(true).get(); + } return super.onNodeStopped(nodeName); } }); diff --git a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java index 28fa440d96ac..b0b4ec3930ad 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java @@ -32,6 +32,7 @@ import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.IndexShardTestCase; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -306,7 +307,7 @@ public void testAsyncTranslogTrimActuallyWorks() throws Exception { .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), -1)) .get(); IndexShard shard = indexService.getShard(0); - assertBusy(() -> assertThat(shard.estimateTranslogOperationsFromMinSeq(0L), equalTo(0))); + assertBusy(() -> assertThat(IndexShardTestCase.getTranslog(shard).totalOperations(), equalTo(0))); } public void testIllegalFsyncInterval() { diff --git a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java index b7da5add2acf..64a2fa69bcbd 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java @@ -553,4 +553,12 @@ public void testQueryDefaultField() { ); assertThat(index.getDefaultFields(), equalTo(Arrays.asList("body", "title"))); } + + public void testUpdateSoftDeletesFails() { + IndexScopedSettings settings = new IndexScopedSettings(Settings.EMPTY, IndexScopedSettings.BUILT_IN_INDEX_SETTINGS); + IllegalArgumentException error = expectThrows(IllegalArgumentException.class, () -> + settings.updateSettings(Settings.builder().put("index.soft_deletes.enabled", randomBoolean()).build(), + Settings.builder(), Settings.builder(), "index")); + assertThat(error.getMessage(), equalTo("final index setting [index.soft_deletes.enabled], not updateable")); + } } diff --git a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java index ea7de50b7b34..3f9fc9a0429b 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java @@ -51,20 +51,24 @@ public class CombinedDeletionPolicyTests extends ESTestCase { public void testKeepCommitsAfterGlobalCheckpoint() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); + final int extraRetainedOps = between(0, 100); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, extraRetainedOps); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); final LongArrayList maxSeqNoList = new LongArrayList(); final LongArrayList translogGenList = new LongArrayList(); final List commitList = new ArrayList<>(); int totalCommits = between(2, 20); long lastMaxSeqNo = 0; + long lastCheckpoint = lastMaxSeqNo; long lastTranslogGen = 0; final UUID translogUUID = UUID.randomUUID(); for (int i = 0; i < totalCommits; i++) { lastMaxSeqNo += between(1, 10000); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); lastTranslogGen += between(1, 100); - commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); maxSeqNoList.add(lastMaxSeqNo); translogGenList.add(lastTranslogGen); } @@ -85,14 +89,19 @@ public void testKeepCommitsAfterGlobalCheckpoint() throws Exception { } assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(translogGenList.get(keptIndex))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(commitList.get(keptIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } public void testAcquireIndexCommit() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); + final int extraRetainedOps = between(0, 100); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, extraRetainedOps); final UUID translogUUID = UUID.randomUUID(); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); long lastMaxSeqNo = between(1, 1000); + long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); long lastTranslogGen = between(1, 20); int safeIndex = 0; List commitList = new ArrayList<>(); @@ -102,8 +111,9 @@ public void testAcquireIndexCommit() throws Exception { int newCommits = between(1, 10); for (int n = 0; n < newCommits; n++) { lastMaxSeqNo += between(1, 1000); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); lastTranslogGen += between(1, 20); - commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); } // Advance the global checkpoint to between [safeIndex, safeIndex + 1) safeIndex = randomIntBetween(safeIndex, commitList.size() - 1); @@ -114,6 +124,9 @@ public void testAcquireIndexCommit() throws Exception { globalCheckpoint.set(randomLongBetween(lower, upper)); commitList.forEach(this::resetDeletion); indexPolicy.onCommit(commitList); + IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); // Captures and releases some commits int captures = between(0, 5); for (int n = 0; n < captures; n++) { @@ -132,7 +145,7 @@ public void testAcquireIndexCommit() throws Exception { snapshottingCommits.remove(snapshot); final long pendingSnapshots = snapshottingCommits.stream().filter(snapshot::equals).count(); final IndexCommit lastCommit = commitList.get(commitList.size() - 1); - final IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); + safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); assertThat(indexPolicy.releaseCommit(snapshot), equalTo(pendingSnapshots == 0 && snapshot.equals(lastCommit) == false && snapshot.equals(safeCommit) == false)); } @@ -143,6 +156,8 @@ public void testAcquireIndexCommit() throws Exception { equalTo(Long.parseLong(commitList.get(safeIndex).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(Long.parseLong(commitList.get(commitList.size() - 1).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(commitList.get(safeIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } snapshottingCommits.forEach(indexPolicy::releaseCommit); globalCheckpoint.set(randomLongBetween(lastMaxSeqNo, Long.MAX_VALUE)); @@ -154,25 +169,27 @@ public void testAcquireIndexCommit() throws Exception { assertThat(commitList.get(commitList.size() - 1).isDeleted(), equalTo(false)); assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(lastTranslogGen)); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); + IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } public void testLegacyIndex() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); final UUID translogUUID = UUID.randomUUID(); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); long legacyTranslogGen = randomNonNegativeLong(); IndexCommit legacyCommit = mockLegacyIndexCommit(translogUUID, legacyTranslogGen); - indexPolicy.onCommit(singletonList(legacyCommit)); - verify(legacyCommit, never()).delete(); - assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(legacyTranslogGen)); - assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(legacyTranslogGen)); + assertThat(CombinedDeletionPolicy.findSafeCommitPoint(singletonList(legacyCommit), globalCheckpoint.get()), + equalTo(legacyCommit)); long safeTranslogGen = randomLongBetween(legacyTranslogGen, Long.MAX_VALUE); long maxSeqNo = randomLongBetween(1, Long.MAX_VALUE); - final IndexCommit freshCommit = mockIndexCommit(maxSeqNo, translogUUID, safeTranslogGen); + final IndexCommit freshCommit = mockIndexCommit(randomLongBetween(-1, maxSeqNo), maxSeqNo, translogUUID, safeTranslogGen); globalCheckpoint.set(randomLongBetween(0, maxSeqNo - 1)); indexPolicy.onCommit(Arrays.asList(legacyCommit, freshCommit)); @@ -189,25 +206,32 @@ public void testLegacyIndex() throws Exception { verify(freshCommit, times(0)).delete(); assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(safeTranslogGen)); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(safeTranslogGen)); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), equalTo(getLocalCheckpoint(freshCommit) + 1)); } public void testDeleteInvalidCommits() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(randomNonNegativeLong()); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); final int invalidCommits = between(1, 10); final List commitList = new ArrayList<>(); for (int i = 0; i < invalidCommits; i++) { - commitList.add(mockIndexCommit(randomNonNegativeLong(), UUID.randomUUID(), randomNonNegativeLong())); + long maxSeqNo = randomNonNegativeLong(); + commitList.add(mockIndexCommit(randomLongBetween(-1, maxSeqNo), maxSeqNo, UUID.randomUUID(), randomNonNegativeLong())); } final UUID expectedTranslogUUID = UUID.randomUUID(); long lastTranslogGen = 0; final int validCommits = between(1, 10); + long lastMaxSeqNo = between(1, 1000); + long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); for (int i = 0; i < validCommits; i++) { lastTranslogGen += between(1, 1000); - commitList.add(mockIndexCommit(randomNonNegativeLong(), expectedTranslogUUID, lastTranslogGen)); + lastMaxSeqNo += between(1, 1000); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, expectedTranslogUUID, lastTranslogGen)); } // We should never keep invalid commits regardless of the value of the global checkpoint. @@ -215,21 +239,26 @@ public void testDeleteInvalidCommits() throws Exception { for (int i = 0; i < invalidCommits - 1; i++) { verify(commitList.get(i), times(1)).delete(); } + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(getLocalCheckpoint(CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get())) + 1)); } public void testCheckUnreferencedCommits() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.UNASSIGNED_SEQ_NO); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); final UUID translogUUID = UUID.randomUUID(); final TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); final List commitList = new ArrayList<>(); int totalCommits = between(2, 20); long lastMaxSeqNo = between(1, 1000); + long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); long lastTranslogGen = between(1, 50); for (int i = 0; i < totalCommits; i++) { lastMaxSeqNo += between(1, 10000); lastTranslogGen += between(1, 100); - commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); } IndexCommit safeCommit = randomFrom(commitList); globalCheckpoint.set(Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.MAX_SEQ_NO))); @@ -256,8 +285,9 @@ public void testCheckUnreferencedCommits() throws Exception { } } - IndexCommit mockIndexCommit(long maxSeqNo, UUID translogUUID, long translogGen) throws IOException { + IndexCommit mockIndexCommit(long localCheckpoint, long maxSeqNo, UUID translogUUID, long translogGen) throws IOException { final Map userData = new HashMap<>(); + userData.put(SequenceNumbers.LOCAL_CHECKPOINT_KEY, Long.toString(localCheckpoint)); userData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(maxSeqNo)); userData.put(Translog.TRANSLOG_UUID_KEY, translogUUID.toString()); userData.put(Translog.TRANSLOG_GENERATION_KEY, Long.toString(translogGen)); @@ -278,6 +308,10 @@ void resetDeletion(IndexCommit commit) { }).when(commit).delete(); } + private long getLocalCheckpoint(IndexCommit commit) throws IOException { + return Long.parseLong(commit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + } + IndexCommit mockLegacyIndexCommit(UUID translogUUID, long translogGen) throws IOException { final Map userData = new HashMap<>(); userData.put(Translog.TRANSLOG_UUID_KEY, translogUUID.toString()); @@ -287,4 +321,5 @@ IndexCommit mockLegacyIndexCommit(UUID translogUUID, long translogGen) throws IO resetDeletion(commit); return commit; } + } 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 76e05ba1e0b5..d3aead9e44e1 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.engine; +import java.io.Closeable; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.Charset; @@ -77,10 +78,12 @@ import org.apache.lucene.index.LiveIndexWriterConfig; import org.apache.lucene.index.LogByteSizeMergePolicy; import org.apache.lucene.index.LogDocMergePolicy; +import org.apache.lucene.index.MergePolicy; import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.index.NumericDocValues; import org.apache.lucene.index.PointValues; import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.index.TieredMergePolicy; import org.apache.lucene.search.IndexSearcher; @@ -114,6 +117,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver; @@ -133,6 +137,7 @@ import org.elasticsearch.index.mapper.ContentPath; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.Mapper.BuilderContext; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.MetadataFieldMapper; import org.elasticsearch.index.mapper.ParseContext; @@ -172,8 +177,10 @@ import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -247,8 +254,13 @@ public void testVersionMapAfterAutoIDDocument() throws IOException { } public void testSegments() throws Exception { + Settings settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( + IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); try (Store store = createStore(); - InternalEngine engine = createEngine(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE)) { + InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { List segments = engine.segments(false); assertThat(segments.isEmpty(), equalTo(true)); assertThat(engine.segmentsStats(false).getCount(), equalTo(0L)); @@ -1311,9 +1323,13 @@ public void testVersioningNewIndex() throws IOException { assertThat(indexResult.getVersion(), equalTo(1L)); } - public void testForceMerge() throws IOException { + public void testForceMergeWithoutSoftDeletes() throws IOException { + Settings settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); try (Store store = createStore(); - Engine engine = createEngine(config(defaultSettings, store, createTempDir(), + Engine engine = createEngine(config(IndexSettingsModule.newIndexSettings(indexMetaData), store, createTempDir(), new LogByteSizeMergePolicy(), null))) { // use log MP here we test some behavior in ESMP int numDocs = randomIntBetween(10, 100); for (int i = 0; i < numDocs; i++) { @@ -1354,6 +1370,165 @@ public void testForceMerge() throws IOException { } } + public void testForceMergeWithSoftDeletesRetention() throws Exception { + final long retainedExtraOps = randomLongBetween(0, 10); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + final MapperService mapperService = createMapperService("test"); + final Set liveDocs = new HashSet<>(); + try (Store store = createStore(); + InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get))) { + int numDocs = scaledRandomIntBetween(10, 100); + for (int i = 0; i < numDocs; i++) { + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null); + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + } + for (int i = 0; i < numDocs; i++) { + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null); + if (randomBoolean()) { + engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); + liveDocs.remove(doc.id()); + } + if (randomBoolean()) { + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + } + if (randomBoolean()) { + engine.flush(randomBoolean(), true); + } + } + engine.flush(); + + long localCheckpoint = engine.getLocalCheckpoint(); + globalCheckpoint.set(randomLongBetween(0, localCheckpoint)); + engine.syncTranslog(); + final long safeCommitCheckpoint; + try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { + safeCommitCheckpoint = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + } + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + Map ops = readAllOperationsInLucene(engine, mapperService) + .stream().collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); + for (long seqno = 0; seqno <= localCheckpoint; seqno++) { + long minSeqNoToRetain = Math.min(globalCheckpoint.get() + 1 - retainedExtraOps, safeCommitCheckpoint + 1); + String msg = "seq# [" + seqno + "], global checkpoint [" + globalCheckpoint + "], retained-ops [" + retainedExtraOps + "]"; + if (seqno < minSeqNoToRetain) { + Translog.Operation op = ops.get(seqno); + if (op != null) { + assertThat(op, instanceOf(Translog.Index.class)); + assertThat(msg, ((Translog.Index) op).id(), isIn(liveDocs)); + assertEquals(msg, ((Translog.Index) op).source(), B_1); + } + } else { + assertThat(msg, ops.get(seqno), notNullValue()); + } + } + settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); + indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); + engine.onSettingsChanged(); + globalCheckpoint.set(localCheckpoint); + engine.syncTranslog(); + + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + assertThat(readAllOperationsInLucene(engine, mapperService), hasSize(liveDocs.size())); + } + } + + public void testForceMergeWithSoftDeletesRetentionAndRecoverySource() throws Exception { + final long retainedExtraOps = randomLongBetween(0, 10); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + final MapperService mapperService = createMapperService("test"); + final boolean omitSourceAllTheTime = randomBoolean(); + final Set liveDocs = new HashSet<>(); + final Set liveDocsWithSource = new HashSet<>(); + try (Store store = createStore(); + InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, + globalCheckpoint::get))) { + int numDocs = scaledRandomIntBetween(10, 100); + for (int i = 0; i < numDocs; i++) { + boolean useRecoverySource = randomBoolean() || omitSourceAllTheTime; + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null, useRecoverySource); + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + if (useRecoverySource == false) { + liveDocsWithSource.add(Integer.toString(i)); + } + } + for (int i = 0; i < numDocs; i++) { + boolean useRecoverySource = randomBoolean() || omitSourceAllTheTime; + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null, useRecoverySource); + if (randomBoolean()) { + engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); + liveDocs.remove(doc.id()); + liveDocsWithSource.remove(doc.id()); + } + if (randomBoolean()) { + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + if (useRecoverySource == false) { + liveDocsWithSource.add(doc.id()); + } else { + liveDocsWithSource.remove(doc.id()); + } + } + if (randomBoolean()) { + engine.flush(randomBoolean(), true); + } + } + engine.flush(); + globalCheckpoint.set(randomLongBetween(0, engine.getLocalCheckpoint())); + engine.syncTranslog(); + final long minSeqNoToRetain; + try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { + long safeCommitLocalCheckpoint = Long.parseLong( + safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + minSeqNoToRetain = Math.min(globalCheckpoint.get() + 1 - retainedExtraOps, safeCommitLocalCheckpoint + 1); + } + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + Map ops = readAllOperationsInLucene(engine, mapperService) + .stream().collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); + for (long seqno = 0; seqno <= engine.getLocalCheckpoint(); seqno++) { + String msg = "seq# [" + seqno + "], global checkpoint [" + globalCheckpoint + "], retained-ops [" + retainedExtraOps + "]"; + if (seqno < minSeqNoToRetain) { + Translog.Operation op = ops.get(seqno); + if (op != null) { + assertThat(op, instanceOf(Translog.Index.class)); + assertThat(msg, ((Translog.Index) op).id(), isIn(liveDocs)); + } + } else { + Translog.Operation op = ops.get(seqno); + assertThat(msg, op, notNullValue()); + if (op instanceof Translog.Index) { + assertEquals(msg, ((Translog.Index) op).source(), B_1); + } + } + } + settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); + indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); + engine.onSettingsChanged(); + globalCheckpoint.set(engine.getLocalCheckpoint()); + engine.syncTranslog(); + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + assertThat(readAllOperationsInLucene(engine, mapperService), hasSize(liveDocsWithSource.size())); + } + } + public void testForceMergeAndClose() throws IOException, InterruptedException { int numIters = randomIntBetween(2, 10); for (int j = 0; j < numIters; j++) { @@ -1422,126 +1597,10 @@ public void testVersioningCreateExistsException() throws IOException { assertThat(indexResult.getFailure(), instanceOf(VersionConflictEngineException.class)); } - protected List generateSingleDocHistory(boolean forReplica, VersionType versionType, - long primaryTerm, - int minOpCount, int maxOpCount, String docId) { - final int numOfOps = randomIntBetween(minOpCount, maxOpCount); - final List ops = new ArrayList<>(); - final Term id = newUid(docId); - final int startWithSeqNo = 0; - final String valuePrefix = (forReplica ? "r_" : "p_" ) + docId + "_"; - final boolean incrementTermWhenIntroducingSeqNo = randomBoolean(); - for (int i = 0; i < numOfOps; i++) { - final Engine.Operation op; - final long version; - switch (versionType) { - case INTERNAL: - version = forReplica ? i : Versions.MATCH_ANY; - break; - case EXTERNAL: - version = i; - break; - case EXTERNAL_GTE: - version = randomBoolean() ? Math.max(i - 1, 0) : i; - break; - case FORCE: - version = randomNonNegativeLong(); - break; - default: - throw new UnsupportedOperationException("unknown version type: " + versionType); - } - if (randomBoolean()) { - op = new Engine.Index(id, testParsedDocument(docId, null, testDocumentWithTextField(valuePrefix + i), B_1, null), - forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, - forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, - version, - forReplica ? null : versionType, - forReplica ? REPLICA : PRIMARY, - System.currentTimeMillis(), -1, false - ); - } else { - op = new Engine.Delete("test", docId, id, - forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, - forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, - version, - forReplica ? null : versionType, - forReplica ? REPLICA : PRIMARY, - System.currentTimeMillis()); - } - ops.add(op); - } - return ops; - } - public void testOutOfOrderDocsOnReplica() throws IOException { final List ops = generateSingleDocHistory(true, randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL, VersionType.EXTERNAL_GTE, VersionType.FORCE), 2, 2, 20, "1"); - assertOpsOnReplica(ops, replicaEngine, true); - } - - private void assertOpsOnReplica(List ops, InternalEngine replicaEngine, boolean shuffleOps) throws IOException { - final Engine.Operation lastOp = ops.get(ops.size() - 1); - final String lastFieldValue; - if (lastOp instanceof Engine.Index) { - Engine.Index index = (Engine.Index) lastOp; - lastFieldValue = index.docs().get(0).get("value"); - } else { - // delete - lastFieldValue = null; - } - if (shuffleOps) { - int firstOpWithSeqNo = 0; - while (firstOpWithSeqNo < ops.size() && ops.get(firstOpWithSeqNo).seqNo() < 0) { - firstOpWithSeqNo++; - } - // shuffle ops but make sure legacy ops are first - shuffle(ops.subList(0, firstOpWithSeqNo), random()); - shuffle(ops.subList(firstOpWithSeqNo, ops.size()), random()); - } - boolean firstOp = true; - for (Engine.Operation op : ops) { - logger.info("performing [{}], v [{}], seq# [{}], term [{}]", - op.operationType().name().charAt(0), op.version(), op.seqNo(), op.primaryTerm()); - if (op instanceof Engine.Index) { - Engine.IndexResult result = replicaEngine.index((Engine.Index) op); - // replicas don't really care to about creation status of documents - // this allows to ignore the case where a document was found in the live version maps in - // a delete state and return false for the created flag in favor of code simplicity - // as deleted or not. This check is just signal regression so a decision can be made if it's - // intentional - assertThat(result.isCreated(), equalTo(firstOp)); - assertThat(result.getVersion(), equalTo(op.version())); - assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); - - } else { - Engine.DeleteResult result = replicaEngine.delete((Engine.Delete) op); - // Replicas don't really care to about found status of documents - // this allows to ignore the case where a document was found in the live version maps in - // a delete state and return true for the found flag in favor of code simplicity - // his check is just signal regression so a decision can be made if it's - // intentional - assertThat(result.isFound(), equalTo(firstOp == false)); - assertThat(result.getVersion(), equalTo(op.version())); - assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); - } - if (randomBoolean()) { - engine.refresh("test"); - } - if (randomBoolean()) { - engine.flush(); - engine.refresh("test"); - } - firstOp = false; - } - - assertVisibleCount(replicaEngine, lastFieldValue == null ? 0 : 1); - if (lastFieldValue != null) { - try (Searcher searcher = replicaEngine.acquireSearcher("test")) { - final TotalHitCountCollector collector = new TotalHitCountCollector(); - searcher.searcher().search(new TermQuery(new Term("value", lastFieldValue)), collector); - assertThat(collector.getTotalHits(), equalTo(1)); - } - } + assertOpsOnReplica(ops, replicaEngine, true, logger); } public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, InterruptedException { @@ -1569,11 +1628,12 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup } // randomly interleave final AtomicLong seqNoGenerator = new AtomicLong(); - Function seqNoUpdater = operation -> { - final long newSeqNo = seqNoGenerator.getAndIncrement(); + BiFunction seqNoUpdater = (operation, newSeqNo) -> { if (operation instanceof Engine.Index) { Engine.Index index = (Engine.Index) operation; - return new Engine.Index(index.uid(), index.parsedDoc(), newSeqNo, index.primaryTerm(), index.version(), + Document doc = testDocumentWithTextField(index.docs().get(0).get("value")); + ParsedDocument parsedDocument = testParsedDocument(index.id(), index.routing(), doc, index.source(), null); + return new Engine.Index(index.uid(), parsedDocument, newSeqNo, index.primaryTerm(), index.version(), index.versionType(), index.origin(), index.startTime(), index.getAutoGeneratedIdTimestamp(), index.isRetry()); } else { Engine.Delete delete = (Engine.Delete) operation; @@ -1586,12 +1646,12 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup Iterator iter2 = opsDoc2.iterator(); while (iter1.hasNext() && iter2.hasNext()) { final Engine.Operation next = randomBoolean() ? iter1.next() : iter2.next(); - allOps.add(seqNoUpdater.apply(next)); + allOps.add(seqNoUpdater.apply(next, seqNoGenerator.getAndIncrement())); } - iter1.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o))); - iter2.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o))); + iter1.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o, seqNoGenerator.getAndIncrement()))); + iter2.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o, seqNoGenerator.getAndIncrement()))); // insert some duplicates - allOps.addAll(randomSubsetOf(allOps)); + randomSubsetOf(allOps).forEach(op -> allOps.add(seqNoUpdater.apply(op, op.seqNo()))); shuffle(allOps, random()); concurrentlyApplyOps(allOps, engine); @@ -1623,42 +1683,6 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup assertVisibleCount(engine, totalExpectedOps); } - private void concurrentlyApplyOps(List ops, InternalEngine engine) throws InterruptedException { - Thread[] thread = new Thread[randomIntBetween(3, 5)]; - CountDownLatch startGun = new CountDownLatch(thread.length); - AtomicInteger offset = new AtomicInteger(-1); - for (int i = 0; i < thread.length; i++) { - thread[i] = new Thread(() -> { - startGun.countDown(); - try { - startGun.await(); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - int docOffset; - while ((docOffset = offset.incrementAndGet()) < ops.size()) { - try { - final Engine.Operation op = ops.get(docOffset); - if (op instanceof Engine.Index) { - engine.index((Engine.Index) op); - } else { - engine.delete((Engine.Delete) op); - } - if ((docOffset + 1) % 4 == 0) { - engine.refresh("test"); - } - } catch (IOException e) { - throw new AssertionError(e); - } - } - }); - thread[i].start(); - } - for (int i = 0; i < thread.length; i++) { - thread[i].join(); - } - } - public void testInternalVersioningOnPrimary() throws IOException { final List ops = generateSingleDocHistory(false, VersionType.INTERNAL, 2, 2, 20, "1"); assertOpsOnPrimary(ops, Versions.NOT_FOUND, true, engine); @@ -1869,7 +1893,7 @@ public void testVersioningPromotedReplica() throws IOException { final boolean deletedOnReplica = lastReplicaOp instanceof Engine.Delete; final long finalReplicaVersion = lastReplicaOp.version(); final long finalReplicaSeqNo = lastReplicaOp.seqNo(); - assertOpsOnReplica(replicaOps, replicaEngine, true); + assertOpsOnReplica(replicaOps, replicaEngine, true, logger); final int opsOnPrimary = assertOpsOnPrimary(primaryOps, finalReplicaVersion, deletedOnReplica, replicaEngine); final long currentSeqNo = getSequenceID(replicaEngine, new Engine.Get(false, false, "type", lastReplicaOp.uid().text(), lastReplicaOp.uid())).v1(); @@ -2674,14 +2698,16 @@ public void testSkipTranslogReplay() throws IOException { Engine.IndexResult indexResult = engine.index(firstIndexRequest); assertThat(indexResult.getVersion(), equalTo(1L)); } + EngineConfig config = engine.config(); assertVisibleCount(engine, numDocs); engine.close(); - trimUnsafeCommits(engine.config()); - engine = new InternalEngine(engine.config()); - engine.skipTranslogRecovery(); - try (Engine.Searcher searcher = engine.acquireSearcher("test")) { - TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), randomIntBetween(numDocs, numDocs + 10)); - assertThat(topDocs.totalHits, equalTo(0L)); + trimUnsafeCommits(config); + try (InternalEngine engine = new InternalEngine(config)) { + engine.skipTranslogRecovery(); + try (Engine.Searcher searcher = engine.acquireSearcher("test")) { + TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), randomIntBetween(numDocs, numDocs + 10)); + assertThat(topDocs.totalHits, equalTo(0L)); + } } } @@ -2811,7 +2837,7 @@ public void testRecoverFromForeignTranslog() throws IOException { new CodecService(null, logger), config.getEventListener(), IndexSearcher.getDefaultQueryCache(), IndexSearcher.getDefaultQueryCachingPolicy(), translogConfig, TimeValue.timeValueMinutes(5), config.getExternalRefreshListener(), config.getInternalRefreshListener(), null, config.getTranslogRecoveryRunner(), - new NoneCircuitBreakerService(), () -> SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm::get); + new NoneCircuitBreakerService(), () -> SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm::get, tombstoneDocSupplier()); try { InternalEngine internalEngine = new InternalEngine(brokenConfig); fail("translog belongs to a different engine"); @@ -2940,6 +2966,12 @@ private void maybeThrowFailure() throws IOException { } } + @Override + public long softUpdateDocument(Term term, Iterable doc, Field... softDeletes) throws IOException { + maybeThrowFailure(); + return super.softUpdateDocument(term, doc, softDeletes); + } + @Override public long deleteDocuments(Term... terms) throws IOException { maybeThrowFailure(); @@ -3140,10 +3172,10 @@ public void testDoubleDeliveryReplicaAppendingAndDeleteOnly() throws IOException } public void testDoubleDeliveryReplicaAppendingOnly() throws IOException { - final ParsedDocument doc = testParsedDocument("1", null, testDocumentWithTextField(), + final Supplier doc = () -> testParsedDocument("1", null, testDocumentWithTextField(), new BytesArray("{}".getBytes(Charset.defaultCharset())), null); - Engine.Index operation = appendOnlyReplica(doc, false, 1, randomIntBetween(0, 5)); - Engine.Index retry = appendOnlyReplica(doc, true, 1, randomIntBetween(0, 5)); + Engine.Index operation = appendOnlyReplica(doc.get(), false, 1, randomIntBetween(0, 5)); + Engine.Index retry = appendOnlyReplica(doc.get(), true, 1, randomIntBetween(0, 5)); // operations with a seq# equal or lower to the local checkpoint are not indexed to lucene // and the version lookup is skipped final boolean belowLckp = operation.seqNo() == 0 && retry.seqNo() == 0; @@ -3182,8 +3214,8 @@ public void testDoubleDeliveryReplicaAppendingOnly() throws IOException { TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), 10); assertEquals(1, topDocs.totalHits); } - operation = randomAppendOnly(doc, false, 1); - retry = randomAppendOnly(doc, true, 1); + operation = randomAppendOnly(doc.get(), false, 1); + retry = randomAppendOnly(doc.get(), true, 1); if (randomBoolean()) { Engine.IndexResult indexResult = engine.index(operation); assertNotNull(indexResult.getTranslogLocation()); @@ -3248,6 +3280,8 @@ public void testDoubleDeliveryReplica() throws IOException { TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), 10); assertEquals(1, topDocs.totalHits); } + List ops = readAllOperationsInLucene(engine, createMapperService("test")); + assertThat(ops.stream().map(o -> o.seqNo()).collect(Collectors.toList()), hasItem(20L)); } public void testRetryWithAutogeneratedIdWorksAndNoDuplicateDocs() throws IOException { @@ -3716,20 +3750,22 @@ public void testOutOfOrderSequenceNumbersWithVersionConflict() throws IOExceptio final List operations = new ArrayList<>(); final int numberOfOperations = randomIntBetween(16, 32); - final Document document = testDocumentWithTextField(); final AtomicLong sequenceNumber = new AtomicLong(); final Engine.Operation.Origin origin = randomFrom(LOCAL_TRANSLOG_RECOVERY, PEER_RECOVERY, PRIMARY, REPLICA); final LongSupplier sequenceNumberSupplier = origin == PRIMARY ? () -> SequenceNumbers.UNASSIGNED_SEQ_NO : sequenceNumber::getAndIncrement; - document.add(new Field(SourceFieldMapper.NAME, BytesReference.toBytes(B_1), SourceFieldMapper.Defaults.FIELD_TYPE)); - final ParsedDocument doc = testParsedDocument("1", null, document, B_1, null); - final Term uid = newUid(doc); + final Supplier doc = () -> { + final Document document = testDocumentWithTextField(); + document.add(new Field(SourceFieldMapper.NAME, BytesReference.toBytes(B_1), SourceFieldMapper.Defaults.FIELD_TYPE)); + return testParsedDocument("1", null, document, B_1, null); + }; + final Term uid = newUid("1"); final BiFunction searcherFactory = engine::acquireSearcher; for (int i = 0; i < numberOfOperations; i++) { if (randomBoolean()) { final Engine.Index index = new Engine.Index( uid, - doc, + doc.get(), sequenceNumberSupplier.getAsLong(), 1, i, @@ -3805,7 +3841,9 @@ public void testNoOps() throws IOException { maxSeqNo, localCheckpoint); trimUnsafeCommits(engine.config()); - noOpEngine = new InternalEngine(engine.config(), supplier) { + EngineConfig noopEngineConfig = copy(engine.config(), new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, + () -> new MatchAllDocsQuery(), engine.config().getMergePolicy())); + noOpEngine = new InternalEngine(noopEngineConfig, supplier) { @Override protected long doGenerateSeqNoForOperation(Operation operation) { throw new UnsupportedOperationException(); @@ -3813,7 +3851,7 @@ protected long doGenerateSeqNoForOperation(Operation operation) { }; noOpEngine.recoverFromTranslog(Long.MAX_VALUE); final int gapsFilled = noOpEngine.fillSeqNoGaps(primaryTerm.get()); - final String reason = randomAlphaOfLength(16); + final String reason = "filling gaps"; noOpEngine.noOp(new Engine.NoOp(maxSeqNo + 1, primaryTerm.get(), LOCAL_TRANSLOG_RECOVERY, System.nanoTime(), reason)); assertThat(noOpEngine.getLocalCheckpoint(), equalTo((long) (maxSeqNo + 1))); assertThat(noOpEngine.getTranslog().stats().getUncommittedOperations(), equalTo(gapsFilled)); @@ -3835,11 +3873,77 @@ protected long doGenerateSeqNoForOperation(Operation operation) { assertThat(noOp.seqNo(), equalTo((long) (maxSeqNo + 2))); assertThat(noOp.primaryTerm(), equalTo(primaryTerm.get())); assertThat(noOp.reason(), equalTo(reason)); + if (engine.engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + MapperService mapperService = createMapperService("test"); + List operationsFromLucene = readAllOperationsInLucene(noOpEngine, mapperService); + assertThat(operationsFromLucene, hasSize(maxSeqNo + 2 - localCheckpoint)); // fills n gap and 2 manual noop. + for (int i = 0; i < operationsFromLucene.size(); i++) { + assertThat(operationsFromLucene.get(i), equalTo(new Translog.NoOp(localCheckpoint + 1 + i, primaryTerm.get(), "filling gaps"))); + } + assertConsistentHistoryBetweenTranslogAndLuceneIndex(noOpEngine, mapperService); + } } finally { IOUtils.close(noOpEngine); } } + /** + * Verifies that a segment containing only no-ops can be used to look up _version and _seqno. + */ + public void testSegmentContainsOnlyNoOps() throws Exception { + Engine.NoOpResult noOpResult = engine.noOp(new Engine.NoOp(1, primaryTerm.get(), + randomFrom(Engine.Operation.Origin.values()), randomNonNegativeLong(), "test")); + assertThat(noOpResult.getFailure(), nullValue()); + engine.refresh("test"); + Engine.DeleteResult deleteResult = engine.delete(replicaDeleteForDoc("id", 1, 2, randomNonNegativeLong())); + assertThat(deleteResult.getFailure(), nullValue()); + engine.refresh("test"); + } + + /** + * A simple test to check that random combination of operations can coexist in segments and be lookup. + * This is needed as some fields in Lucene may not exist if a segment misses operation types and this code is to check for that. + * For example, a segment containing only no-ops does not have neither _uid or _version. + */ + public void testRandomOperations() throws Exception { + int numOps = between(10, 100); + for (int i = 0; i < numOps; i++) { + String id = Integer.toString(randomIntBetween(1, 10)); + ParsedDocument doc = createParsedDoc(id, null); + Engine.Operation.TYPE type = randomFrom(Engine.Operation.TYPE.values()); + switch (type) { + case INDEX: + Engine.IndexResult index = engine.index(replicaIndexForDoc(doc, between(1, 100), i, randomBoolean())); + assertThat(index.getFailure(), nullValue()); + break; + case DELETE: + Engine.DeleteResult delete = engine.delete(replicaDeleteForDoc(doc.id(), between(1, 100), i, randomNonNegativeLong())); + assertThat(delete.getFailure(), nullValue()); + break; + case NO_OP: + Engine.NoOpResult noOp = engine.noOp(new Engine.NoOp(i, primaryTerm.get(), + randomFrom(Engine.Operation.Origin.values()), randomNonNegativeLong(), "")); + assertThat(noOp.getFailure(), nullValue()); + break; + default: + throw new IllegalStateException("Invalid op [" + type + "]"); + } + if (randomBoolean()) { + engine.refresh("test"); + } + if (randomBoolean()) { + engine.flush(); + } + if (randomBoolean()) { + engine.forceMerge(randomBoolean(), between(1, 10), randomBoolean(), false, false); + } + } + if (engine.engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + List operations = readAllOperationsInLucene(engine, createMapperService("test")); + assertThat(operations, hasSize(numOps)); + } + } + public void testMinGenerationForSeqNo() throws IOException, BrokenBarrierException, InterruptedException { engine.close(); final int numberOfTriplets = randomIntBetween(1, 32); @@ -4405,7 +4509,7 @@ public void testCleanUpCommitsWhenGlobalCheckpointAdvanced() throws Exception { globalCheckpoint.set(randomLongBetween(engine.getLocalCheckpoint(), Long.MAX_VALUE)); engine.syncTranslog(); assertThat(DirectoryReader.listCommits(store.directory()), contains(commits.get(commits.size() - 1))); - assertThat(engine.estimateTranslogOperationsFromMinSeq(0L), equalTo(0)); + assertThat(engine.getTranslog().totalOperations(), equalTo(0)); } } @@ -4768,6 +4872,154 @@ public void testTrimUnsafeCommits() throws Exception { } } + public void testLuceneHistoryOnPrimary() throws Exception { + final List operations = generateSingleDocHistory(false, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "1"); + assertOperationHistoryInLucene(operations); + } + + public void testLuceneHistoryOnReplica() throws Exception { + final List operations = generateSingleDocHistory(true, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "2"); + Randomness.shuffle(operations); + assertOperationHistoryInLucene(operations); + } + + private void assertOperationHistoryInLucene(List operations) throws IOException { + final MergePolicy keepSoftDeleteDocsMP = new SoftDeletesRetentionMergePolicy( + Lucene.SOFT_DELETES_FIELD, () -> new MatchAllDocsQuery(), engine.config().getMergePolicy()); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + Set expectedSeqNos = new HashSet<>(); + try (Store store = createStore(); + Engine engine = createEngine(config(indexSettings, store, createTempDir(), keepSoftDeleteDocsMP, null))) { + for (Engine.Operation op : operations) { + if (op instanceof Engine.Index) { + Engine.IndexResult indexResult = engine.index((Engine.Index) op); + assertThat(indexResult.getFailure(), nullValue()); + expectedSeqNos.add(indexResult.getSeqNo()); + } else { + Engine.DeleteResult deleteResult = engine.delete((Engine.Delete) op); + assertThat(deleteResult.getFailure(), nullValue()); + expectedSeqNos.add(deleteResult.getSeqNo()); + } + if (rarely()) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(); + } + if (rarely()) { + engine.forceMerge(true); + } + } + MapperService mapperService = createMapperService("test"); + List actualOps = readAllOperationsInLucene(engine, mapperService); + assertThat(actualOps.stream().map(o -> o.seqNo()).collect(Collectors.toList()), containsInAnyOrder(expectedSeqNos.toArray())); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + } + } + + public void testKeepMinRetainedSeqNoByMergePolicy() throws IOException { + IOUtils.close(engine, store); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + final List operations = generateSingleDocHistory(true, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "2"); + Randomness.shuffle(operations); + Set existingSeqNos = new HashSet<>(); + store = createStore(); + engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get)); + assertThat(engine.getMinRetainedSeqNo(), equalTo(0L)); + long lastMinRetainedSeqNo = engine.getMinRetainedSeqNo(); + for (Engine.Operation op : operations) { + final Engine.Result result; + if (op instanceof Engine.Index) { + result = engine.index((Engine.Index) op); + } else { + result = engine.delete((Engine.Delete) op); + } + existingSeqNos.add(result.getSeqNo()); + if (randomBoolean()) { + globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getLocalCheckpointTracker().getCheckpoint())); + } + if (rarely()) { + settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); + indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); + engine.onSettingsChanged(); + } + if (rarely()) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(true, true); + assertThat(Long.parseLong(engine.getLastCommittedSegmentInfos().userData.get(Engine.MIN_RETAINED_SEQNO)), + equalTo(engine.getMinRetainedSeqNo())); + } + if (rarely()) { + engine.forceMerge(randomBoolean()); + } + try (Closeable ignored = engine.acquireRetentionLockForPeerRecovery()) { + long minRetainSeqNos = engine.getMinRetainedSeqNo(); + assertThat(minRetainSeqNos, lessThanOrEqualTo(globalCheckpoint.get() + 1)); + Long[] expectedOps = existingSeqNos.stream().filter(seqno -> seqno >= minRetainSeqNos).toArray(Long[]::new); + Set actualOps = readAllOperationsInLucene(engine, createMapperService("test")).stream() + .map(Translog.Operation::seqNo).collect(Collectors.toSet()); + assertThat(actualOps, containsInAnyOrder(expectedOps)); + } + try (Engine.IndexCommitRef commitRef = engine.acquireSafeIndexCommit()) { + IndexCommit safeCommit = commitRef.getIndexCommit(); + if (safeCommit.getUserData().containsKey(Engine.MIN_RETAINED_SEQNO)) { + lastMinRetainedSeqNo = Long.parseLong(safeCommit.getUserData().get(Engine.MIN_RETAINED_SEQNO)); + } + } + } + if (randomBoolean()) { + engine.close(); + } else { + engine.flushAndClose(); + } + trimUnsafeCommits(engine.config()); + try (InternalEngine recoveringEngine = new InternalEngine(engine.config())) { + assertThat(recoveringEngine.getMinRetainedSeqNo(), equalTo(lastMinRetainedSeqNo)); + } + } + + public void testLastRefreshCheckpoint() throws Exception { + AtomicBoolean done = new AtomicBoolean(); + Thread[] refreshThreads = new Thread[between(1, 8)]; + CountDownLatch latch = new CountDownLatch(refreshThreads.length); + for (int i = 0; i < refreshThreads.length; i++) { + latch.countDown(); + refreshThreads[i] = new Thread(() -> { + while (done.get() == false) { + long checkPointBeforeRefresh = engine.getLocalCheckpoint(); + engine.refresh("test", randomFrom(Engine.SearcherScope.values())); + assertThat(engine.lastRefreshedCheckpoint(), greaterThanOrEqualTo(checkPointBeforeRefresh)); + } + }); + refreshThreads[i].start(); + } + latch.await(); + List ops = generateSingleDocHistory(true, VersionType.EXTERNAL, 1, 10, 1000, "1"); + concurrentlyApplyOps(ops, engine); + done.set(true); + for (Thread thread : refreshThreads) { + thread.join(); + } + engine.refresh("test"); + assertThat(engine.lastRefreshedCheckpoint(), equalTo(engine.getLocalCheckpoint())); + } + private static void trimUnsafeCommits(EngineConfig config) throws IOException { final Store store = config.getStore(); final TranslogConfig translogConfig = config.getTranslogConfig(); diff --git a/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java b/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java new file mode 100644 index 000000000000..2d097366a272 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java @@ -0,0 +1,289 @@ +/* + * 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.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.store.Store; +import org.elasticsearch.index.translog.SnapshotMatchers; +import org.elasticsearch.index.translog.Translog; +import org.elasticsearch.test.IndexSettingsModule; +import org.junit.Before; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class LuceneChangesSnapshotTests extends EngineTestCase { + private MapperService mapperService; + + @Before + public void createMapper() throws Exception { + mapperService = createMapperService("test"); + } + + @Override + protected Settings indexSettings() { + return Settings.builder().put(super.indexSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) // always enable soft-deletes + .build(); + } + + public void testBasics() throws Exception { + long fromSeqNo = randomNonNegativeLong(); + long toSeqNo = randomLongBetween(fromSeqNo, Long.MAX_VALUE); + // Empty engine + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, true)) { + IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); + assertThat(error.getMessage(), + containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); + } + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, false)) { + assertThat(snapshot, SnapshotMatchers.size(0)); + } + int numOps = between(1, 100); + int refreshedSeqNo = -1; + for (int i = 0; i < numOps; i++) { + String id = Integer.toString(randomIntBetween(i, i + 5)); + ParsedDocument doc = createParsedDoc(id, null, randomBoolean()); + if (randomBoolean()) { + engine.index(indexForDoc(doc)); + } else { + engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); + } + if (rarely()) { + if (randomBoolean()) { + engine.flush(); + } else { + engine.refresh("test"); + } + refreshedSeqNo = i; + } + } + if (refreshedSeqNo == -1) { + fromSeqNo = between(0, numOps); + toSeqNo = randomLongBetween(fromSeqNo, numOps * 2); + + Engine.Searcher searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, false)) { + searcher = null; + assertThat(snapshot, SnapshotMatchers.size(0)); + } finally { + IOUtils.close(searcher); + } + + searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { + searcher = null; + IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); + assertThat(error.getMessage(), + containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); + }finally { + IOUtils.close(searcher); + } + } else { + fromSeqNo = randomLongBetween(0, refreshedSeqNo); + toSeqNo = randomLongBetween(refreshedSeqNo + 1, numOps * 2); + Engine.Searcher searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, false)) { + searcher = null; + assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, refreshedSeqNo)); + } finally { + IOUtils.close(searcher); + } + searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { + searcher = null; + IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); + assertThat(error.getMessage(), + containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); + }finally { + IOUtils.close(searcher); + } + toSeqNo = randomLongBetween(fromSeqNo, refreshedSeqNo); + searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { + searcher = null; + assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, toSeqNo)); + } finally { + IOUtils.close(searcher); + } + } + // Get snapshot via engine will auto refresh + fromSeqNo = randomLongBetween(0, numOps - 1); + toSeqNo = randomLongBetween(fromSeqNo, numOps - 1); + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, randomBoolean())) { + assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, toSeqNo)); + } + } + + public void testDedupByPrimaryTerm() throws Exception { + Map latestOperations = new HashMap<>(); + List terms = Arrays.asList(between(1, 1000), between(1000, 2000)); + int totalOps = 0; + for (long term : terms) { + final List ops = generateSingleDocHistory(true, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL, VersionType.EXTERNAL_GTE), term, 2, 20, "1"); + primaryTerm.set(Math.max(primaryTerm.get(), term)); + engine.rollTranslogGeneration(); + for (Engine.Operation op : ops) { + // We need to simulate a rollback here as only ops after local checkpoint get into the engine + if (op.seqNo() <= engine.getLocalCheckpointTracker().getCheckpoint()) { + engine.getLocalCheckpointTracker().resetCheckpoint(randomLongBetween(-1, op.seqNo() - 1)); + engine.rollTranslogGeneration(); + } + if (op instanceof Engine.Index) { + engine.index((Engine.Index) op); + } else if (op instanceof Engine.Delete) { + engine.delete((Engine.Delete) op); + } + latestOperations.put(op.seqNo(), op.primaryTerm()); + if (rarely()) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(); + } + totalOps++; + } + } + long maxSeqNo = engine.getLocalCheckpointTracker().getMaxSeqNo(); + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, 0, maxSeqNo, false)) { + Translog.Operation op; + while ((op = snapshot.next()) != null) { + assertThat(op.toString(), op.primaryTerm(), equalTo(latestOperations.get(op.seqNo()))); + } + assertThat(snapshot.skippedOperations(), equalTo(totalOps - latestOperations.size())); + } + } + + public void testUpdateAndReadChangesConcurrently() throws Exception { + Follower[] followers = new Follower[between(1, 3)]; + CountDownLatch readyLatch = new CountDownLatch(followers.length + 1); + AtomicBoolean isDone = new AtomicBoolean(); + for (int i = 0; i < followers.length; i++) { + followers[i] = new Follower(engine, isDone, readyLatch); + followers[i].start(); + } + boolean onPrimary = randomBoolean(); + List operations = new ArrayList<>(); + int numOps = scaledRandomIntBetween(1, 1000); + for (int i = 0; i < numOps; i++) { + String id = Integer.toString(randomIntBetween(1, 10)); + ParsedDocument doc = createParsedDoc(id, randomAlphaOfLengthBetween(1, 5), randomBoolean()); + final Engine.Operation op; + if (onPrimary) { + if (randomBoolean()) { + op = new Engine.Index(newUid(doc), primaryTerm.get(), doc); + } else { + op = new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get()); + } + } else { + if (randomBoolean()) { + op = replicaIndexForDoc(doc, randomNonNegativeLong(), i, randomBoolean()); + } else { + op = replicaDeleteForDoc(doc.id(), randomNonNegativeLong(), i, randomNonNegativeLong()); + } + } + operations.add(op); + } + readyLatch.countDown(); + concurrentlyApplyOps(operations, engine); + assertThat(engine.getLocalCheckpointTracker().getCheckpoint(), equalTo(operations.size() - 1L)); + isDone.set(true); + for (Follower follower : followers) { + follower.join(); + } + } + + class Follower extends Thread { + private final Engine leader; + private final TranslogHandler translogHandler; + private final AtomicBoolean isDone; + private final CountDownLatch readLatch; + + Follower(Engine leader, AtomicBoolean isDone, CountDownLatch readLatch) { + this.leader = leader; + this.isDone = isDone; + this.readLatch = readLatch; + this.translogHandler = new TranslogHandler(xContentRegistry(), IndexSettingsModule.newIndexSettings(shardId.getIndexName(), + engine.engineConfig.getIndexSettings().getSettings())); + } + + void pullOperations(Engine follower) throws IOException { + long leaderCheckpoint = leader.getLocalCheckpoint(); + long followerCheckpoint = follower.getLocalCheckpoint(); + if (followerCheckpoint < leaderCheckpoint) { + long fromSeqNo = followerCheckpoint + 1; + long batchSize = randomLongBetween(0, 100); + long toSeqNo = Math.min(fromSeqNo + batchSize, leaderCheckpoint); + try (Translog.Snapshot snapshot = leader.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, true)) { + translogHandler.run(follower, snapshot); + } + } + } + + @Override + public void run() { + try (Store store = createStore(); + InternalEngine follower = createEngine(store, createTempDir())) { + readLatch.countDown(); + readLatch.await(); + while (isDone.get() == false || + follower.getLocalCheckpointTracker().getCheckpoint() < leader.getLocalCheckpoint()) { + pullOperations(follower); + } + assertConsistentHistoryBetweenTranslogAndLuceneIndex(follower, mapperService); + assertThat(getDocIds(follower, true), equalTo(getDocIds(leader, true))); + } catch (Exception ex) { + throw new AssertionError(ex); + } + } + } + + private List drainAll(Translog.Snapshot snapshot) throws IOException { + List operations = new ArrayList<>(); + Translog.Operation op; + while ((op = snapshot.next()) != null) { + final Translog.Operation newOp = op; + logger.error("Reading [{}]", op); + assert operations.stream().allMatch(o -> o.seqNo() < newOp.seqNo()) : "Operations [" + operations + "], op [" + op + "]"; + operations.add(newOp); + } + return operations; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java new file mode 100644 index 000000000000..c46b47b87d06 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java @@ -0,0 +1,161 @@ +/* + * 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.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.CodecReader; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.MergePolicy; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.SegmentCommitInfo; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.StandardDirectoryReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.InfoStream; +import org.apache.lucene.util.NullInfoStream; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +public class RecoverySourcePruneMergePolicyTests extends ESTestCase { + + public void testPruneAll() throws IOException { + try (Directory dir = newDirectory()) { + IndexWriterConfig iwc = newIndexWriterConfig(); + RecoverySourcePruneMergePolicy mp = new RecoverySourcePruneMergePolicy("extra_source", MatchNoDocsQuery::new, + newLogMergePolicy()); + iwc.setMergePolicy(mp); + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + for (int i = 0; i < 20; i++) { + if (i > 0 && randomBoolean()) { + writer.flush(); + } + Document doc = new Document(); + doc.add(new StoredField("source", "hello world")); + doc.add(new StoredField("extra_source", "hello world")); + doc.add(new NumericDocValuesField("extra_source", 1)); + writer.addDocument(doc); + } + writer.forceMerge(1); + writer.commit(); + try (DirectoryReader reader = DirectoryReader.open(writer)) { + for (int i = 0; i < reader.maxDoc(); i++) { + Document document = reader.document(i); + assertEquals(1, document.getFields().size()); + assertEquals("source", document.getFields().get(0).name()); + } + assertEquals(1, reader.leaves().size()); + LeafReader leafReader = reader.leaves().get(0).reader(); + NumericDocValues extra_source = leafReader.getNumericDocValues("extra_source"); + if (extra_source != null) { + assertEquals(DocIdSetIterator.NO_MORE_DOCS, extra_source.nextDoc()); + } + if (leafReader instanceof CodecReader && reader instanceof StandardDirectoryReader) { + CodecReader codecReader = (CodecReader) leafReader; + StandardDirectoryReader sdr = (StandardDirectoryReader) reader; + SegmentInfos segmentInfos = sdr.getSegmentInfos(); + MergePolicy.MergeSpecification forcedMerges = mp.findForcedDeletesMerges(segmentInfos, + new MergePolicy.MergeContext() { + @Override + public int numDeletesToMerge(SegmentCommitInfo info) { + return info.info.maxDoc() - 1; + } + + @Override + public int numDeletedDocs(SegmentCommitInfo info) { + return info.info.maxDoc() - 1; + } + + @Override + public InfoStream getInfoStream() { + return new NullInfoStream(); + } + + @Override + public Set getMergingSegments() { + return Collections.emptySet(); + } + }); + // don't wrap if there is nothing to do + assertSame(codecReader, forcedMerges.merges.get(0).wrapForMerge(codecReader)); + } + } + } + } + } + + + public void testPruneSome() throws IOException { + try (Directory dir = newDirectory()) { + IndexWriterConfig iwc = newIndexWriterConfig(); + iwc.setMergePolicy(new RecoverySourcePruneMergePolicy("extra_source", + () -> new TermQuery(new Term("even", "true")), iwc.getMergePolicy())); + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + for (int i = 0; i < 20; i++) { + if (i > 0 && randomBoolean()) { + writer.flush(); + } + Document doc = new Document(); + doc.add(new StringField("even", Boolean.toString(i % 2 == 0), Field.Store.YES)); + doc.add(new StoredField("source", "hello world")); + doc.add(new StoredField("extra_source", "hello world")); + doc.add(new NumericDocValuesField("extra_source", 1)); + writer.addDocument(doc); + } + writer.forceMerge(1); + writer.commit(); + try (DirectoryReader reader = DirectoryReader.open(writer)) { + assertEquals(1, reader.leaves().size()); + NumericDocValues extra_source = reader.leaves().get(0).reader().getNumericDocValues("extra_source"); + assertNotNull(extra_source); + for (int i = 0; i < reader.maxDoc(); i++) { + Document document = reader.document(i); + Set collect = document.getFields().stream().map(IndexableField::name).collect(Collectors.toSet()); + assertTrue(collect.contains("source")); + assertTrue(collect.contains("even")); + if (collect.size() == 3) { + assertTrue(collect.contains("extra_source")); + assertEquals("true", document.getField("even").stringValue()); + assertEquals(i, extra_source.nextDoc()); + } else { + assertEquals(2, document.getFields().size()); + } + } + assertEquals(DocIdSetIterator.NO_MORE_DOCS, extra_source.nextDoc()); + } + } + } + } +} diff --git a/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java new file mode 100644 index 000000000000..f35901003828 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.engine; + +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static org.hamcrest.Matchers.equalTo; + +public class SoftDeletesPolicyTests extends ESTestCase { + /** + * Makes sure we won't advance the retained seq# if the retention lock is held + */ + public void testSoftDeletesRetentionLock() { + long retainedOps = between(0, 10000); + AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + long safeCommitCheckpoint = globalCheckpoint.get(); + SoftDeletesPolicy policy = new SoftDeletesPolicy(globalCheckpoint::get, between(1, 10000), retainedOps); + long minRetainedSeqNo = policy.getMinRetainedSeqNo(); + List locks = new ArrayList<>(); + int iters = scaledRandomIntBetween(10, 1000); + for (int i = 0; i < iters; i++) { + if (randomBoolean()) { + locks.add(policy.acquireRetentionLock()); + } + // Advances the global checkpoint and the local checkpoint of a safe commit + globalCheckpoint.addAndGet(between(0, 1000)); + safeCommitCheckpoint = randomLongBetween(safeCommitCheckpoint, globalCheckpoint.get()); + policy.setLocalCheckpointOfSafeCommit(safeCommitCheckpoint); + if (rarely()) { + retainedOps = between(0, 10000); + policy.setRetentionOperations(retainedOps); + } + // Release some locks + List releasingLocks = randomSubsetOf(locks); + locks.removeAll(releasingLocks); + releasingLocks.forEach(Releasable::close); + + // We only expose the seqno to the merge policy if the retention lock is not held. + policy.getRetentionQuery(); + if (locks.isEmpty()) { + long retainedSeqNo = Math.min(safeCommitCheckpoint, globalCheckpoint.get() - retainedOps) + 1; + minRetainedSeqNo = Math.max(minRetainedSeqNo, retainedSeqNo); + } + assertThat(policy.getMinRetainedSeqNo(), equalTo(minRetainedSeqNo)); + } + + locks.forEach(Releasable::close); + long retainedSeqNo = Math.min(safeCommitCheckpoint, globalCheckpoint.get() - retainedOps) + 1; + minRetainedSeqNo = Math.max(minRetainedSeqNo, retainedSeqNo); + assertThat(policy.getMinRetainedSeqNo(), equalTo(minRetainedSeqNo)); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java index 76ca6aa7ea8d..5a46b9a889fd 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -311,15 +312,18 @@ DocumentMapper createDummyMapping(MapperService mapperService) throws Exception // creates an object mapper, which is about 100x harder than it should be.... ObjectMapper createObjectMapper(MapperService mapperService, String name) throws Exception { - ParseContext context = new ParseContext.InternalParseContext( - Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(), + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext context = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), mapperService.documentMapper("type"), null, null); String[] nameParts = name.split("\\."); for (int i = 0; i < nameParts.length - 1; ++i) { context.path().add(nameParts[i]); } Mapper.Builder builder = new ObjectMapper.Builder(nameParts[nameParts.length - 1]).enabled(true); - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); return (ObjectMapper)builder.build(builderContext); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java index cb2ed785699c..b11e4876f9ea 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.BooleanFieldMapper.BooleanFieldType; import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType; import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType; @@ -215,7 +216,10 @@ private String serialize(ToXContent mapper) throws Exception { } private Mapper parse(DocumentMapper mapper, DocumentMapperParser parser, XContentBuilder builder) throws Exception { - Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(); + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); SourceToParse source = SourceToParse.source("test", mapper.type(), "some_id", BytesReference.bytes(builder), builder.contentType()); try (XContentParser xContentParser = createParser(JsonXContent.jsonXContent, source.source())) { ParseContext.InternalParseContext ctx = new ParseContext.InternalParseContext(settings, parser, mapper, source, xContentParser); 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 1d1e423afc1b..fba71dd1e529 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.index.replication; +import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.Term; @@ -41,6 +42,7 @@ import org.elasticsearch.index.engine.InternalEngineTests; import org.elasticsearch.index.engine.SegmentsStats; import org.elasticsearch.index.engine.VersionConflictEngineException; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; @@ -140,7 +142,9 @@ public void cleanFiles(int totalTranslogOps, Store.MetadataSnapshot sourceMetaDa } public void testInheritMaxValidAutoIDTimestampOnRecovery() throws Exception { - try (ReplicationGroup shards = createGroup(0)) { + //TODO: Enables this test with soft-deletes once we have timestamp + Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + try (ReplicationGroup shards = createGroup(0, settings)) { shards.startAll(); final IndexRequest indexRequest = new IndexRequest(index.getName(), "type").source("{}", XContentType.JSON); indexRequest.onRetry(); // force an update of the timestamp @@ -346,7 +350,13 @@ public void testDocumentFailureReplication() throws Exception { final AtomicBoolean throwAfterIndexedOneDoc = new AtomicBoolean(); // need one document to trigger delete in IW. @Override public long addDocument(Iterable doc) throws IOException { - if (throwAfterIndexedOneDoc.getAndSet(true)) { + boolean isTombstone = false; + for (IndexableField field : doc) { + if (SeqNoFieldMapper.TOMBSTONE_NAME.equals(field.name())) { + isTombstone = true; + } + } + if (isTombstone == false && throwAfterIndexedOneDoc.getAndSet(true)) { throw indexException; } else { return super.addDocument(doc); @@ -356,6 +366,10 @@ public long addDocument(Iterable doc) throws IOExcepti public long deleteDocuments(Term... terms) throws IOException { throw deleteException; } + @Override + public long softUpdateDocument(Term term, Iterable doc, Field...fields) throws IOException { + throw deleteException; // a delete uses softUpdateDocument API if soft-deletes enabled + } }, null, null, config); try (ReplicationGroup shards = new ReplicationGroup(buildIndexMetaData(0)) { @Override @@ -390,6 +404,9 @@ public long deleteDocuments(Term... terms) throws IOException { try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } + try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } } // unlike previous failures, these two failures replicated directly from the replication channel. indexResp = shards.index(new IndexRequest(index.getName(), "type", "any").source("{}", XContentType.JSON)); @@ -404,6 +421,9 @@ public long deleteDocuments(Term... terms) throws IOException { try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } + try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } } shards.assertAllEqual(1); } @@ -501,8 +521,9 @@ public void testSeqNoCollision() throws Exception { recoverReplica(replica3, replica2, true); try (Translog.Snapshot snapshot = getTranslog(replica3).newSnapshot()) { assertThat(snapshot.totalOperations(), equalTo(initDocs + 1)); - assertThat(snapshot.next(), equalTo(op2)); - assertThat("Remaining of snapshot should contain init operations", snapshot, containsOperationsInAnyOrder(initOperations)); + 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. 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 2d198c32ba74..28122665e9bb 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java @@ -98,7 +98,8 @@ public void testIndexingDuringFileRecovery() throws Exception { } public void testRecoveryOfDisconnectedReplica() throws Exception { - try (ReplicationGroup shards = createGroup(1)) { + Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + try (ReplicationGroup shards = createGroup(1, settings)) { shards.startAll(); int docs = shards.indexDocs(randomInt(50)); shards.flush(); @@ -266,6 +267,7 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { builder.settings(Settings.builder().put(newPrimary.indexSettings().getSettings()) .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), "-1") .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1") + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) ); newPrimary.indexSettings().updateIndexMetaData(builder.build()); newPrimary.onSettingsChanged(); @@ -275,7 +277,12 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { shards.syncGlobalCheckpoint(); assertThat(newPrimary.getLastSyncedGlobalCheckpoint(), equalTo(newPrimary.seqNoStats().getMaxSeqNo())); }); - newPrimary.flush(new FlushRequest()); + newPrimary.flush(new FlushRequest().force(true)); + if (replica.indexSettings().isSoftDeleteEnabled()) { + // We need an extra flush to advance the min_retained_seqno on the new primary so ops-based won't happen. + // The min_retained_seqno only advances when a merge asks for the retention query. + newPrimary.flush(new FlushRequest().force(true)); + } uncommittedOpsOnPrimary = shards.indexDocs(randomIntBetween(0, 10)); totalDocs += uncommittedOpsOnPrimary; } 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 2228e1b017fd..50f95bf4d473 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -22,6 +22,7 @@ import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TermQuery; @@ -30,6 +31,7 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.FilterDirectory; import org.apache.lucene.store.IOContext; +import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.Constants; import org.elasticsearch.Assertions; import org.elasticsearch.Version; @@ -89,8 +91,13 @@ import org.elasticsearch.index.fielddata.IndexFieldDataService; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.ParseContext; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; @@ -160,6 +167,7 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThan; @@ -237,7 +245,8 @@ public void testFailShard() throws Exception { assertNotNull(shardPath); // fail shard shard.failShard("test shard fail", new CorruptIndexException("", "")); - closeShards(shard); + shard.close("do not assert history", false); + shard.store().close(); // check state file still exists ShardStateMetaData shardStateMetaData = load(logger, shardPath.getShardStatePath()); assertEquals(shardStateMetaData, getShardStateMetadata(shard)); @@ -2394,7 +2403,8 @@ public void testRecoverFromLocalShard() throws IOException { public void testDocStats() throws IOException, InterruptedException { IndexShard indexShard = null; try { - indexShard = newStartedShard(); + indexShard = newStartedShard( + Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0).build()); final long numDocs = randomIntBetween(2, 32); // at least two documents so we have docs to delete final long numDocsToDelete = randomLongBetween(1, numDocs); for (int i = 0; i < numDocs; i++) { @@ -2424,7 +2434,16 @@ public void testDocStats() throws IOException, InterruptedException { deleteDoc(indexShard, "_doc", id); indexDoc(indexShard, "_doc", id); } - + // Need to update and sync the global checkpoint as the soft-deletes retention MergePolicy depends on it. + if (indexShard.indexSettings.isSoftDeleteEnabled()) { + if (indexShard.routingEntry().primary()) { + indexShard.updateGlobalCheckpointForShard(indexShard.routingEntry().allocationId().getId(), + indexShard.getLocalCheckpoint()); + } else { + indexShard.updateGlobalCheckpointOnReplica(indexShard.getLocalCheckpoint(), "test"); + } + indexShard.sync(); + } // flush the buffered deletes final FlushRequest flushRequest = new FlushRequest(); flushRequest.force(false); @@ -2962,6 +2981,7 @@ public void testSegmentMemoryTrackedInBreaker() throws Exception { assertThat(breaker.getUsed(), greaterThan(preRefreshBytes)); indexDoc(primary, "_doc", "4", "{\"foo\": \"potato\"}"); + indexDoc(primary, "_doc", "5", "{\"foo\": \"potato\"}"); // Forces a refresh with the INTERNAL scope ((InternalEngine) primary.getEngine()).writeIndexingBuffer(); @@ -2973,6 +2993,13 @@ public void testSegmentMemoryTrackedInBreaker() throws Exception { // Deleting a doc causes its memory to be freed from the breaker deleteDoc(primary, "_doc", "0"); + // Here we are testing that a fully deleted segment should be dropped and its memory usage is freed. + // In order to instruct the merge policy not to keep a fully deleted segment, + // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. + if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { + primary.sync(); + flushShard(primary); + } primary.refresh("force refresh"); ss = primary.segmentStats(randomBoolean()); @@ -3064,6 +3091,7 @@ public void testSegmentMemoryTrackedWithRandomSearchers() throws Exception { // Close remaining searchers IOUtils.close(searchers); + primary.refresh("test"); SegmentsStats ss = primary.segmentStats(randomBoolean()); CircuitBreaker breaker = primary.circuitBreakerService.getBreaker(CircuitBreaker.ACCOUNTING); @@ -3181,4 +3209,28 @@ public void testOnCloseStats() throws IOException { } + public void testSupplyTombstoneDoc() throws Exception { + IndexShard shard = newStartedShard(); + String id = randomRealisticUnicodeOfLengthBetween(1, 10); + ParsedDocument deleteTombstone = shard.getEngine().config().getTombstoneDocSupplier().newDeleteTombstoneDoc("doc", id); + assertThat(deleteTombstone.docs(), hasSize(1)); + ParseContext.Document deleteDoc = deleteTombstone.docs().get(0); + assertThat(deleteDoc.getFields().stream().map(IndexableField::name).collect(Collectors.toList()), + containsInAnyOrder(IdFieldMapper.NAME, VersionFieldMapper.NAME, + SeqNoFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME)); + assertThat(deleteDoc.getField(IdFieldMapper.NAME).binaryValue(), equalTo(Uid.encodeId(id))); + assertThat(deleteDoc.getField(SeqNoFieldMapper.TOMBSTONE_NAME).numericValue().longValue(), equalTo(1L)); + + final String reason = randomUnicodeOfLength(200); + ParsedDocument noopTombstone = shard.getEngine().config().getTombstoneDocSupplier().newNoopTombstoneDoc(reason); + assertThat(noopTombstone.docs(), hasSize(1)); + ParseContext.Document noopDoc = noopTombstone.docs().get(0); + assertThat(noopDoc.getFields().stream().map(IndexableField::name).collect(Collectors.toList()), + containsInAnyOrder(VersionFieldMapper.NAME, SourceFieldMapper.NAME, SeqNoFieldMapper.TOMBSTONE_NAME, + SeqNoFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME)); + assertThat(noopDoc.getField(SeqNoFieldMapper.TOMBSTONE_NAME).numericValue().longValue(), equalTo(1L)); + assertThat(noopDoc.getField(SourceFieldMapper.NAME).binaryValue(), equalTo(new BytesRef(reason))); + + closeShards(shard); + } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java index ae2cc84e4870..29b16ca28f4d 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java @@ -106,17 +106,22 @@ public void testSyncerSendsOffCorrectDocuments() throws Exception { .isPresent(), is(false)); } - - assertEquals(globalCheckPoint == numDocs - 1 ? 0 : numDocs, resyncTask.getTotalOperations()); if (syncNeeded && globalCheckPoint < numDocs - 1) { - long skippedOps = globalCheckPoint + 1; // everything up to global checkpoint included - assertEquals(skippedOps, resyncTask.getSkippedOperations()); - assertEquals(numDocs - skippedOps, resyncTask.getResyncedOperations()); + if (shard.indexSettings.isSoftDeleteEnabled()) { + assertThat(resyncTask.getSkippedOperations(), equalTo(0)); + assertThat(resyncTask.getResyncedOperations(), equalTo(resyncTask.getTotalOperations())); + assertThat(resyncTask.getTotalOperations(), equalTo(Math.toIntExact(numDocs - 1 - globalCheckPoint))); + } else { + int skippedOps = Math.toIntExact(globalCheckPoint + 1); // everything up to global checkpoint included + assertThat(resyncTask.getSkippedOperations(), equalTo(skippedOps)); + assertThat(resyncTask.getResyncedOperations(), equalTo(numDocs - skippedOps)); + assertThat(resyncTask.getTotalOperations(), equalTo(globalCheckPoint == numDocs - 1 ? 0 : numDocs)); + } } else { - assertEquals(0, resyncTask.getSkippedOperations()); - assertEquals(0, resyncTask.getResyncedOperations()); + assertThat(resyncTask.getSkippedOperations(), equalTo(0)); + assertThat(resyncTask.getResyncedOperations(), equalTo(0)); + assertThat(resyncTask.getTotalOperations(), equalTo(0)); } - closeShards(shard); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java index 774b272121a5..b93f170174c3 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java @@ -42,6 +42,7 @@ import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.engine.EngineConfig; +import org.elasticsearch.index.engine.EngineTestCase; import org.elasticsearch.index.engine.InternalEngine; import org.elasticsearch.index.fieldvisitor.SingleFieldsVisitor; import org.elasticsearch.index.mapper.IdFieldMapper; @@ -130,7 +131,8 @@ public void onFailedEngine(String reason, @Nullable Exception e) { indexSettings, null, store, newMergePolicy(), iwc.getAnalyzer(), iwc.getSimilarity(), new CodecService(null, logger), eventListener, IndexSearcher.getDefaultQueryCache(), IndexSearcher.getDefaultQueryCachingPolicy(), translogConfig, TimeValue.timeValueMinutes(5), Collections.singletonList(listeners), Collections.emptyList(), null, - (e, s) -> 0, new NoneCircuitBreakerService(), () -> SequenceNumbers.NO_OPS_PERFORMED, () -> primaryTerm); + (e, s) -> 0, new NoneCircuitBreakerService(), () -> SequenceNumbers.NO_OPS_PERFORMED, () -> primaryTerm, + EngineTestCase.tombstoneDocSupplier()); engine = new InternalEngine(config); engine.recoverFromTranslog(Long.MAX_VALUE); listeners.setCurrentRefreshLocationSupplier(engine::getTranslogLastWriteLocation); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java index 89a8813e3e07..81afab4bb8f7 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java @@ -67,6 +67,7 @@ import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; +import org.junit.After; import java.io.IOException; import java.util.ArrayList; @@ -110,6 +111,11 @@ protected Collection> nodePlugins() { RecoverySettingsChunkSizePlugin.class); } + @After + public void assertConsistentHistoryInLuceneIndex() throws Exception { + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); + } + private void assertRecoveryStateWithoutStage(RecoveryState state, int shardId, RecoverySource recoverySource, boolean primary, String sourceNode, String targetNode) { assertThat(state.getShardId().getId(), equalTo(shardId)); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java index 4b1419375e6e..b6f5a7b64516 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java @@ -25,6 +25,7 @@ import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.NoMergePolicy; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTestCase; @@ -91,6 +92,7 @@ public void testGetStartingSeqNo() throws Exception { replica.close("test", false); final List commits = DirectoryReader.listCommits(replica.store().directory()); IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) .setMergePolicy(NoMergePolicy.INSTANCE) .setOpenMode(IndexWriterConfig.OpenMode.APPEND); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index f0644b029c3d..0351111c305c 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -411,12 +411,6 @@ public void testThrowExceptionOnPrimaryRelocatedBeforePhase1Started() throws IOE recoverySettings.getChunkSize().bytesAsInt(), Settings.EMPTY) { - - @Override - boolean isTranslogReadyForSequenceNumberBasedRecovery() throws IOException { - return randomBoolean(); - } - @Override public void phase1(final IndexCommit snapshot, final Supplier translogOps) { phase1Called.set(true); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java index 5547a629ab2a..45535e19672c 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.mapper.SourceToParse; @@ -63,13 +64,13 @@ public void testTranslogHistoryTransferred() throws Exception { int docs = shards.indexDocs(10); getTranslog(shards.getPrimary()).rollGeneration(); shards.flush(); - if (randomBoolean()) { - docs += shards.indexDocs(10); - } + int moreDocs = shards.indexDocs(randomInt(10)); shards.addReplica(); shards.startAll(); final IndexShard replica = shards.getReplicas().get(0); - assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(docs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? moreDocs : docs + moreDocs)); + shards.assertAllEqual(docs + moreDocs); } } @@ -101,12 +102,12 @@ public void testRetentionPolicyChangeDuringRecovery() throws Exception { // rolling/flushing is async assertBusy(() -> { assertThat(replica.getLastSyncedGlobalCheckpoint(), equalTo(19L)); - assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(0)); + assertThat(getTranslog(replica).totalOperations(), equalTo(0)); }); } } - public void testRecoveryWithOutOfOrderDelete() throws Exception { + public void testRecoveryWithOutOfOrderDeleteWithTranslog() throws Exception { /* * The flow of this test: * - delete #1 @@ -118,7 +119,8 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { * - index #5 * - If flush and the translog retention disabled, delete #1 will be removed while index #0 is still retained and replayed. */ - try (ReplicationGroup shards = createGroup(1)) { + Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + try (ReplicationGroup shards = createGroup(1, settings)) { shards.startAll(); // create out of order delete and index op on replica final IndexShard orgReplica = shards.getReplicas().get(0); @@ -170,7 +172,63 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { shards.recoverReplica(newReplica); shards.assertAllEqual(3); - assertThat(newReplica.estimateTranslogOperationsFromMinSeq(0), equalTo(translogOps)); + assertThat(getTranslog(newReplica).totalOperations(), equalTo(translogOps)); + } + } + + public void testRecoveryWithOutOfOrderDeleteWithSoftDeletes() throws Exception { + Settings settings = Settings.builder() + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 10) + // If soft-deletes is enabled, delete#1 will be reclaimed because its segment (segment_1) is fully deleted + // index#0 will be retained if merge is disabled; otherwise it will be reclaimed because gcp=3 and retained_ops=0 + .put(MergePolicyConfig.INDEX_MERGE_ENABLED, false).build(); + try (ReplicationGroup shards = createGroup(1, settings)) { + shards.startAll(); + // create out of order delete and index op on replica + final IndexShard orgReplica = shards.getReplicas().get(0); + final String indexName = orgReplica.shardId().getIndexName(); + + // delete #1 + orgReplica.applyDeleteOperationOnReplica(1, 2, "type", "id"); + orgReplica.flush(new FlushRequest().force(true)); // isolate delete#1 in its own translog generation and lucene segment + // index #0 + orgReplica.applyIndexOperationOnReplica(0, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id", new BytesArray("{}"), XContentType.JSON)); + // index #3 + orgReplica.applyIndexOperationOnReplica(3, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-3", new BytesArray("{}"), XContentType.JSON)); + // Flushing a new commit with local checkpoint=1 allows to delete the translog gen #1. + orgReplica.flush(new FlushRequest().force(true).waitIfOngoing(true)); + // index #2 + orgReplica.applyIndexOperationOnReplica(2, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-2", new BytesArray("{}"), XContentType.JSON)); + orgReplica.updateGlobalCheckpointOnReplica(3L, "test"); + // index #5 -> force NoOp #4. + orgReplica.applyIndexOperationOnReplica(5, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-5", new BytesArray("{}"), XContentType.JSON)); + + if (randomBoolean()) { + if (randomBoolean()) { + logger.info("--> flushing shard (translog/soft-deletes will be trimmed)"); + IndexMetaData.Builder builder = IndexMetaData.builder(orgReplica.indexSettings().getIndexMetaData()); + builder.settings(Settings.builder().put(orgReplica.indexSettings().getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0)); + orgReplica.indexSettings().updateIndexMetaData(builder.build()); + orgReplica.onSettingsChanged(); + } + flushShard(orgReplica); + } + + final IndexShard orgPrimary = shards.getPrimary(); + shards.promoteReplicaToPrimary(orgReplica).get(); // wait for primary/replica sync to make sure seq# gap is closed. + + IndexShard newReplica = shards.addReplicaWithExistingPath(orgPrimary.shardPath(), orgPrimary.routingEntry().currentNodeId()); + shards.recoverReplica(newReplica); + shards.assertAllEqual(3); + try (Translog.Snapshot snapshot = newReplica.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.size(6)); + } } } @@ -222,7 +280,8 @@ public void testDifferentHistoryUUIDDisablesOPsRecovery() throws Exception { shards.recoverReplica(newReplica); // file based recovery should be made assertThat(newReplica.recoveryState().getIndex().fileDetails(), not(empty())); - assertThat(newReplica.estimateTranslogOperationsFromMinSeq(0), equalTo(numDocs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(newReplica).totalOperations(), equalTo(softDeletesEnabled ? nonFlushedDocs : numDocs)); // history uuid was restored assertThat(newReplica.getHistoryUUID(), equalTo(historyUUID)); @@ -326,7 +385,8 @@ public void testShouldFlushAfterPeerRecovery() throws Exception { shards.recoverReplica(replica); // Make sure the flushing will eventually be completed (eg. `shouldPeriodicallyFlush` is false) assertBusy(() -> assertThat(getEngine(replica).shouldPeriodicallyFlush(), equalTo(false))); - assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(numDocs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? 0 : numDocs)); shards.assertAllEqual(numDocs); } } diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index fa591411bba1..ce162b9600cf 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -43,6 +43,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexModule; +import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.MergeSchedulerConfig; @@ -50,6 +51,7 @@ import org.elasticsearch.index.cache.query.QueryCacheStats; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.indices.IndicesQueryCache; import org.elasticsearch.indices.IndicesRequestCache; @@ -69,6 +71,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -115,6 +118,7 @@ public Settings indexSettings() { return Settings.builder().put(super.indexSettings()) .put(IndexModule.INDEX_QUERY_CACHE_EVERYTHING_SETTING.getKey(), true) .put(IndexModule.INDEX_QUERY_CACHE_ENABLED_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) .build(); } @@ -1006,10 +1010,15 @@ private void assertCumulativeQueryCacheStats(IndicesStatsResponse response) { @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32506") public void testFilterCacheStats() throws Exception { - assertAcked(prepareCreate("index").setSettings(Settings.builder().put(indexSettings()).put("number_of_replicas", 0).build()).get()); - indexRandom(true, + Settings settings = Settings.builder().put(indexSettings()).put("number_of_replicas", 0).build(); + assertAcked(prepareCreate("index").setSettings(settings).get()); + indexRandom(false, true, client().prepareIndex("index", "type", "1").setSource("foo", "bar"), client().prepareIndex("index", "type", "2").setSource("foo", "baz")); + if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { + persistGlobalCheckpoint("index"); // Need to persist the global checkpoint for the soft-deletes retention MP. + } + refresh(); ensureGreen(); IndicesStatsResponse response = client().admin().indices().prepareStats("index").setQueryCache(true).get(); @@ -1040,6 +1049,13 @@ public void testFilterCacheStats() throws Exception { assertEquals(DocWriteResponse.Result.DELETED, client().prepareDelete("index", "type", "1").get().getResult()); assertEquals(DocWriteResponse.Result.DELETED, client().prepareDelete("index", "type", "2").get().getResult()); + // Here we are testing that a fully deleted segment should be dropped and its cached is evicted. + // In order to instruct the merge policy not to keep a fully deleted segment, + // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. + if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { + persistGlobalCheckpoint("index"); + flush("index"); + } refresh(); response = client().admin().indices().prepareStats("index").setQueryCache(true).get(); assertCumulativeQueryCacheStats(response); @@ -1173,4 +1189,21 @@ public void testConcurrentIndexingAndStatsRequests() throws BrokenBarrierExcepti assertThat(executionFailures.get(), emptyCollectionOf(Exception.class)); } + + /** + * Persist the global checkpoint on all shards of the given index into disk. + * This makes sure that the persisted global checkpoint on those shards will equal to the in-memory value. + */ + private void persistGlobalCheckpoint(String index) throws Exception { + final Set nodes = internalCluster().nodesInclude(index); + for (String node : nodes) { + final IndicesService indexServices = internalCluster().getInstance(IndicesService.class, node); + for (IndexService indexService : indexServices) { + for (IndexShard indexShard : indexService) { + indexShard.sync(); + assertThat(indexShard.getLastSyncedGlobalCheckpoint(), equalTo(indexShard.getGlobalCheckpoint())); + } + } + } + } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java b/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java index 23c56688e00b..c25cad61e074 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java +++ b/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java @@ -27,6 +27,7 @@ import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.snapshots.mockstore.MockRepository; import org.elasticsearch.test.ESIntegTestCase; +import org.junit.After; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -58,6 +59,11 @@ protected Collection> nodePlugins() { return Arrays.asList(MockRepository.Plugin.class); } + @After + public void assertConsistentHistoryInLuceneIndex() throws Exception { + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); + } + public static long getFailureCount(String repository) { long failureCount = 0; for (RepositoriesService repositoriesService : diff --git a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java index 1230d594b98a..632a1ecbee1a 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java @@ -122,6 +122,7 @@ import static org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider.SETTING_ALLOCATION_MAX_RETRY; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.IndexSettings.INDEX_REFRESH_INTERVAL_SETTING; +import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.elasticsearch.index.shard.IndexShardTests.getEngineFromShard; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -2048,7 +2049,9 @@ public void testSnapshotMoreThanOnce() throws ExecutionException, InterruptedExc .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); // only one shard - assertAcked(prepareCreate("test").setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1))); + final Settings indexSettings = Settings.builder() + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1).build(); + assertAcked(prepareCreate("test").setSettings(indexSettings)); ensureGreen(); logger.info("--> indexing"); @@ -2094,7 +2097,13 @@ public void testSnapshotMoreThanOnce() throws ExecutionException, InterruptedExc SnapshotStatus snapshotStatus = client.admin().cluster().prepareSnapshotStatus("test-repo").setSnapshots("test-2").get().getSnapshots().get(0); List shards = snapshotStatus.getShards(); for (SnapshotIndexShardStatus status : shards) { - assertThat(status.getStats().getProcessedFileCount(), equalTo(2)); // we flush before the snapshot such that we have to process the segments_N files plus the .del file + // we flush before the snapshot such that we have to process the segments_N files plus the .del file + if (INDEX_SOFT_DELETES_SETTING.get(indexSettings)) { + // soft-delete generates DV files. + assertThat(status.getStats().getProcessedFileCount(), greaterThan(2)); + } else { + assertThat(status.getStats().getProcessedFileCount(), equalTo(2)); + } } } } diff --git a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java index caf4f725fa45..588118db4aef 100644 --- a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java +++ b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.VersionType; @@ -785,4 +786,26 @@ public void testGCDeletesZero() throws Exception { .getVersion(), equalTo(-1L)); } + + public void testSpecialVersioning() { + internalCluster().ensureAtLeastNumDataNodes(2); + createIndex("test", Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0).build()); + IndexResponse doc1 = client().prepareIndex("test", "type", "1").setSource("field", "value1") + .setVersion(0).setVersionType(VersionType.EXTERNAL).execute().actionGet(); + assertThat(doc1.getVersion(), equalTo(0L)); + IndexResponse doc2 = client().prepareIndex("test", "type", "1").setSource("field", "value2") + .setVersion(Versions.MATCH_ANY).setVersionType(VersionType.INTERNAL).execute().actionGet(); + assertThat(doc2.getVersion(), equalTo(1L)); + client().prepareDelete("test", "type", "1").get(); //v2 + IndexResponse doc3 = client().prepareIndex("test", "type", "1").setSource("field", "value3") + .setVersion(Versions.MATCH_DELETED).setVersionType(VersionType.INTERNAL).execute().actionGet(); + assertThat(doc3.getVersion(), equalTo(3L)); + IndexResponse doc4 = client().prepareIndex("test", "type", "1").setSource("field", "value4") + .setVersion(4L).setVersionType(VersionType.EXTERNAL_GTE).execute().actionGet(); + assertThat(doc4.getVersion(), equalTo(4L)); + // Make sure that these versions are replicated correctly + client().admin().indices().prepareUpdateSettings("test") + .setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)).get(); + ensureGreen("test"); + } } 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 b5ba5f18b395..b558cd1ba900 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 @@ -19,14 +19,18 @@ package org.elasticsearch.index.engine; +import org.apache.logging.log4j.Logger; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.codecs.Codec; +import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.StoredField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; +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.Term; @@ -34,32 +38,41 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.ReferenceManager; import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TotalHitCountCollector; import org.apache.lucene.store.Directory; +import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.cluster.ClusterModule; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.routing.AllocationId; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.MapperTestUtils; +import org.elasticsearch.index.VersionType; import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.index.mapper.IdFieldMapper; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.index.seqno.LocalCheckpointTracker; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.SequenceNumbers; @@ -80,17 +93,30 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.LongSupplier; import java.util.function.ToLongBiFunction; +import java.util.stream.Collectors; import static java.util.Collections.emptyList; +import static java.util.Collections.shuffle; +import static org.elasticsearch.index.engine.Engine.Operation.Origin.PRIMARY; +import static org.elasticsearch.index.engine.Engine.Operation.Origin.REPLICA; import static org.elasticsearch.index.translog.TranslogDeletionPolicies.createTranslogDeletionPolicy; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; public abstract class EngineTestCase extends ESTestCase { @@ -128,6 +154,20 @@ protected static void assertVisibleCount(Engine engine, int numDocs, boolean ref } } + protected Settings indexSettings() { + // TODO randomize more settings + return Settings.builder() + .put(IndexSettings.INDEX_GC_DELETES_SETTING.getKey(), "1h") // make sure this doesn't kick in on us + .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), codecName) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.getKey(), + between(10, 10 * IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.get(Settings.EMPTY))) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + .build(); + } + @Override @Before public void setUp() throws Exception { @@ -142,13 +182,7 @@ public void setUp() throws Exception { } else { codecName = "default"; } - defaultSettings = IndexSettingsModule.newIndexSettings("test", Settings.builder() - .put(IndexSettings.INDEX_GC_DELETES_SETTING.getKey(), "1h") // make sure this doesn't kick in on us - .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), codecName) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.getKey(), - between(10, 10 * IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.get(Settings.EMPTY))) - .build()); // TODO randomize more settings + defaultSettings = IndexSettingsModule.newIndexSettings("test", indexSettings()); threadPool = new TestThreadPool(getClass().getName()); store = createStore(); storeReplica = createStore(); @@ -180,7 +214,7 @@ public EngineConfig copy(EngineConfig config, LongSupplier globalCheckpointSuppl new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), config.getTranslogConfig(), config.getFlushMergesAfter(), config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), - config.getCircuitBreakerService(), globalCheckpointSupplier, config.getPrimaryTermSupplier()); + config.getCircuitBreakerService(), globalCheckpointSupplier, config.getPrimaryTermSupplier(), tombstoneDocSupplier()); } public EngineConfig copy(EngineConfig config, Analyzer analyzer) { @@ -189,7 +223,18 @@ public EngineConfig copy(EngineConfig config, Analyzer analyzer) { new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), config.getTranslogConfig(), config.getFlushMergesAfter(), config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), - config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier()); + config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier(), + config.getTombstoneDocSupplier()); + } + + public EngineConfig copy(EngineConfig config, MergePolicy mergePolicy) { + return new EngineConfig(config.getShardId(), config.getAllocationId(), config.getThreadPool(), config.getIndexSettings(), + config.getWarmer(), config.getStore(), mergePolicy, config.getAnalyzer(), config.getSimilarity(), + new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), + config.getTranslogConfig(), config.getFlushMergesAfter(), + config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), + config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier(), + config.getTombstoneDocSupplier()); } @Override @@ -198,9 +243,11 @@ public void tearDown() throws Exception { super.tearDown(); if (engine != null && engine.isClosed.get() == false) { engine.getTranslog().getDeletionPolicy().assertNoOpenTranslogRefs(); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, createMapperService("test")); } if (replicaEngine != null && replicaEngine.isClosed.get() == false) { replicaEngine.getTranslog().getDeletionPolicy().assertNoOpenTranslogRefs(); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(replicaEngine, createMapperService("test")); } IOUtils.close( replicaEngine, storeReplica, @@ -228,8 +275,18 @@ public static ParsedDocument createParsedDoc(String id, String routing) { return testParsedDocument(id, routing, testDocumentWithTextField(), new BytesArray("{ \"value\" : \"test\" }"), null); } + public static ParsedDocument createParsedDoc(String id, String routing, boolean recoverySource) { + return testParsedDocument(id, routing, testDocumentWithTextField(), new BytesArray("{ \"value\" : \"test\" }"), null, + recoverySource); + } + protected static ParsedDocument testParsedDocument( String id, String routing, ParseContext.Document document, BytesReference source, Mapping mappingUpdate) { + return testParsedDocument(id, routing, document, source, mappingUpdate, false); + } + protected static ParsedDocument testParsedDocument( + String id, String routing, ParseContext.Document document, BytesReference source, Mapping mappingUpdate, + boolean recoverySource) { Field uidField = new Field("_id", Uid.encodeId(id), IdFieldMapper.Defaults.FIELD_TYPE); Field versionField = new NumericDocValuesField("_version", 0); SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); @@ -239,11 +296,57 @@ protected static ParsedDocument testParsedDocument( document.add(seqID.seqNoDocValue); document.add(seqID.primaryTerm); BytesRef ref = source.toBytesRef(); - document.add(new StoredField(SourceFieldMapper.NAME, ref.bytes, ref.offset, ref.length)); + if (recoverySource) { + document.add(new StoredField(SourceFieldMapper.RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); + document.add(new NumericDocValuesField(SourceFieldMapper.RECOVERY_SOURCE_NAME, 1)); + } else { + document.add(new StoredField(SourceFieldMapper.NAME, ref.bytes, ref.offset, ref.length)); + } return new ParsedDocument(versionField, seqID, id, "test", routing, Arrays.asList(document), source, XContentType.JSON, mappingUpdate); } + /** + * Creates a tombstone document that only includes uid, seq#, term and version fields. + */ + public static EngineConfig.TombstoneDocSupplier tombstoneDocSupplier(){ + return new EngineConfig.TombstoneDocSupplier() { + @Override + public ParsedDocument newDeleteTombstoneDoc(String type, String id) { + final ParseContext.Document doc = new ParseContext.Document(); + Field uidField = new Field(IdFieldMapper.NAME, Uid.encodeId(id), IdFieldMapper.Defaults.FIELD_TYPE); + doc.add(uidField); + Field versionField = new NumericDocValuesField(VersionFieldMapper.NAME, 0); + doc.add(versionField); + SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); + doc.add(seqID.seqNo); + doc.add(seqID.seqNoDocValue); + doc.add(seqID.primaryTerm); + seqID.tombstoneField.setLongValue(1); + doc.add(seqID.tombstoneField); + return new ParsedDocument(versionField, seqID, id, type, null, + Collections.singletonList(doc), new BytesArray("{}"), XContentType.JSON, null); + } + + @Override + public ParsedDocument newNoopTombstoneDoc(String reason) { + final ParseContext.Document doc = new ParseContext.Document(); + SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); + doc.add(seqID.seqNo); + doc.add(seqID.seqNoDocValue); + doc.add(seqID.primaryTerm); + seqID.tombstoneField.setLongValue(1); + doc.add(seqID.tombstoneField); + Field versionField = new NumericDocValuesField(VersionFieldMapper.NAME, 0); + doc.add(versionField); + BytesRef byteRef = new BytesRef(reason); + doc.add(new StoredField(SourceFieldMapper.NAME, byteRef.bytes, byteRef.offset, byteRef.length)); + return new ParsedDocument(versionField, seqID, null, null, null, + Collections.singletonList(doc), null, XContentType.JSON, null); + } + }; + } + protected Store createStore() throws IOException { return createStore(newDirectory()); } @@ -461,7 +564,7 @@ public void onFailedEngine(String reason, @Nullable Exception e) { new NoneCircuitBreakerService(), globalCheckpointSupplier == null ? new ReplicationTracker(shardId, allocationId.getId(), indexSettings, SequenceNumbers.NO_OPS_PERFORMED, update -> {}) : - globalCheckpointSupplier, primaryTerm::get); + globalCheckpointSupplier, primaryTerm::get, tombstoneDocSupplier()); return config; } @@ -474,7 +577,7 @@ protected static BytesArray bytesArray(String string) { return new BytesArray(string.getBytes(Charset.defaultCharset())); } - protected Term newUid(String id) { + protected static Term newUid(String id) { return new Term("_id", Uid.encodeId(id)); } @@ -499,6 +602,279 @@ protected Engine.Index replicaIndexForDoc(ParsedDocument doc, long version, long protected Engine.Delete replicaDeleteForDoc(String id, long version, long seqNo, long startTime) { return new Engine.Delete("test", id, newUid(id), seqNo, 1, version, null, Engine.Operation.Origin.REPLICA, startTime); } + protected static void assertVisibleCount(InternalEngine engine, int numDocs) throws IOException { + assertVisibleCount(engine, numDocs, true); + } + + protected static void assertVisibleCount(InternalEngine engine, int numDocs, boolean refresh) throws IOException { + if (refresh) { + engine.refresh("test"); + } + try (Engine.Searcher searcher = engine.acquireSearcher("test")) { + final TotalHitCountCollector collector = new TotalHitCountCollector(); + searcher.searcher().search(new MatchAllDocsQuery(), collector); + assertThat(collector.getTotalHits(), equalTo(numDocs)); + } + } + + public static List generateSingleDocHistory(boolean forReplica, VersionType versionType, + long primaryTerm, int minOpCount, int maxOpCount, String docId) { + final int numOfOps = randomIntBetween(minOpCount, maxOpCount); + final List ops = new ArrayList<>(); + final Term id = newUid(docId); + final int startWithSeqNo = 0; + final String valuePrefix = (forReplica ? "r_" : "p_" ) + docId + "_"; + final boolean incrementTermWhenIntroducingSeqNo = randomBoolean(); + for (int i = 0; i < numOfOps; i++) { + final Engine.Operation op; + final long version; + switch (versionType) { + case INTERNAL: + version = forReplica ? i : Versions.MATCH_ANY; + break; + case EXTERNAL: + version = i; + break; + case EXTERNAL_GTE: + version = randomBoolean() ? Math.max(i - 1, 0) : i; + break; + case FORCE: + version = randomNonNegativeLong(); + break; + default: + throw new UnsupportedOperationException("unknown version type: " + versionType); + } + if (randomBoolean()) { + op = new Engine.Index(id, testParsedDocument(docId, null, testDocumentWithTextField(valuePrefix + i), B_1, null), + forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, + forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, + version, + forReplica ? null : versionType, + forReplica ? REPLICA : PRIMARY, + System.currentTimeMillis(), -1, false + ); + } else { + op = new Engine.Delete("test", docId, id, + forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, + forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, + version, + forReplica ? null : versionType, + forReplica ? REPLICA : PRIMARY, + System.currentTimeMillis()); + } + ops.add(op); + } + return ops; + } + + public static void assertOpsOnReplica( + final List ops, + final InternalEngine replicaEngine, + boolean shuffleOps, + final Logger logger) throws IOException { + final Engine.Operation lastOp = ops.get(ops.size() - 1); + final String lastFieldValue; + if (lastOp instanceof Engine.Index) { + Engine.Index index = (Engine.Index) lastOp; + lastFieldValue = index.docs().get(0).get("value"); + } else { + // delete + lastFieldValue = null; + } + if (shuffleOps) { + int firstOpWithSeqNo = 0; + while (firstOpWithSeqNo < ops.size() && ops.get(firstOpWithSeqNo).seqNo() < 0) { + firstOpWithSeqNo++; + } + // shuffle ops but make sure legacy ops are first + shuffle(ops.subList(0, firstOpWithSeqNo), random()); + shuffle(ops.subList(firstOpWithSeqNo, ops.size()), random()); + } + boolean firstOp = true; + for (Engine.Operation op : ops) { + logger.info("performing [{}], v [{}], seq# [{}], term [{}]", + op.operationType().name().charAt(0), op.version(), op.seqNo(), op.primaryTerm()); + if (op instanceof Engine.Index) { + Engine.IndexResult result = replicaEngine.index((Engine.Index) op); + // replicas don't really care to about creation status of documents + // this allows to ignore the case where a document was found in the live version maps in + // a delete state and return false for the created flag in favor of code simplicity + // as deleted or not. This check is just signal regression so a decision can be made if it's + // intentional + assertThat(result.isCreated(), equalTo(firstOp)); + assertThat(result.getVersion(), equalTo(op.version())); + assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); + + } else { + Engine.DeleteResult result = replicaEngine.delete((Engine.Delete) op); + // Replicas don't really care to about found status of documents + // this allows to ignore the case where a document was found in the live version maps in + // a delete state and return true for the found flag in favor of code simplicity + // his check is just signal regression so a decision can be made if it's + // intentional + assertThat(result.isFound(), equalTo(firstOp == false)); + assertThat(result.getVersion(), equalTo(op.version())); + assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); + } + if (randomBoolean()) { + replicaEngine.refresh("test"); + } + if (randomBoolean()) { + replicaEngine.flush(); + replicaEngine.refresh("test"); + } + firstOp = false; + } + + assertVisibleCount(replicaEngine, lastFieldValue == null ? 0 : 1); + if (lastFieldValue != null) { + try (Engine.Searcher searcher = replicaEngine.acquireSearcher("test")) { + final TotalHitCountCollector collector = new TotalHitCountCollector(); + searcher.searcher().search(new TermQuery(new Term("value", lastFieldValue)), collector); + assertThat(collector.getTotalHits(), equalTo(1)); + } + } + } + + protected void concurrentlyApplyOps(List ops, InternalEngine engine) throws InterruptedException { + Thread[] thread = new Thread[randomIntBetween(3, 5)]; + CountDownLatch startGun = new CountDownLatch(thread.length); + AtomicInteger offset = new AtomicInteger(-1); + for (int i = 0; i < thread.length; i++) { + thread[i] = new Thread(() -> { + startGun.countDown(); + try { + startGun.await(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + int docOffset; + while ((docOffset = offset.incrementAndGet()) < ops.size()) { + try { + final Engine.Operation op = ops.get(docOffset); + if (op instanceof Engine.Index) { + engine.index((Engine.Index) op); + } else if (op instanceof Engine.Delete){ + engine.delete((Engine.Delete) op); + } else { + engine.noOp((Engine.NoOp) op); + } + if ((docOffset + 1) % 4 == 0) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(); + } + } catch (IOException e) { + throw new AssertionError(e); + } + } + }); + thread[i].start(); + } + for (int i = 0; i < thread.length; i++) { + thread[i].join(); + } + } + + /** + * Gets all docId from the given engine. + */ + public static Set 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<>(); + for (LeafReaderContext leafContext : searcher.reader().leaves()) { + LeafReader reader = leafContext.reader(); + 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))); + } + } + } + return ids; + } + } + + /** + * Reads all engine operations that have been processed by the engine from Lucene index. + * The returned operations are sorted and de-duplicated, thus each sequence number will be have at most one operation. + */ + public static List readAllOperationsInLucene(Engine engine, MapperService mapper) throws IOException { + final List operations = new ArrayList<>(); + long maxSeqNo = Math.max(0, ((InternalEngine)engine).getLocalCheckpointTracker().getMaxSeqNo()); + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapper, 0, maxSeqNo, false)) { + Translog.Operation op; + while ((op = snapshot.next()) != null){ + operations.add(op); + } + } + return operations; + } + + /** + * 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) { + return; + } + final long maxSeqNo = ((InternalEngine) engine).getLocalCheckpointTracker().getMaxSeqNo(); + if (maxSeqNo < 0) { + return; // nothing to check + } + final Map translogOps = new HashMap<>(); + try (Translog.Snapshot snapshot = EngineTestCase.getTranslog(engine).newSnapshot()) { + Translog.Operation op; + while ((op = snapshot.next()) != null) { + translogOps.put(op.seqNo(), op); + } + } + final Map luceneOps = readAllOperationsInLucene(engine, mapper).stream() + .collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); + final long globalCheckpoint = EngineTestCase.getTranslog(engine).getLastSyncedGlobalCheckpoint(); + final long retainedOps = engine.config().getIndexSettings().getSoftDeleteRetentionOperations(); + final long seqNoForRecovery; + try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { + seqNoForRecovery = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + } + final long minSeqNoToRetain = Math.min(seqNoForRecovery, globalCheckpoint + 1 - retainedOps); + for (Translog.Operation translogOp : translogOps.values()) { + final Translog.Operation luceneOp = luceneOps.get(translogOp.seqNo()); + if (luceneOp == null) { + if (minSeqNoToRetain <= translogOp.seqNo() && translogOp.seqNo() <= maxSeqNo) { + fail("Operation not found seq# [" + translogOp.seqNo() + "], global checkpoint [" + globalCheckpoint + "], " + + "retention policy [" + retainedOps + "], maxSeqNo [" + maxSeqNo + "], translog op [" + translogOp + "]"); + } else { + continue; + } + } + assertThat(luceneOp, notNullValue()); + assertThat(luceneOp.toString(), luceneOp.primaryTerm(), equalTo(translogOp.primaryTerm())); + assertThat(luceneOp.opType(), equalTo(translogOp.opType())); + if (luceneOp.opType() == Translog.Operation.Type.INDEX) { + assertThat(luceneOp.getSource().source, equalTo(translogOp.getSource().source)); + } + } + } + + protected MapperService createMapperService(String type) throws IOException { + IndexMetaData indexMetaData = IndexMetaData.builder("test") + .settings(Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1).put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)) + .putMapping(type, "{\"properties\": {}}") + .build(); + MapperService mapperService = MapperTestUtils.newMapperService(new NamedXContentRegistry(ClusterModule.getNamedXWriteables()), + createTempDir(), Settings.EMPTY, "test"); + mapperService.merge(indexMetaData, MapperService.MergeReason.MAPPING_UPDATE); + return mapperService; + } /** * Exposes a translog associated with the given engine for testing purpose. 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 3f1f5daf5148..f2afdff9c3a3 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 @@ -60,6 +60,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.InternalEngineFactory; import org.elasticsearch.index.seqno.GlobalCheckpointSyncAction; @@ -99,10 +100,14 @@ public abstract class ESIndexLevelReplicationTestCase extends IndexShardTestCase protected final Index index = new Index("test", "uuid"); private final ShardId shardId = new ShardId(index, 0); - private final Map indexMapping = Collections.singletonMap("type", "{ \"type\": {} }"); + protected final Map indexMapping = Collections.singletonMap("type", "{ \"type\": {} }"); protected ReplicationGroup createGroup(int replicas) throws IOException { - IndexMetaData metaData = buildIndexMetaData(replicas); + return createGroup(replicas, Settings.EMPTY); + } + + protected ReplicationGroup createGroup(int replicas, Settings settings) throws IOException { + IndexMetaData metaData = buildIndexMetaData(replicas, settings, indexMapping); return new ReplicationGroup(metaData); } @@ -111,9 +116,17 @@ protected IndexMetaData buildIndexMetaData(int replicas) throws IOException { } protected IndexMetaData buildIndexMetaData(int replicas, Map mappings) throws IOException { + return buildIndexMetaData(replicas, Settings.EMPTY, mappings); + } + + protected IndexMetaData buildIndexMetaData(int replicas, Settings indexSettings, Map mappings) throws IOException { Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, replicas) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + .put(indexSettings) .build(); IndexMetaData.Builder metaData = IndexMetaData.builder(index.getName()) .settings(settings) @@ -146,7 +159,7 @@ protected class ReplicationGroup implements AutoCloseable, Iterable } }); - ReplicationGroup(final IndexMetaData indexMetaData) throws IOException { + protected ReplicationGroup(final IndexMetaData indexMetaData) throws IOException { final ShardRouting primaryRouting = this.createShardRouting("s0", true); primary = newShard(primaryRouting, indexMetaData, null, getEngineFactory(primaryRouting), () -> {}); replicas = new CopyOnWriteArrayList<>(); @@ -448,7 +461,7 @@ private void updateAllocationIDsOnPrimary() throws IOException { } } - abstract class ReplicationAction, + protected abstract class ReplicationAction, ReplicaRequest extends ReplicationRequest, Response extends ReplicationResponse> { private final Request request; @@ -456,7 +469,7 @@ abstract class ReplicationAction, private final ReplicationGroup replicationGroup; private final String opType; - ReplicationAction(Request request, ActionListener listener, ReplicationGroup group, String opType) { + protected ReplicationAction(Request request, ActionListener listener, ReplicationGroup group, String opType) { this.request = request; this.listener = listener; this.replicationGroup = group; @@ -582,11 +595,11 @@ public void markShardCopyAsStaleIfNeeded(ShardId shardId, String allocationId, R } } - class PrimaryResult implements ReplicationOperation.PrimaryResult { + protected class PrimaryResult implements ReplicationOperation.PrimaryResult { final ReplicaRequest replicaRequest; final Response finalResponse; - PrimaryResult(ReplicaRequest replicaRequest, Response finalResponse) { + public PrimaryResult(ReplicaRequest replicaRequest, Response finalResponse) { this.replicaRequest = replicaRequest; this.finalResponse = finalResponse; } 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 d2a84589669a..2f4a3dfd6c12 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 @@ -18,13 +18,8 @@ */ package org.elasticsearch.index.shard; -import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexNotFoundException; -import org.apache.lucene.index.LeafReader; -import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.store.Directory; -import org.apache.lucene.util.Bits; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.flush.FlushRequest; import org.elasticsearch.action.index.IndexRequest; @@ -57,10 +52,8 @@ import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.EngineTestCase; import org.elasticsearch.index.engine.InternalEngineFactory; -import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.SourceToParse; -import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.similarity.SimilarityService; @@ -180,37 +173,63 @@ public Directory newDirectory() throws IOException { } /** - * creates a new initializing shard. The shard will have its own unique data path. + * Creates a new initializing shard. The shard will have its own unique data path. * - * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica - * (ready to recover from another shard) + * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica (ready to recover from + * another shard) */ protected IndexShard newShard(boolean primary) throws IOException { - ShardRouting shardRouting = TestShardRouting.newShardRouting(new ShardId("index", "_na_", 0), randomAlphaOfLength(10), primary, - ShardRoutingState.INITIALIZING, - primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE); - return newShard(shardRouting); + return newShard(primary, Settings.EMPTY, new InternalEngineFactory()); } /** - * creates a new initializing shard. The shard will have its own unique data path. + * Creates a new initializing shard. The shard will have its own unique data path. + * + * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica (ready to recover from + * another shard) + * @param settings the settings to use for this shard + * @param engineFactory the engine factory to use for this shard + */ + protected IndexShard newShard(boolean primary, Settings settings, EngineFactory engineFactory) throws IOException { + final RecoverySource recoverySource = + primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE; + final ShardRouting shardRouting = + TestShardRouting.newShardRouting( + new ShardId("index", "_na_", 0), randomAlphaOfLength(10), primary, ShardRoutingState.INITIALIZING, recoverySource); + return newShard(shardRouting, settings, engineFactory); + } + + protected IndexShard newShard(ShardRouting shardRouting, final IndexingOperationListener... listeners) throws IOException { + return newShard(shardRouting, Settings.EMPTY, new InternalEngineFactory(), listeners); + } + + /** + * Creates a new initializing shard. The shard will have its own unique data path. * - * @param shardRouting the {@link ShardRouting} to use for this shard - * @param listeners an optional set of listeners to add to the shard + * @param shardRouting the {@link ShardRouting} to use for this shard + * @param settings the settings to use for this shard + * @param engineFactory the engine factory to use for this shard + * @param listeners an optional set of listeners to add to the shard */ protected IndexShard newShard( final ShardRouting shardRouting, + final Settings settings, + final EngineFactory engineFactory, final IndexingOperationListener... listeners) throws IOException { assert shardRouting.initializing() : shardRouting; - 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(); + Settings indexSettings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + .put(settings) + .build(); IndexMetaData.Builder metaData = IndexMetaData.builder(shardRouting.getIndexName()) - .settings(settings) + .settings(indexSettings) .primaryTerm(0, primaryTerm) .putMapping("_doc", "{ \"properties\": {} }"); - return newShard(shardRouting, metaData.build(), listeners); + return newShard(shardRouting, metaData.build(), engineFactory, listeners); } /** @@ -225,7 +244,7 @@ protected IndexShard newShard(ShardId shardId, boolean primary, IndexingOperatio ShardRouting shardRouting = TestShardRouting.newShardRouting(shardId, randomAlphaOfLength(5), primary, ShardRoutingState.INITIALIZING, primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE); - return newShard(shardRouting, listeners); + return newShard(shardRouting, Settings.EMPTY, new InternalEngineFactory(), listeners); } /** @@ -265,9 +284,10 @@ protected IndexShard newShard(ShardId shardId, boolean primary, String nodeId, I * @param indexMetaData indexMetaData for the shard, including any mapping * @param listeners an optional set of listeners to add to the shard */ - protected IndexShard newShard(ShardRouting routing, IndexMetaData indexMetaData, IndexingOperationListener... listeners) + protected IndexShard newShard( + ShardRouting routing, IndexMetaData indexMetaData, EngineFactory engineFactory, IndexingOperationListener... listeners) throws IOException { - return newShard(routing, indexMetaData, null, new InternalEngineFactory(), () -> {}, listeners); + return newShard(routing, indexMetaData, null, engineFactory, () -> {}, listeners); } /** @@ -372,19 +392,39 @@ protected IndexShard reinitShard(IndexShard current, ShardRouting routing, Index } /** - * creates a new empyu shard and starts it. The shard will be either a replica or a primary. + * Creates a new empty shard and starts it. The shard will randomly be a replica or a primary. */ protected IndexShard newStartedShard() throws IOException { return newStartedShard(randomBoolean()); } /** - * creates a new empty shard and starts it. + * Creates a new empty shard and starts it + * @param settings the settings to use for this shard + */ + protected IndexShard newStartedShard(Settings settings) throws IOException { + return newStartedShard(randomBoolean(), settings, new InternalEngineFactory()); + } + + /** + * Creates a new empty shard and starts it. * * @param primary controls whether the shard will be a primary or a replica. */ - protected IndexShard newStartedShard(boolean primary) throws IOException { - IndexShard shard = newShard(primary); + protected IndexShard newStartedShard(final boolean primary) throws IOException { + return newStartedShard(primary, Settings.EMPTY, new InternalEngineFactory()); + } + + /** + * Creates a new empty shard with the specified settings and engine factory and starts it. + * + * @param primary controls whether the shard will be a primary or a replica. + * @param settings the settings to use for this shard + * @param engineFactory the engine factory to use for this shard + */ + protected IndexShard newStartedShard( + final boolean primary, final Settings settings, final EngineFactory engineFactory) throws IOException { + IndexShard shard = newShard(primary, settings, engineFactory); if (primary) { recoverShardFromStore(shard); } else { @@ -401,6 +441,7 @@ 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()); @@ -582,22 +623,7 @@ private Store.MetadataSnapshot getMetadataSnapshotOrEmpty(IndexShard replica) th } protected Set getShardDocUIDs(final IndexShard shard) throws IOException { - shard.refresh("get_uids"); - try (Engine.Searcher searcher = shard.acquireSearcher("test")) { - Set ids = new HashSet<>(); - for (LeafReaderContext leafContext : searcher.reader().leaves()) { - LeafReader reader = leafContext.reader(); - 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))); - } - } - } - return ids; - } + return EngineTestCase.getDocIds(shard.getEngine(), true); } protected void assertDocCount(IndexShard shard, int docDount) throws IOException { @@ -610,6 +636,12 @@ protected void assertDocs(IndexShard shard, String... ids) throws IOException { assertThat(shardDocUIDs, hasSize(ids.length)); } + public static void assertConsistentHistoryBetweenTranslogAndLucene(IndexShard shard) throws IOException { + final Engine engine = shard.getEngineOrNull(); + if (engine != null) { + EngineTestCase.assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, shard.mapperService()); + } + } protected Engine.IndexResult indexDoc(IndexShard shard, String type, String id) throws IOException { return indexDoc(shard, type, id, "{}"); @@ -653,11 +685,14 @@ protected void updateMappings(IndexShard shard, IndexMetaData indexMetadata) { } protected Engine.DeleteResult deleteDoc(IndexShard shard, String type, String id) throws IOException { + final Engine.DeleteResult result; if (shard.routingEntry().primary()) { - return shard.applyDeleteOperationOnPrimary(Versions.MATCH_ANY, type, id, VersionType.INTERNAL); + result = shard.applyDeleteOperationOnPrimary(Versions.MATCH_ANY, type, id, VersionType.INTERNAL); + shard.updateLocalCheckpointForShard(shard.routingEntry().allocationId().getId(), shard.getEngine().getLocalCheckpoint()); } else { - return shard.applyDeleteOperationOnReplica(shard.seqNoStats().getMaxSeqNo() + 1, 0L, type, id); + result = shard.applyDeleteOperationOnReplica(shard.seqNoStats().getMaxSeqNo() + 1, 0L, type, id); } + return result; } protected void flushShard(IndexShard shard) { 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 322e2a128c97..be9e40ab4209 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -723,6 +723,10 @@ public Settings indexSettings() { } // always default delayed allocation to 0 to make sure we have tests are not delayed builder.put(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), 0); + builder.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()); + if (randomBoolean()) { + builder.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), between(0, 1000)); + } return builder.build(); } 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 9633f56dea94..19290f8cf118 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java @@ -41,6 +41,7 @@ import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.breaker.HierarchyCircuitBreakerService; import org.elasticsearch.node.MockNode; @@ -87,6 +88,14 @@ protected void startNode(long seed) throws Exception { .setOrder(0) .setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)).get(); + client().admin().indices() + .preparePutTemplate("random-soft-deletes-template") + .setPatterns(Collections.singletonList("*")) + .setOrder(0) + .setSettings(Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + ).get(); } private static void stopNode() throws IOException { 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 306f79e5e16e..4c813372fae3 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -1163,6 +1163,26 @@ private void assertOpenTranslogReferences() throws Exception { }); } + /** + * Asserts that the document history in Lucene index is consistent with Translog's on every index shard of the cluster. + * This assertion might be expensive, thus we prefer not to execute on every test but only interesting tests. + */ + public void assertConsistentHistoryBetweenTranslogAndLuceneIndex() throws IOException { + final Collection nodesAndClients = nodes.values(); + for (NodeAndClient nodeAndClient : nodesAndClients) { + IndicesService indexServices = getInstance(IndicesService.class, nodeAndClient.name); + for (IndexService indexService : indexServices) { + for (IndexShard indexShard : indexService) { + try { + IndexShardTestCase.assertConsistentHistoryBetweenTranslogAndLucene(indexShard); + } catch (AlreadyClosedException ignored) { + // shard is closed + } + } + } + } + } + private void randomlyResetClients() throws IOException { // only reset the clients on nightly tests, it causes heavy load... if (RandomizedTest.isNightly() && rarely(random)) { From 273c82d7c9d70f3378c3e2dafcd2fab851c4308f Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Fri, 31 Aug 2018 13:25:27 +1000 Subject: [PATCH 249/283] Add support for "authorization_realms" (#33262) Authorization Realms allow an authenticating realm to delegate the task of constructing a User object (with name, roles, etc) to one or more other realms. E.g. A client could authenticate using PKI, but then delegate to an LDAP realm. The LDAP realm performs a "lookup" by principal, and then does regular role-mapping from the discovered user. This commit includes: - authorization_realm support in the pki, ldap, saml & kerberos realms - docs for authorization_realms - checks that there are no "authorization chains" (whereby "realm-a" delegates to "realm-b", but "realm-b" delegates to "realm-c") Authorization realms is a platinum feature. --- .../settings/security-settings.asciidoc | 19 ++ .../configuring-kerberos-realm.asciidoc | 5 + .../configuring-ldap-realm.asciidoc | 7 +- .../configuring-pki-realm.asciidoc | 10 +- .../configuring-saml-realm.asciidoc | 5 + .../authentication/saml-guide.asciidoc | 21 +- .../authorization/mapping-roles.asciidoc | 3 + .../authorization/run-as-privilege.asciidoc | 2 +- .../license/XPackLicenseState.java | 12 +- .../xpack/core/security/authc/Realm.java | 10 + .../authc/kerberos/KerberosRealmSettings.java | 7 +- .../authc/ldap/LdapRealmSettings.java | 2 + .../security/authc/pki/PkiRealmSettings.java | 2 + .../authc/saml/SamlRealmSettings.java | 2 + .../DelegatedAuthorizationSettings.java | 27 +++ .../security/authc/AuthenticationService.java | 41 ++-- .../xpack/security/authc/Realms.java | 1 + .../authc/kerberos/KerberosRealm.java | 56 ++++-- .../xpack/security/authc/ldap/LdapRealm.java | 95 ++++++--- .../xpack/security/authc/pki/PkiRealm.java | 53 +++-- .../xpack/security/authc/saml/SamlRealm.java | 41 +++- .../support/CachingUsernamePasswordRealm.java | 30 ++- .../DelegatedAuthorizationSupport.java | 146 ++++++++++++++ .../authc/support/RealmUserLookup.java | 63 ++++++ .../KerberosRealmAuthenticateFailedTests.java | 35 ++++ .../authc/kerberos/KerberosRealmTestCase.java | 12 ++ .../authc/kerberos/KerberosRealmTests.java | 38 ++++ .../authc/ldap/ActiveDirectoryRealmTests.java | 14 ++ .../security/authc/ldap/LdapRealmTests.java | 89 ++++++++- .../security/authc/pki/PkiRealmTests.java | 115 ++++++++--- .../security/authc/saml/SamlRealmTests.java | 131 ++++++++---- .../CachingUsernamePasswordRealmTests.java | 29 +++ .../DelegatedAuthorizationSupportTests.java | 189 ++++++++++++++++++ .../authc/support/MockLookupRealm.java | 52 +++++ .../authc/support/RealmUserLookupTests.java | 128 ++++++++++++ x-pack/qa/saml-idp-tests/build.gradle | 17 +- .../authc/saml/SamlAuthenticationIT.java | 154 ++++++++++---- 37 files changed, 1430 insertions(+), 233 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/DelegatedAuthorizationSettings.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupport.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookup.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupportTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/MockLookupRealm.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookupTests.java diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index bcc00ce30c5f..1fc441a06226 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -246,6 +246,13 @@ This setting is multivalued; you can specify multiple user contexts. Required to operate in user template mode. If `user_search.base_dn` is specified, this setting is not valid. For more information on the different modes, see {xpack-ref}/ldap-realm.html[LDAP realms]. + +`authorization_realms`:: +The names of the realms that should be consulted for delegate authorization. +If this setting is used, then the LDAP realm does not perform role mapping and +instead loads the user from the listed realms. The referenced realms are +consulted in the order that they are defined in this list. +See {stack-ov}/realm-chains.html#authorization_realms[Delegating authorization to another realm] + -- NOTE: If any settings starting with `user_search` are specified, the @@ -733,6 +740,12 @@ Specifies the {xpack-ref}/security-files.html[location] of the {xpack-ref}/mapping-roles.html[YAML role mapping configuration file]. Defaults to `ES_PATH_CONF/role_mapping.yml`. +`authorization_realms`:: +The names of the realms that should be consulted for delegate authorization. +If this setting is used, then the PKI realm does not perform role mapping and +instead loads the user from the listed realms. +See {stack-ov}/realm-chains.html#authorization_realms[Delegating authorization to another realm] + `cache.ttl`:: Specifies the time-to-live for cached user entries. A user and a hash of its credentials are cached for this period of time. Use the @@ -856,6 +869,12 @@ Defaults to `false`. Specifies whether to populate the {es} user's metadata with the values that are provided by the SAML attributes. Defaults to `true`. +`authorization_realms`:: +The names of the realms that should be consulted for delegate authorization. +If this setting is used, then the SAML realm does not perform role mapping and +instead loads the user from the listed realms. +See {stack-ov}/realm-chains.html#authorization_realms[Delegating authorization to another realm] + `allowed_clock_skew`:: The maximum amount of skew that can be tolerated between the IdP's clock and the {es} node's clock. 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 30968355f3ca..9e7ed4762728 100644 --- a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc @@ -166,5 +166,10 @@ POST _xpack/security/role_mapping/kerbrolemapping // CONSOLE For more information, see {stack-ov}/mapping-roles.html[Mapping users and groups to roles]. + +NOTE: The Kerberos realm supports +{stack-ov}/realm-chains.html#authorization_realms[authorization realms] as an +alternative to role mapping. + -- diff --git a/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc index d3572ae5e1b9..a5f8c3e44120 100644 --- a/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc @@ -189,6 +189,11 @@ For more information, see {xpack-ref}/ldap-realm.html#mapping-roles-ldap[Mapping LDAP Groups to Roles] and {xpack-ref}/mapping-roles.html[Mapping Users and Groups to Roles]. + +NOTE: The LDAP realm supports +{stack-ov}/realm-chains.html#authorization_realms[authorization realms] as an +alternative to role mapping. + -- . (Optional) Configure the `metadata` setting on the LDAP realm to include extra @@ -211,4 +216,4 @@ xpack: type: ldap metadata: cn -------------------------------------------------- --- \ No newline at end of file +-- diff --git a/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc index acaa8429d07f..9a4d5fcf18bf 100644 --- a/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-pki-realm.asciidoc @@ -10,7 +10,8 @@ NOTE: You cannot use PKI certificates to authenticate users in {kib}. To use PKI in {es}, you configure a PKI realm, enable client authentication on the desired network layers (transport or http), and map the Distinguished Names -(DNs) from the user certificates to {security} roles in the role mapping file. +(DNs) from the user certificates to {security} roles in the +<> or role-mapping file. You can also use a combination of PKI and username/password authentication. For example, you can enable SSL/TLS on the transport layer and define a PKI realm to @@ -173,4 +174,9 @@ key. You can also use the authenticate API to validate your role mapping. For more information, see {xpack-ref}/mapping-roles.html[Mapping Users and Groups to Roles]. --- \ No newline at end of file + +NOTE: The PKI realm supports +{stack-ov}/realm-chains.html#authorization_realms[authorization realms] as an +alternative to role mapping. + +-- diff --git a/x-pack/docs/en/security/authentication/configuring-saml-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-saml-realm.asciidoc index cbcbeebb359e..d16e13025509 100644 --- a/x-pack/docs/en/security/authentication/configuring-saml-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-saml-realm.asciidoc @@ -219,6 +219,11 @@ access any data. Your SAML users cannot do anything until they are mapped to {security} roles. See {stack-ov}/saml-role-mapping.html[Configuring role mappings]. + +NOTE: The SAML realm supports +{stack-ov}/realm-chains.html#authorization_realms[authorization realms] as an +alternative to role mapping. + -- . {stack-ov}/saml-kibana.html[Configure {kib} to use SAML SSO]. diff --git a/x-pack/docs/en/security/authentication/saml-guide.asciidoc b/x-pack/docs/en/security/authentication/saml-guide.asciidoc index 4facceff81cd..b0077dc1ba9d 100644 --- a/x-pack/docs/en/security/authentication/saml-guide.asciidoc +++ b/x-pack/docs/en/security/authentication/saml-guide.asciidoc @@ -473,7 +473,7 @@ or separate keys used for each of those. The Elastic Stack uses X.509 certificates with RSA private keys for SAML cryptography. These keys can be generated using any standard SSL tool, including -the `elasticsearch-certutil` tool that ships with X-Pack. +the `elasticsearch-certutil` tool that ships with {xpack}. Your IdP may require that the Elastic Stack have a cryptographic key for signing SAML messages, and that you provide the corresponding signing certificate within @@ -624,9 +624,10 @@ When a user authenticates using SAML, they are identified to the Elastic Stack, but this does not automatically grant them access to perform any actions or access any data. -Your SAML users cannot do anything until they are mapped to {security} -roles. This mapping is performed through the -{ref}/security-api-put-role-mapping.html[add role mapping API]. +Your SAML users cannot do anything until they are assigned {security} +roles. This is done through either the +{ref}/security-api-put-role-mapping.html[add role mapping API], or with +<>. This is an example of a simple role mapping that grants the `kibana_user` role to any user who authenticates against the `saml1` realm: @@ -683,6 +684,18 @@ PUT /_xpack/security/role_mapping/saml-finance // CONSOLE // TEST +If your users also exist in a repository that can be directly accessed by {security} +(such as an LDAP directory) then you can use +<> instead of role mappings. + +In this case, you perform the following steps: +1. In your SAML realm, assigned a SAML attribute to act as the lookup userid, + by configuring the `attributes.principal` setting. +2. Create a new realm that can lookup users from your local repository (e.g. an + `ldap` realm) +3. In your SAML realm, set `authorization_realms` to the name of the realm you + created in step 2. + [[saml-user-metadata]] === User metadata diff --git a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc index ecafe2bd3ec9..166238c32ac5 100644 --- a/x-pack/docs/en/security/authorization/mapping-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/mapping-roles.asciidoc @@ -24,6 +24,9 @@ either role management method. For example, when you use the role mapping API, you are able to map users to both API-managed roles and file-managed roles (and likewise for file-based role-mappings). +NOTE: The PKI, LDAP, Kerberos and SAML realms support using +<> as an alternative to role mapping. + [[mapping-roles-api]] ==== Using the role mapping API diff --git a/x-pack/docs/en/security/authorization/run-as-privilege.asciidoc b/x-pack/docs/en/security/authorization/run-as-privilege.asciidoc index 93d11c0ab2af..8dba764cc1cb 100644 --- a/x-pack/docs/en/security/authorization/run-as-privilege.asciidoc +++ b/x-pack/docs/en/security/authorization/run-as-privilege.asciidoc @@ -12,7 +12,7 @@ the realm you use to authenticate. Both the internal `native` and `file` realms support this out of the box. The LDAP realm must be configured to run in <>. The Active Directory realm must be <> to support -_run as_. The PKI realm does not support _run as_. +_run as_. The PKI, Kerberos, and SAML realms do not support _run as_. To submit requests on behalf of other users, you need to have the `run_as` permission. For example, the following role grants permission to submit request 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 722c9d0e711a..a0dbc6449226 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 @@ -410,10 +410,20 @@ public AllowedRealmType allowedRealmType() { */ public boolean isCustomRoleProvidersAllowed() { final Status localStatus = status; - return (localStatus.mode == OperationMode.PLATINUM || localStatus.mode == OperationMode.TRIAL ) + return (localStatus.mode == OperationMode.PLATINUM || localStatus.mode == OperationMode.TRIAL) && localStatus.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; + } + /** * Determine if Watcher is available based on the current license. *

diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java index 2c63ca95eb98..bc8869d5d835 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Realm.java @@ -8,6 +8,8 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.security.user.User; @@ -146,6 +148,14 @@ public String toString() { return type + "/" + config.name; } + /** + * This is no-op in the base class, but allows realms to be aware of what other realms are configured + * + * @see DelegatedAuthorizationSettings + */ + public void initialize(Iterable realms, XPackLicenseState licenseState) { + } + /** * A factory interface to construct a security realm. */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java index 7524ef08c1e7..656632a2ec63 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import java.util.Set; @@ -44,7 +45,9 @@ private KerberosRealmSettings() { * @return the valid set of {@link Setting}s for a {@value #TYPE} realm */ public static Set> getSettings() { - return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE, - SETTING_REMOVE_REALM_NAME); + final Set> settings = Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, + SETTING_KRB_DEBUG_ENABLE, SETTING_REMOVE_REALM_NAME); + settings.addAll(DelegatedAuthorizationSettings.getSettings()); + return settings; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/LdapRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/LdapRealmSettings.java index 0bb9f195af7f..3f79c722be3f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/LdapRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/ldap/LdapRealmSettings.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapMetaDataResolverSettings; import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.security.authc.support.mapper.CompositeRoleMapperSettings; import java.util.HashSet; @@ -37,6 +38,7 @@ public static Set> getSettings(String type) { assert LDAP_TYPE.equals(type) : "type [" + type + "] is unknown. expected one of [" + AD_TYPE + ", " + LDAP_TYPE + "]"; settings.addAll(LdapSessionFactorySettings.getSettings()); settings.addAll(LdapUserSearchSessionFactorySettings.getSettings()); + settings.addAll(DelegatedAuthorizationSettings.getSettings()); } settings.addAll(LdapMetaDataResolverSettings.getSettings()); return settings; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java index a3539b30d3e5..53af4938a8ff 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java @@ -7,6 +7,7 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.security.authc.support.mapper.CompositeRoleMapperSettings; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; @@ -43,6 +44,7 @@ public static Set> getSettings() { settings.add(SSL_SETTINGS.truststoreAlgorithm); settings.add(SSL_SETTINGS.caPaths); + settings.addAll(DelegatedAuthorizationSettings.getSettings()); settings.addAll(CompositeRoleMapperSettings.getSettings()); return settings; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java index cf28b995127c..e254cee12430 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/saml/SamlRealmSettings.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings; @@ -89,6 +90,7 @@ public static Set> getSettings() { set.addAll(DN_ATTRIBUTE.settings()); set.addAll(NAME_ATTRIBUTE.settings()); set.addAll(MAIL_ATTRIBUTE.settings()); + set.addAll(DelegatedAuthorizationSettings.getSettings()); return set; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/DelegatedAuthorizationSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/DelegatedAuthorizationSettings.java new file mode 100644 index 000000000000..b8384a76b41a --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/DelegatedAuthorizationSettings.java @@ -0,0 +1,27 @@ +/* + * 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.security.authc.support; + +import org.elasticsearch.common.settings.Setting; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +/** + * Settings related to "Delegated Authorization" (aka Lookup Realms) + */ +public class DelegatedAuthorizationSettings { + + public static final Setting> AUTHZ_REALMS = Setting.listSetting("authorization_realms", + Collections.emptyList(), Function.identity(), Setting.Property.NodeScope); + + public static Collection> getSettings() { + return Collections.singleton(AUTHZ_REALMS); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 85084da84648..c3888ba9453c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -34,10 +34,12 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -381,33 +383,18 @@ private void consumeUser(User user, Map> message * names of users that exist using a timing attack */ private void lookupRunAsUser(final User user, String runAsUsername, Consumer userConsumer) { - final List realmsList = realms.asList(); - final BiConsumer> realmLookupConsumer = (realm, lookupUserListener) -> - realm.lookupUser(runAsUsername, ActionListener.wrap((lookedupUser) -> { - if (lookedupUser != null) { - lookedupBy = new RealmRef(realm.name(), realm.type(), nodeName); - lookupUserListener.onResponse(lookedupUser); - } else { - lookupUserListener.onResponse(null); - } - }, lookupUserListener::onFailure)); - - final IteratingActionListener userLookupListener = - new IteratingActionListener<>(ActionListener.wrap((lookupUser) -> { - if (lookupUser == null) { - // the user does not exist, but we still create a User object, which will later be rejected by authz - userConsumer.accept(new User(runAsUsername, null, user)); - } else { - userConsumer.accept(new User(lookupUser, user)); - } - }, - (e) -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken))), - realmLookupConsumer, realmsList, threadContext); - try { - userLookupListener.run(); - } catch (Exception e) { - listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken)); - } + final RealmUserLookup lookup = new RealmUserLookup(realms.asList(), threadContext); + lookup.lookup(runAsUsername, ActionListener.wrap(tuple -> { + if (tuple == null) { + // the user does not exist, but we still create a User object, which will later be rejected by authz + userConsumer.accept(new User(runAsUsername, null, user)); + } else { + User foundUser = Objects.requireNonNull(tuple.v1()); + Realm realm = Objects.requireNonNull(tuple.v2()); + lookedupBy = new RealmRef(realm.name(), realm.type(), nodeName); + userConsumer.accept(new User(foundUser, user)); + } + }, exception -> listener.onFailure(request.exceptionProcessingRequest(exception, authenticationToken)))); } /** 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 8b80c1f1d1ca..d2573b9343d0 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 @@ -93,6 +93,7 @@ public Realms(Settings settings, Environment env, Map fac this.standardRealmsOnly = Collections.unmodifiableList(standardRealms); this.nativeRealmsOnly = Collections.unmodifiableList(nativeRealms); + realms.forEach(r -> r.initialize(this, licenseState)); } @Override 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 d57bb3052d8e..9c531d3159f1 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 @@ -13,6 +13,7 @@ import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -21,6 +22,7 @@ import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.support.CachingRealm; +import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.ietf.jgss.GSSException; @@ -63,6 +65,7 @@ public final class KerberosRealm extends Realm implements CachingRealm { private final Path keytabPath; private final boolean enableKerberosDebug; private final boolean removeRealmName; + private DelegatedAuthorizationSupport delegatedRealms; public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nativeRoleMappingStore, final ThreadPool threadPool) { this(config, nativeRoleMappingStore, new KerberosTicketValidator(), threadPool, null); @@ -100,6 +103,15 @@ public KerberosRealm(final RealmConfig config, final NativeRoleMappingStore nati } this.enableKerberosDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); this.removeRealmName = KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.get(config.settings()); + this.delegatedRealms = null; + } + + @Override + public void initialize(Iterable realms, XPackLicenseState licenseState) { + if (delegatedRealms != null) { + throw new IllegalStateException("Realm has already been initialized"); + } + delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState); } @Override @@ -133,13 +145,14 @@ public AuthenticationToken token(final ThreadContext context) { @Override public void authenticate(final AuthenticationToken token, final ActionListener listener) { + assert delegatedRealms != null : "Realm has not been initialized correctly"; assert token instanceof KerberosAuthenticationToken; final KerberosAuthenticationToken kerbAuthnToken = (KerberosAuthenticationToken) token; kerberosTicketValidator.validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, enableKerberosDebug, ActionListener.wrap(userPrincipalNameOutToken -> { if (userPrincipalNameOutToken.v1() != null) { final String username = maybeRemoveRealmName(userPrincipalNameOutToken.v1()); - buildUser(username, userPrincipalNameOutToken.v2(), listener); + resolveUser(username, userPrincipalNameOutToken.v2(), listener); } else { /** * This is when security context could not be established may be due to ongoing @@ -192,35 +205,36 @@ private void handleException(Exception e, final ActionListener listener) { + private void resolveUser(final String username, 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 User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null; - if (user != null) { - /** - * TODO: bizybot If authorizing realms configured, resolve user from those - * realms and then return. - */ - listener.onResponse(AuthenticationResult.success(user)); + + if (delegatedRealms.hasDelegation()) { + delegatedRealms.resolve(username, listener); } else { - /** - * TODO: bizybot If authorizing realms configured, resolve user from those - * realms, cache it and then return. - */ - final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config); - userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { - final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); - if (userPrincipalNameToUserCache != null) { - userPrincipalNameToUserCache.put(username, computedUser); - } - listener.onResponse(AuthenticationResult.success(computedUser)); - }, listener::onFailure)); + final User user = (userPrincipalNameToUserCache != null) ? userPrincipalNameToUserCache.get(username) : null; + if (user != null) { + listener.onResponse(AuthenticationResult.success(user)); + } else { + buildUser(username, listener); + } } } + private void buildUser(final String username, final ActionListener listener) { + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config); + userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { + final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + if (userPrincipalNameToUserCache != null) { + userPrincipalNameToUserCache.put(username, computedUser); + } + listener.onResponse(AuthenticationResult.success(computedUser)); + }, listener::onFailure)); + } + @Override public void lookupUser(final String username, final ActionListener listener) { listener.onResponse(null); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java index 87749850141b..193b33b7d8fe 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealm.java @@ -8,7 +8,6 @@ import com.unboundid.ldap.sdk.LDAPException; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ContextPreservingActionListener; @@ -16,10 +15,13 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPool.Names; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.ldap.LdapRealmSettings; @@ -31,6 +33,7 @@ import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; import org.elasticsearch.xpack.security.authc.support.CachingUsernamePasswordRealm; +import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData; import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper; @@ -53,7 +56,7 @@ public final class LdapRealm extends CachingUsernamePasswordRealm { private final UserRoleMapper roleMapper; private final ThreadPool threadPool; private final TimeValue executionTimeout; - + private DelegatedAuthorizationSupport delegatedRealms; public LdapRealm(String type, RealmConfig config, SSLService sslService, ResourceWatcherService watcherService, @@ -118,6 +121,7 @@ static SessionFactory sessionFactory(RealmConfig config, SSLService sslService, */ @Override protected void doAuthenticate(UsernamePasswordToken token, ActionListener listener) { + assert delegatedRealms != null : "Realm has not been initialized correctly"; // we submit to the threadpool because authentication using LDAP will execute blocking I/O for a bind request and we don't want // network threads stuck waiting for a socket to connect. After the bind, then all interaction with LDAP should be async final CancellableLdapRunnable cancellableLdapRunnable = new CancellableLdapRunnable<>(listener, @@ -159,6 +163,14 @@ private ContextPreservingActionListener contextPreservingListener(L sessionListener); } + @Override + public void initialize(Iterable realms, XPackLicenseState licenseState) { + if (delegatedRealms != null) { + throw new IllegalStateException("Realm has already been initialized"); + } + delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState); + } + @Override public void usageStats(ActionListener> listener) { super.usageStats(ActionListener.wrap(usage -> { @@ -171,39 +183,56 @@ public void usageStats(ActionListener> listener) { } private static void buildUser(LdapSession session, String username, ActionListener listener, - UserRoleMapper roleMapper) { + UserRoleMapper roleMapper, DelegatedAuthorizationSupport delegatedAuthz) { + assert delegatedAuthz != null : "DelegatedAuthorizationSupport is null"; if (session == null) { listener.onResponse(AuthenticationResult.notHandled()); + } else if (delegatedAuthz.hasDelegation()) { + delegatedAuthz.resolve(username, listener); } else { - boolean loadingGroups = false; - try { - final Consumer onFailure = e -> { - IOUtils.closeWhileHandlingException(session); - listener.onFailure(e); - }; - session.resolve(ActionListener.wrap((ldapData) -> { - final Map metadata = MapBuilder.newMapBuilder() - .put("ldap_dn", session.userDn()) - .put("ldap_groups", ldapData.groups) - .putAll(ldapData.metaData) - .map(); - final UserData user = new UserData(username, session.userDn(), ldapData.groups, - metadata, session.realm()); - roleMapper.resolveRoles(user, ActionListener.wrap( - roles -> { - IOUtils.close(session); - String[] rolesArray = roles.toArray(new String[roles.size()]); - listener.onResponse(AuthenticationResult.success( - new User(username, rolesArray, null, null, metadata, true)) - ); - }, onFailure - )); - }, onFailure)); - loadingGroups = true; - } finally { - if (loadingGroups == false) { - session.close(); - } + lookupUserFromSession(username, session, roleMapper, listener); + } + } + + @Override + protected void handleCachedAuthentication(User user, ActionListener listener) { + if (delegatedRealms.hasDelegation()) { + delegatedRealms.resolve(user.principal(), listener); + } else { + super.handleCachedAuthentication(user, listener); + } + } + + private static void lookupUserFromSession(String username, LdapSession session, UserRoleMapper roleMapper, + ActionListener listener) { + boolean loadingGroups = false; + try { + final Consumer onFailure = e -> { + IOUtils.closeWhileHandlingException(session); + listener.onFailure(e); + }; + session.resolve(ActionListener.wrap((ldapData) -> { + final Map metadata = MapBuilder.newMapBuilder() + .put("ldap_dn", session.userDn()) + .put("ldap_groups", ldapData.groups) + .putAll(ldapData.metaData) + .map(); + final UserData user = new UserData(username, session.userDn(), ldapData.groups, + metadata, session.realm()); + roleMapper.resolveRoles(user, ActionListener.wrap( + roles -> { + IOUtils.close(session); + String[] rolesArray = roles.toArray(new String[roles.size()]); + listener.onResponse(AuthenticationResult.success( + new User(username, rolesArray, null, null, metadata, true)) + ); + }, onFailure + )); + }, onFailure)); + loadingGroups = true; + } finally { + if (loadingGroups == false) { + session.close(); } } } @@ -233,7 +262,7 @@ public void onResponse(LdapSession session) { resultListener.onResponse(AuthenticationResult.notHandled()); } else { ldapSessionAtomicReference.set(session); - buildUser(session, username, resultListener, roleMapper); + buildUser(session, username, resultListener, roleMapper, delegatedRealms); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index 7b9eabfd7066..4d13f332ffe2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; @@ -31,12 +32,12 @@ import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; import org.elasticsearch.xpack.security.authc.BytesKey; import org.elasticsearch.xpack.security.authc.support.CachingRealm; +import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.CompositeRoleMapper; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import javax.net.ssl.X509TrustManager; - import java.security.MessageDigest; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; @@ -75,6 +76,7 @@ public class PkiRealm extends Realm implements CachingRealm { private final Pattern principalPattern; private final UserRoleMapper roleMapper; private final Cache cache; + private DelegatedAuthorizationSupport delegatedRealms; public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, NativeRoleMappingStore nativeRoleMappingStore) { this(config, new CompositeRoleMapper(PkiRealmSettings.TYPE, config, watcherService, nativeRoleMappingStore)); @@ -91,6 +93,15 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ .setExpireAfterWrite(PkiRealmSettings.CACHE_TTL_SETTING.get(config.settings())) .setMaximumWeight(PkiRealmSettings.CACHE_MAX_USERS_SETTING.get(config.settings())) .build(); + this.delegatedRealms = null; + } + + @Override + public void initialize(Iterable realms, XPackLicenseState licenseState) { + if (delegatedRealms != null) { + throw new IllegalStateException("Realm has already been initialized"); + } + delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState); } @Override @@ -105,32 +116,50 @@ public X509AuthenticationToken token(ThreadContext context) { @Override public void authenticate(AuthenticationToken authToken, ActionListener listener) { + assert delegatedRealms != null : "Realm has not been initialized correctly"; X509AuthenticationToken token = (X509AuthenticationToken)authToken; try { final BytesKey fingerprint = computeFingerprint(token.credentials()[0]); User user = cache.get(fingerprint); if (user != null) { - listener.onResponse(AuthenticationResult.success(user)); + if (delegatedRealms.hasDelegation()) { + delegatedRealms.resolve(token.principal(), listener); + } else { + listener.onResponse(AuthenticationResult.success(user)); + } } else if (isCertificateChainTrusted(trustManager, token, logger) == false) { listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " is not trusted", null)); } else { - final Map metadata = Collections.singletonMap("pki_dn", token.dn()); - final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(token.principal(), - token.dn(), Collections.emptySet(), metadata, this.config); - roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { - final User computedUser = - new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true); - try (ReleasableLock ignored = readLock.acquire()) { - cache.put(fingerprint, computedUser); + final ActionListener cachingListener = ActionListener.wrap(result -> { + if (result.isAuthenticated()) { + try (ReleasableLock ignored = readLock.acquire()) { + cache.put(fingerprint, result.getUser()); + } } - listener.onResponse(AuthenticationResult.success(computedUser)); - }, listener::onFailure)); + listener.onResponse(result); + }, listener::onFailure); + if (delegatedRealms.hasDelegation()) { + delegatedRealms.resolve(token.principal(), cachingListener); + } else { + this.buildUser(token, cachingListener); + } } } catch (CertificateEncodingException e) { listener.onResponse(AuthenticationResult.unsuccessful("Certificate for " + token.dn() + " has encoding issues", e)); } } + private void buildUser(X509AuthenticationToken token, ActionListener listener) { + final Map metadata = Collections.singletonMap("pki_dn", token.dn()); + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(token.principal(), + token.dn(), Collections.emptySet(), metadata, this.config); + roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { + final User computedUser = + new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true); + listener.onResponse(AuthenticationResult.success(computedUser)); + }, listener::onFailure)); + } + @Override public void lookupUser(String username, ActionListener listener) { listener.onResponse(null); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java index cc160c8f78b3..4a9db7c5d612 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/saml/SamlRealm.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; @@ -46,10 +47,12 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLConfiguration; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; +import org.elasticsearch.xpack.core.ssl.SSLConfiguration; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings; import org.elasticsearch.xpack.security.authc.Realms; import org.elasticsearch.xpack.security.authc.TokenService; +import org.elasticsearch.xpack.security.authc.support.DelegatedAuthorizationSupport; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.opensaml.core.criterion.EntityIdCriterion; import org.opensaml.saml.common.xml.SAMLConstants; @@ -117,6 +120,7 @@ import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.NAME_ATTRIBUTE; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.POPULATE_USER_METADATA; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.PRINCIPAL_ATTRIBUTE; +import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.REQUESTED_AUTHN_CONTEXT_CLASS_REF; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SIGNING_KEY_ALIAS; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SIGNING_MESSAGE_TYPES; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SIGNING_SETTINGS; @@ -124,7 +128,6 @@ import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SP_ENTITY_ID; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.SP_LOGOUT; import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.TYPE; -import static org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings.REQUESTED_AUTHN_CONTEXT_CLASS_REF; /** * This class is {@link Releasable} because it uses a library that thinks timers and timer tasks @@ -166,6 +169,7 @@ public final class SamlRealm extends Realm implements Releasable { private final AttributeParser nameAttribute; private final AttributeParser mailAttribute; + private DelegatedAuthorizationSupport delegatedRealms; /** * Factory for SAML realm. @@ -231,6 +235,14 @@ public static SamlRealm create(RealmConfig config, SSLService sslService, Resour this.releasables = new ArrayList<>(); } + @Override + public void initialize(Iterable realms, XPackLicenseState licenseState) { + if (delegatedRealms != null) { + throw new IllegalStateException("Realm has already been initialized"); + } + delegatedRealms = new DelegatedAuthorizationSupport(realms, config, licenseState); + } + static String require(RealmConfig config, Setting setting) { final String value = setting.get(config.settings()); if (value.isEmpty()) { @@ -402,14 +414,27 @@ public void authenticate(AuthenticationToken authenticationToken, ActionListener } } - private void buildUser(SamlAttributes attributes, ActionListener listener) { + private void buildUser(SamlAttributes attributes, ActionListener baseListener) { final String principal = resolveSingleValueAttribute(attributes, principalAttribute, PRINCIPAL_ATTRIBUTE.name()); if (Strings.isNullOrEmpty(principal)) { - listener.onResponse(AuthenticationResult.unsuccessful( + baseListener.onResponse(AuthenticationResult.unsuccessful( principalAttribute + " not found in " + attributes.attributes(), null)); return; } + final Map tokenMetadata = createTokenMetadata(attributes.name(), attributes.session()); + ActionListener wrappedListener = ActionListener.wrap(auth -> { + if (auth.isAuthenticated()) { + config.threadContext().putTransient(CONTEXT_TOKEN_DATA, tokenMetadata); + } + baseListener.onResponse(auth); + }, baseListener::onFailure); + + if (delegatedRealms.hasDelegation()) { + delegatedRealms.resolve(principal, wrappedListener); + return; + } + final Map userMeta = new HashMap<>(); if (populateUserMetadata) { for (SamlAttributes.SamlAttribute a : attributes.attributes()) { @@ -424,7 +449,6 @@ private void buildUser(SamlAttributes attributes, ActionListener tokenMetadata = createTokenMetadata(attributes.name(), attributes.session()); final List groups = groupsAttribute.getAttribute(attributes); final String dn = resolveSingleValueAttribute(attributes, dnAttribute, DN_ATTRIBUTE.name()); @@ -433,9 +457,8 @@ private void buildUser(SamlAttributes attributes, ActionListener { final User user = new User(principal, roles.toArray(new String[roles.size()]), name, mail, userMeta, true); - config.threadContext().putTransient(CONTEXT_TOKEN_DATA, tokenMetadata); - listener.onResponse(AuthenticationResult.success(user)); - }, listener::onFailure)); + wrappedListener.onResponse(AuthenticationResult.success(user)); + }, wrappedListener::onFailure)); } public Map createTokenMetadata(SamlNameId nameId, String session) { @@ -745,10 +768,10 @@ static AttributeParser forSetting(Logger logger, SamlRealmSettings.AttributeSett attributes -> attributes.getAttributeValues(attributeName)); } } else if (required) { - throw new SettingsException("Setting" + RealmSettings.getFullSettingKey(realmConfig, setting.getAttribute()) + throw new SettingsException("Setting " + RealmSettings.getFullSettingKey(realmConfig, setting.getAttribute()) + " is required"); } else if (setting.getPattern().exists(settings)) { - throw new SettingsException("Setting" + RealmSettings.getFullSettingKey(realmConfig, setting.getPattern()) + throw new SettingsException("Setting " + RealmSettings.getFullSettingKey(realmConfig, setting.getPattern()) + " cannot be set unless " + RealmSettings.getFullSettingKey(realmConfig, setting.getAttribute()) + " is also set"); } else { return new AttributeParser("No SAML attribute for [" + setting.name() + "]", attributes -> Collections.emptyList()); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java index 6e321f9f7dd5..af93a180072a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java @@ -39,9 +39,9 @@ protected CachingUsernamePasswordRealm(String type, RealmConfig config, ThreadPo final TimeValue ttl = CachingUsernamePasswordRealmSettings.CACHE_TTL_SETTING.get(config.settings()); if (ttl.getNanos() > 0) { cache = CacheBuilder.>builder() - .setExpireAfterWrite(ttl) - .setMaximumWeight(CachingUsernamePasswordRealmSettings.CACHE_MAX_USERS_SETTING.get(config.settings())) - .build(); + .setExpireAfterWrite(ttl) + .setMaximumWeight(CachingUsernamePasswordRealmSettings.CACHE_MAX_USERS_SETTING.get(config.settings())) + .build(); } else { cache = null; } @@ -108,10 +108,16 @@ private void authenticateWithCache(UsernamePasswordToken token, ActionListener { if (authenticatedUserWithHash != null && authenticatedUserWithHash.verify(token.credentials())) { // cached credential hash matches the credential hash for this forestalled request - final User user = authenticatedUserWithHash.user; - logger.debug("realm [{}] authenticated user [{}], with roles [{}], from cache", name(), token.principal(), - user.roles()); - listener.onResponse(AuthenticationResult.success(user)); + handleCachedAuthentication(authenticatedUserWithHash.user, ActionListener.wrap(cacheResult -> { + if (cacheResult.isAuthenticated()) { + logger.debug("realm [{}] authenticated user [{}], with roles [{}]", + name(), token.principal(), cacheResult.getUser().roles()); + } else { + logger.debug("realm [{}] authenticated user [{}] from cache, but then failed [{}]", + name(), token.principal(), cacheResult.getMessage()); + } + listener.onResponse(cacheResult); + }, listener::onFailure)); } else { // The inflight request has failed or its credential hash does not match the // hash of the credential for this forestalled request. @@ -153,6 +159,16 @@ private void authenticateWithCache(UsernamePasswordToken token, ActionListener listener) { + listener.onResponse(AuthenticationResult.success(user)); + } + @Override public void usageStats(ActionListener> listener) { super.usageStats(ActionListener.wrap(stats -> { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupport.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupport.java new file mode 100644 index 000000000000..ff6fc6042e7e --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupport.java @@ -0,0 +1,146 @@ +/* + * 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.security.authc.support; + +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.authc.RealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.Strings.collectionToDelimitedString; + +/** + * Utility class for supporting "delegated authorization" (aka "authorization_realms", aka "lookup realms"). + * A {@link Realm} may support delegating authorization to another realm. It does this by registering a + * setting for {@link DelegatedAuthorizationSettings#AUTHZ_REALMS}, and constructing an instance of this + * class. Then, after the realm has performed any authentication steps, if {@link #hasDelegation()} is + * {@code true}, it delegates the construction of the {@link User} object and {@link AuthenticationResult} + * to {@link #resolve(String, ActionListener)}. + */ +public class DelegatedAuthorizationSupport { + + private final RealmUserLookup lookup; + private final Logger logger; + private final XPackLicenseState licenseState; + + /** + * Resolves the {@link DelegatedAuthorizationSettings#AUTHZ_REALMS} setting from {@code config} and calls + * {@link #DelegatedAuthorizationSupport(Iterable, List, Settings, ThreadContext, XPackLicenseState)} + */ + public DelegatedAuthorizationSupport(Iterable allRealms, RealmConfig config, XPackLicenseState licenseState) { + this(allRealms, DelegatedAuthorizationSettings.AUTHZ_REALMS.get(config.settings()), config.globalSettings(), config.threadContext(), + licenseState); + } + + /** + * Constructs a new object that delegates to the named realms ({@code lookupRealms}), which must exist within + * {@code allRealms}. + * @throws IllegalArgumentException if one of the specified realms does not exist + */ + protected DelegatedAuthorizationSupport(Iterable allRealms, List lookupRealms, Settings settings, + ThreadContext threadContext, XPackLicenseState licenseState) { + final List resolvedLookupRealms = resolveRealms(allRealms, lookupRealms); + checkForRealmChains(resolvedLookupRealms, settings); + this.lookup = new RealmUserLookup(resolvedLookupRealms, threadContext); + this.logger = Loggers.getLogger(getClass()); + this.licenseState = licenseState; + } + + /** + * Are there any realms configured for delegated lookup + */ + public boolean hasDelegation() { + return this.lookup.hasRealms(); + } + + /** + * Attempts to find the user specified by {@code username} in one of the delegated realms. + * The realms are searched in the order specified during construction. + * Returns a {@link AuthenticationResult#success(User) successful result} if a {@link User} + * was found, otherwise returns an + * {@link AuthenticationResult#unsuccessful(String, Exception) unsuccessful result} + * with a meaningful diagnostic message. + */ + public void resolve(String username, ActionListener resultListener) { + if (licenseState.isAuthorizationRealmAllowed() == false) { + resultListener.onResponse(AuthenticationResult.unsuccessful( + DelegatedAuthorizationSettings.AUTHZ_REALMS.getKey() + " are not permitted", + LicenseUtils.newComplianceException(DelegatedAuthorizationSettings.AUTHZ_REALMS.getKey()) + )); + return; + } + if (hasDelegation() == false) { + resultListener.onResponse(AuthenticationResult.unsuccessful( + "No [" + DelegatedAuthorizationSettings.AUTHZ_REALMS.getKey() + "] have been configured", null)); + return; + } + ActionListener> userListener = ActionListener.wrap(tuple -> { + if (tuple != null) { + logger.trace("Found user " + tuple.v1() + " in realm " + tuple.v2()); + resultListener.onResponse(AuthenticationResult.success(tuple.v1())); + } else { + resultListener.onResponse(AuthenticationResult.unsuccessful("the principal [" + username + + "] was authenticated, but no user could be found in realms [" + collectionToDelimitedString(lookup.getRealms(), ",") + + "]", null)); + } + }, resultListener::onFailure); + lookup.lookup(username, userListener); + } + + private List resolveRealms(Iterable allRealms, List lookupRealms) { + final List result = new ArrayList<>(lookupRealms.size()); + for (String name : lookupRealms) { + result.add(findRealm(name, allRealms)); + } + assert result.size() == lookupRealms.size(); + return result; + } + + /** + * Checks for (and rejects) chains of delegation in the provided realms. + * A chain occurs when "realmA" delegates authorization to "realmB", and realmB also delegates authorization (to any realm). + * Since "realmB" does not handle its own authorization, it is not a valid target for delegated authorization. + * @param delegatedRealms The list of realms that are going to be used for authorization. If is an error if any of these realms are + * also configured to delegate their authorization. + * @throws IllegalArgumentException if a chain is detected + */ + private void checkForRealmChains(Iterable delegatedRealms, Settings globalSettings) { + final Map settingsByRealm = RealmSettings.getRealmSettings(globalSettings); + for (Realm realm : delegatedRealms) { + final Settings realmSettings = settingsByRealm.get(realm.name()); + if (realmSettings != null && DelegatedAuthorizationSettings.AUTHZ_REALMS.exists(realmSettings)) { + throw new IllegalArgumentException("cannot use realm [" + realm + + "] as an authorization realm - it is already delegating authorization to [" + + DelegatedAuthorizationSettings.AUTHZ_REALMS.get(realmSettings) + "]"); + } + } + } + + private Realm findRealm(String name, Iterable allRealms) { + for (Realm realm : allRealms) { + if (name.equals(realm.name())) { + return realm; + } + } + throw new IllegalArgumentException("configured authorization realm [" + name + "] does not exist (or is not enabled)"); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookup.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookup.java new file mode 100644 index 000000000000..428b7c1e4a1c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookup.java @@ -0,0 +1,63 @@ +/* + * 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.security.authc.support; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.xpack.core.common.IteratingActionListener; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.Collections; +import java.util.List; + +public class RealmUserLookup { + + private final List realms; + private final ThreadContext threadContext; + + public RealmUserLookup(List realms, ThreadContext threadContext) { + this.realms = realms; + this.threadContext = threadContext; + } + + public List getRealms() { + return Collections.unmodifiableList(realms); + } + + public boolean hasRealms() { + return realms.isEmpty() == false; + } + + /** + * Lookup the {@code principal} in the list of {@link #realms}. + * The realms are consulted in order. When one realm responds with a non-null {@link User}, this + * is returned with the matching realm, through the {@code listener}. + * If no user if found (including the case where the {@link #realms} list is empty), then + * {@link ActionListener#onResponse(Object)} is called with a {@code null} {@link Tuple}. + */ + public void lookup(String principal, ActionListener> listener) { + final IteratingActionListener, ? extends Realm> userLookupListener = + new IteratingActionListener<>(listener, + (realm, lookupUserListener) -> realm.lookupUser(principal, + ActionListener.wrap(foundUser -> { + if (foundUser != null) { + lookupUserListener.onResponse(new Tuple<>(foundUser, realm)); + } else { + lookupUserListener.onResponse(null); + } + }, + lookupUserListener::onFailure)), + realms, threadContext); + try { + userLookupListener.run(); + } catch (Exception e) { + listener.onFailure(e); + } + } +} 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 5bc239241cf1..7c5904d048a6 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 @@ -11,13 +11,20 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; import org.elasticsearch.xpack.core.security.user.User; import org.ietf.jgss.GSSException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import javax.security.auth.login.LoginException; @@ -29,7 +36,9 @@ import static org.mockito.AdditionalMatchers.aryEq; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class KerberosRealmAuthenticateFailedTests extends KerberosRealmTestCase { @@ -105,4 +114,30 @@ public void testAuthenticateDifferentFailureScenarios() throws LoginException, G any(ActionListener.class)); } } + + public void testDelegatedAuthorizationFailedToResolve() throws Exception { + final String username = randomPrincipalName(); + final MockLookupRealm otherRealm = new MockLookupRealm(new RealmConfig("other_realm", Settings.EMPTY, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings))); + final User lookupUser = new User(randomAlphaOfLength(5)); + otherRealm.registerUser(lookupUser); + + settings = Settings.builder().put(settings).putList("authorization_realms", "other_realm").build(); + final KerberosRealm kerberosRealm = createKerberosRealm(Collections.singletonList(otherRealm), username); + 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()); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, "out-token"), null); + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); + + final PlainActionFuture future = new PlainActionFuture<>(); + kerberosRealm.authenticate(kerberosAuthenticationToken, future); + + AuthenticationResult result = future.actionGet(); + assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.CONTINUE))); + verify(mockKerberosTicketValidator, times(1)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), + any(ActionListener.class)); + verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm); + verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore); + } } 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 9c2c6484c82a..dd83da49a0bb 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 @@ -13,11 +13,13 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.support.Exceptions; @@ -30,6 +32,7 @@ import java.nio.file.Path; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -58,6 +61,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase { protected KerberosTicketValidator mockKerberosTicketValidator; protected NativeRoleMappingStore mockNativeRoleMappingStore; + protected XPackLicenseState licenseState; protected static final Set roles = Sets.newHashSet("admin", "kibana_user"); @@ -69,6 +73,8 @@ public void setup() throws Exception { globalSettings = Settings.builder().put("path.home", dir).build(); settings = KerberosTestCase.buildKerberosRealmSettings(KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), "asa").toString(), 100, "10m", true, randomBoolean()); + licenseState = mock(XPackLicenseState.class); + when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); } @After @@ -102,12 +108,18 @@ protected void assertSuccessAuthenticationResult(final User expectedUser, final } protected KerberosRealm createKerberosRealm(final String... userForRoleMapping) { + return createKerberosRealm(Collections.emptyList(), userForRoleMapping); + } + + protected KerberosRealm createKerberosRealm(final List delegatedRealms, final String... userForRoleMapping) { config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); mockNativeRoleMappingStore = roleMappingStore(Arrays.asList(userForRoleMapping)); mockKerberosTicketValidator = mock(KerberosTicketValidator.class); final KerberosRealm kerberosRealm = new KerberosRealm(config, mockNativeRoleMappingStore, mockKerberosTicketValidator, threadPool, null); + Collections.shuffle(delegatedRealms, random()); + kerberosRealm.initialize(delegatedRealms, licenseState); return kerberosRealm; } 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 fee8df535f25..d35068fd07af 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 @@ -12,6 +12,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.xpack.core.security.user.User; @@ -20,6 +21,7 @@ import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper.UserData; import org.ietf.jgss.GSSException; @@ -34,6 +36,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.Locale; import java.util.Set; @@ -47,6 +50,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -160,4 +164,38 @@ private void assertKerberosRealmConstructorFails(final String keytabPath, final () -> new KerberosRealm(config, mockNativeRoleMappingStore, mockKerberosTicketValidator, threadPool, null)); assertThat(iae.getMessage(), is(equalTo(expectedErrorMessage))); } + + public void testDelegatedAuthorization() throws Exception { + final String username = randomPrincipalName(); + final String expectedUsername = maybeRemoveRealmName(username); + final MockLookupRealm otherRealm = spy(new MockLookupRealm(new RealmConfig("other_realm", Settings.EMPTY, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)))); + final User lookupUser = new User(expectedUsername, new String[] { "admin-role" }, expectedUsername, + expectedUsername + "@example.com", Collections.singletonMap("k1", "v1"), true); + otherRealm.registerUser(lookupUser); + + settings = Settings.builder().put(settings).putList("authorization_realms", "other_realm").build(); + final KerberosRealm kerberosRealm = createKerberosRealm(Collections.singletonList(otherRealm), username); + final User expectedUser = lookupUser; + 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()); + mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(username, "out-token"), null); + final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); + + PlainActionFuture future = new PlainActionFuture<>(); + kerberosRealm.authenticate(kerberosAuthenticationToken, future); + assertSuccessAuthenticationResult(expectedUser, "out-token", future.actionGet()); + + future = new PlainActionFuture<>(); + kerberosRealm.authenticate(kerberosAuthenticationToken, future); + assertSuccessAuthenticationResult(expectedUser, "out-token", future.actionGet()); + + verify(mockKerberosTicketValidator, times(2)).validateTicket(aryEq(decodedTicket), eq(keytabPath), eq(krbDebug), + any(ActionListener.class)); + verify(mockNativeRoleMappingStore).refreshRealmOnChange(kerberosRealm); + verifyNoMoreInteractions(mockKerberosTicketValidator, mockNativeRoleMappingStore); + verify(otherRealm, times(2)).lookupUser(eq(expectedUsername), any(ActionListener.class)); + } } + diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java index 2c6756aada7a..2f5147ca2b17 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRealmTests.java @@ -22,6 +22,8 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.TestUtils; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; @@ -48,6 +50,7 @@ import java.security.AccessController; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -91,6 +94,7 @@ public class ActiveDirectoryRealmTests extends ESTestCase { private ThreadPool threadPool; private Settings globalSettings; private SSLService sslService; + private XPackLicenseState licenseState; @BeforeClass public static void setNumberOfLdapServers() { @@ -125,6 +129,7 @@ public void start() throws Exception { resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); globalSettings = Settings.builder().put("path.home", createTempDir()).build(); sslService = new SSLService(globalSettings, TestEnvironment.newEnvironment(globalSettings)); + licenseState = new TestUtils.UpdatableLicenseState(); } @After @@ -163,6 +168,7 @@ public void testAuthenticateUserPrincipleName() throws Exception { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService, threadPool); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future); @@ -179,6 +185,7 @@ public void testAuthenticateSAMAccountName() throws Exception { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService, threadPool); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); // Thor does not have a UPN of form CN=Thor@ad.test.elasticsearch.com PlainActionFuture future = new PlainActionFuture<>(); @@ -203,6 +210,7 @@ public void testAuthenticateCachesSuccessfulAuthentications() throws Exception { ActiveDirectorySessionFactory sessionFactory = spy(new ActiveDirectorySessionFactory(config, sslService, threadPool)); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); int count = randomIntBetween(2, 10); for (int i = 0; i < count; i++) { @@ -221,6 +229,7 @@ public void testAuthenticateCachingCanBeDisabled() throws Exception { ActiveDirectorySessionFactory sessionFactory = spy(new ActiveDirectorySessionFactory(config, sslService, threadPool)); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); int count = randomIntBetween(2, 10); for (int i = 0; i < count; i++) { @@ -239,6 +248,7 @@ public void testAuthenticateCachingClearsCacheOnRoleMapperRefresh() throws Excep ActiveDirectorySessionFactory sessionFactory = spy(new ActiveDirectorySessionFactory(config, sslService, threadPool)); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); int count = randomIntBetween(2, 10); for (int i = 0; i < count; i++) { @@ -287,6 +297,7 @@ private void doUnauthenticatedLookup(boolean pooled) throws Exception { try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService, threadPool)) { DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); PlainActionFuture future = new PlainActionFuture<>(); realm.lookupUser("CN=Thor", future); @@ -304,6 +315,7 @@ public void testRealmMapsGroupsToRoles() throws Exception { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService, threadPool); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=ironman", new SecureString(PASSWORD)), future); @@ -320,6 +332,7 @@ public void testRealmMapsUsersToRoles() throws Exception { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService, threadPool); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(new UsernamePasswordToken("CN=Thor", new SecureString(PASSWORD)), future); @@ -338,6 +351,7 @@ public void testRealmUsageStats() throws Exception { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService, threadPool); DnRoleMapper roleMapper = new DnRoleMapper(config, resourceWatcherService); LdapRealm realm = new LdapRealm(LdapRealmSettings.AD_TYPE, config, sessionFactory, roleMapper, threadPool); + realm.initialize(Collections.singleton(realm), licenseState); PlainActionFuture> future = new PlainActionFuture<>(); realm.usageStats(future); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java index 4aff821217d1..fb20527575df 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; @@ -25,6 +26,7 @@ import org.elasticsearch.xpack.core.security.authc.ldap.LdapSessionFactorySettings; import org.elasticsearch.xpack.core.security.authc.ldap.support.LdapSearchScope; import org.elasticsearch.xpack.core.security.authc.support.CachingUsernamePasswordRealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; import org.elasticsearch.xpack.core.security.authc.support.DnRoleMapperSettings; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.user.User; @@ -33,10 +35,12 @@ import org.elasticsearch.xpack.security.authc.ldap.support.LdapTestCase; import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; import org.elasticsearch.xpack.security.authc.support.DnRoleMapper; +import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; import org.junit.After; import org.junit.Before; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -50,11 +54,14 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class LdapRealmTests extends LdapTestCase { @@ -68,6 +75,7 @@ public class LdapRealmTests extends LdapTestCase { private ResourceWatcherService resourceWatcherService; private Settings defaultGlobalSettings; private SSLService sslService; + private XPackLicenseState licenseState; @Before public void init() throws Exception { @@ -75,6 +83,8 @@ public void init() throws Exception { resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); defaultGlobalSettings = Settings.builder().put("path.home", createTempDir()).build(); sslService = new SSLService(defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings)); + licenseState = mock(XPackLicenseState.class); + when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); } @After @@ -87,10 +97,12 @@ public void testAuthenticateSubTreeGroupSearch() throws Exception { String groupSearchBase = "o=sevenSeas"; String userTemplate = VALID_USER_TEMPLATE; Settings settings = buildLdapSettings(ldapUrls(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE); - RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); @@ -111,11 +123,13 @@ public void testAuthenticateOneLevelGroupSearch() throws Exception { Settings settings = Settings.builder() .put(buildLdapSettings(ldapUrls(), userTemplate, groupSearchBase, LdapSearchScope.ONE_LEVEL)) .build(); - RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); @@ -136,12 +150,14 @@ public void testAuthenticateCaching() throws Exception { Settings settings = Settings.builder() .put(buildLdapSettings(ldapUrls(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE)) .build(); - RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); ldapFactory = spy(ldapFactory); LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); @@ -161,12 +177,15 @@ public void testAuthenticateCachingRefresh() throws Exception { Settings settings = Settings.builder() .put(buildLdapSettings(ldapUrls(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE)) .build(); - RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); DnRoleMapper roleMapper = buildGroupAsRoleMapper(resourceWatcherService); ldapFactory = spy(ldapFactory); LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, roleMapper, threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); future.actionGet(); @@ -194,12 +213,15 @@ public void testAuthenticateNoncaching() throws Exception { .put(buildLdapSettings(ldapUrls(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE)) .put(CachingUsernamePasswordRealmSettings.CACHE_TTL_SETTING.getKey(), -1) .build(); - RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); ldapFactory = spy(ldapFactory); LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); + PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); future.actionGet(); @@ -211,6 +233,48 @@ public void testAuthenticateNoncaching() throws Exception { verify(ldapFactory, times(2)).session(anyString(), any(SecureString.class), any(ActionListener.class)); } + public void testDelegatedAuthorization() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userTemplate = VALID_USER_TEMPLATE; + final Settings.Builder builder = Settings.builder() + .put(buildLdapSettings(ldapUrls(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE)) + .putList(DelegatedAuthorizationSettings.AUTHZ_REALMS.getKey(), "mock_lookup"); + + if (randomBoolean()) { + // maybe disable caching + builder.put(CachingUsernamePasswordRealmSettings.CACHE_TTL_SETTING.getKey(), -1); + } + + final Settings realmSettings = builder.build(); + final Environment env = TestEnvironment.newEnvironment(defaultGlobalSettings); + RealmConfig config = new RealmConfig("test-ldap-realm", realmSettings, defaultGlobalSettings, env, threadPool.getThreadContext()); + + final LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); + final DnRoleMapper roleMapper = buildGroupAsRoleMapper(resourceWatcherService); + final LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, roleMapper, threadPool); + + final MockLookupRealm mockLookup = new MockLookupRealm(new RealmConfig("mock_lookup", Settings.EMPTY, defaultGlobalSettings, env, + threadPool.getThreadContext())); + + ldap.initialize(Arrays.asList(ldap, mockLookup), licenseState); + mockLookup.initialize(Arrays.asList(ldap, mockLookup), licenseState); + + PlainActionFuture future = new PlainActionFuture<>(); + ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); + final AuthenticationResult result1 = future.actionGet(); + assertThat(result1.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); + assertThat(result1.getMessage(), + equalTo("the principal [" + VALID_USERNAME + "] was authenticated, but no user could be found in realms [mock/mock_lookup]")); + + future = new PlainActionFuture<>(); + final User fakeUser = new User(VALID_USERNAME, "fake_role"); + mockLookup.registerUser(fakeUser); + ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); + final AuthenticationResult result2 = future.actionGet(); + assertThat(result2.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + assertThat(result2.getUser(), sameInstance(fakeUser)); + } + public void testLdapRealmSelectsLdapSessionFactory() throws Exception { String groupSearchBase = "o=sevenSeas"; String userTemplate = VALID_USER_TEMPLATE; @@ -279,7 +343,8 @@ public void testLdapRealmThrowsExceptionForUserTemplateAndSearchSettings() throw .put("group_search.scope", LdapSearchScope.SUB_TREE) .put("ssl.verification_mode", VerificationMode.CERTIFICATE) .build(); - RealmConfig config = new RealmConfig("test-ldap-realm-user-search", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm-user-search", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> LdapRealm.sessionFactory(config, null, threadPool, LdapRealmSettings.LDAP_TYPE)); assertThat(e.getMessage(), @@ -295,7 +360,8 @@ public void testLdapRealmThrowsExceptionWhenNeitherUserTemplateNorSearchSettings .put("group_search.scope", LdapSearchScope.SUB_TREE) .put("ssl.verification_mode", VerificationMode.CERTIFICATE) .build(); - RealmConfig config = new RealmConfig("test-ldap-realm-user-search", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm-user-search", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> LdapRealm.sessionFactory(config, null, threadPool, LdapRealmSettings.LDAP_TYPE)); assertThat(e.getMessage(), @@ -312,11 +378,13 @@ public void testLdapRealmMapsUserDNToRole() throws Exception { .put(DnRoleMapperSettings.ROLE_MAPPING_FILE_SETTING.getKey(), getDataPath("/org/elasticsearch/xpack/security/authc/support/role_mapping.yml")) .build(); - RealmConfig config = new RealmConfig("test-ldap-realm-userdn", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm-userdn", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, new DnRoleMapper(config, resourceWatcherService), threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken("Horatio Hornblower", new SecureString(PASSWORD)), future); @@ -339,10 +407,12 @@ public void testLdapConnectionFailureIsTreatedAsAuthenticationFailure() throws E String groupSearchBase = "o=sevenSeas"; String userTemplate = VALID_USER_TEMPLATE; Settings settings = buildLdapSettings(new String[] { url.toString() }, userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE); - RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); + RealmConfig config = new RealmConfig("test-ldap-realm", settings, defaultGlobalSettings, + TestEnvironment.newEnvironment(defaultGlobalSettings), new ThreadContext(defaultGlobalSettings)); LdapSessionFactory ldapFactory = new LdapSessionFactory(config, sslService, threadPool); LdapRealm ldap = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), threadPool); + ldap.initialize(Collections.singleton(ldap), licenseState); PlainActionFuture future = new PlainActionFuture<>(); ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, new SecureString(PASSWORD)), future); @@ -386,6 +456,7 @@ public void testUsageStats() throws Exception { LdapSessionFactory ldapFactory = new LdapSessionFactory(config, new SSLService(globalSettings, env), threadPool); LdapRealm realm = new LdapRealm(LdapRealmSettings.LDAP_TYPE, config, ldapFactory, new DnRoleMapper(config, resourceWatcherService), threadPool); + realm.initialize(Collections.singleton(realm), licenseState); PlainActionFuture> future = new PlainActionFuture<>(); realm.usageStats(future); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index 44d5859d12b6..8d4c5d75c735 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -12,10 +12,13 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.pki.PkiRealmSettings; @@ -23,6 +26,7 @@ import org.elasticsearch.xpack.core.security.support.NoOpLogger; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; +import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; import org.junit.Before; import org.mockito.Mockito; @@ -43,9 +47,11 @@ import static org.hamcrest.Matchers.arrayContainingInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -56,12 +62,15 @@ public class PkiRealmTests extends ESTestCase { private Settings globalSettings; + private XPackLicenseState licenseState; @Before public void setup() throws Exception { globalSettings = Settings.builder() .put("path.home", createTempDir()) .build(); + licenseState = mock(XPackLicenseState.class); + when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); } public void testTokenSupport() { @@ -98,28 +107,14 @@ public void testAuthenticateWithRoleMapping() throws Exception { } private void assertSuccessfulAuthentication(Set roles) throws Exception { - String dn = "CN=Elasticsearch Test Node,"; - final String expectedUsername = "Elasticsearch Test Node"; - X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); - X509AuthenticationToken token = new X509AuthenticationToken(new X509Certificate[] { certificate }, "Elasticsearch Test Node", dn); - UserRoleMapper roleMapper = mock(UserRoleMapper.class); - PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.EMPTY, globalSettings, TestEnvironment.newEnvironment(globalSettings), - new ThreadContext(globalSettings)), roleMapper); + X509AuthenticationToken token = buildToken(); + UserRoleMapper roleMapper = buildRoleMapper(roles, token.dn()); + PkiRealm realm = buildRealm(roleMapper, Settings.EMPTY); verify(roleMapper).refreshRealmOnChange(realm); - Mockito.doAnswer(invocation -> { - final UserRoleMapper.UserData userData = (UserRoleMapper.UserData) invocation.getArguments()[0]; - final ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; - if (userData.getDn().equals(dn)) { - listener.onResponse(roles); - } else { - listener.onFailure(new IllegalArgumentException("Expected DN '" + dn + "' but was '" + userData + "'")); - } - return null; - }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); - PlainActionFuture future = new PlainActionFuture<>(); - realm.authenticate(token, future); - final AuthenticationResult result = future.actionGet(); + final String expectedUsername = token.principal(); + final AuthenticationResult result = authenticate(token, realm); + final PlainActionFuture future; assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); User user = result.getUser(); assertThat(user, is(notNullValue())); @@ -149,17 +144,54 @@ private void assertSuccessfulAuthentication(Set roles) throws Exception verifyNoMoreInteractions(roleMapper); } + private UserRoleMapper buildRoleMapper(Set roles, String dn) { + UserRoleMapper roleMapper = mock(UserRoleMapper.class); + Mockito.doAnswer(invocation -> { + final UserRoleMapper.UserData userData = (UserRoleMapper.UserData) invocation.getArguments()[0]; + final ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; + if (userData.getDn().equals(dn)) { + listener.onResponse(roles); + } else { + listener.onFailure(new IllegalArgumentException("Expected DN '" + dn + "' but was '" + userData + "'")); + } + return null; + }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); + return roleMapper; + } + + private PkiRealm buildRealm(UserRoleMapper roleMapper, Settings realmSettings, Realm... otherRealms) { + PkiRealm realm = new PkiRealm(new RealmConfig("", realmSettings, globalSettings, TestEnvironment.newEnvironment(globalSettings), + new ThreadContext(globalSettings)), roleMapper); + List allRealms = CollectionUtils.arrayAsArrayList(otherRealms); + allRealms.add(realm); + Collections.shuffle(allRealms, random()); + realm.initialize(allRealms, licenseState); + return realm; + } + + private X509AuthenticationToken buildToken() throws Exception { + X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); + return new X509AuthenticationToken(new X509Certificate[]{certificate}, "Elasticsearch Test Node", "CN=Elasticsearch Test Node,"); + } + + private AuthenticationResult authenticate(X509AuthenticationToken token, PkiRealm realm) { + PlainActionFuture future = new PlainActionFuture<>(); + realm.authenticate(token, future); + return future.actionGet(); + } + public void testCustomUsernamePattern() throws Exception { + ThreadContext threadContext = new ThreadContext(globalSettings); X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); UserRoleMapper roleMapper = mock(UserRoleMapper.class); - PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.builder().put("username_pattern", "OU=(.*?),").build(), globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)), - roleMapper); + PkiRealm realm = new PkiRealm(new RealmConfig("", Settings.builder().put("username_pattern", "OU=(.*?),").build(), globalSettings, + TestEnvironment.newEnvironment(globalSettings), threadContext), roleMapper); + realm.initialize(Collections.emptyList(), licenseState); Mockito.doAnswer(invocation -> { ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; listener.onResponse(Collections.emptySet()); return null; }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); - ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); @@ -182,15 +214,16 @@ public void testVerificationUsingATruststore() throws Exception { .put("truststore.path", getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.jks")) .setSecureSettings(secureSettings) .build(); + ThreadContext threadContext = new ThreadContext(globalSettings); PkiRealm realm = new PkiRealm(new RealmConfig("", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), - new ThreadContext(globalSettings)), roleMapper); + threadContext), roleMapper); + realm.initialize(Collections.emptyList(), licenseState); Mockito.doAnswer(invocation -> { ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; listener.onResponse(Collections.emptySet()); return null; }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); - ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); @@ -213,15 +246,16 @@ public void testVerificationFailsUsingADifferentTruststore() throws Exception { getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode-client-profile.jks")) .setSecureSettings(secureSettings) .build(); + final ThreadContext threadContext = new ThreadContext(globalSettings); PkiRealm realm = new PkiRealm(new RealmConfig("", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), - new ThreadContext(globalSettings)), roleMapper); + threadContext), roleMapper); + realm.initialize(Collections.emptyList(), licenseState); Mockito.doAnswer(invocation -> { ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; listener.onResponse(Collections.emptySet()); return null; }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); - ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate }); X509AuthenticationToken token = realm.token(threadContext); @@ -307,6 +341,33 @@ public void testPKIRealmSettingsPassValidation() throws Exception { assertSettingDeprecationsAndWarnings(new Setting[] { SSLConfigurationSettings.withoutPrefix().legacyTruststorePassword }); } + public void testDelegatedAuthorization() throws Exception { + final X509AuthenticationToken token = buildToken(); + + final MockLookupRealm otherRealm = new MockLookupRealm(new RealmConfig("other_realm", Settings.EMPTY, globalSettings, + TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings))); + final User lookupUser = new User(token.principal()); + otherRealm.registerUser(lookupUser); + + final Settings realmSettings = Settings.builder() + .putList("authorization_realms", "other_realm") + .build(); + final UserRoleMapper roleMapper = buildRoleMapper(Collections.emptySet(), token.dn()); + final PkiRealm pkiRealm = buildRealm(roleMapper, realmSettings, otherRealm); + + AuthenticationResult result = authenticate(token, pkiRealm); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), sameInstance(lookupUser)); + + // check that the authorizing realm is consulted even for cached principals + final User lookupUser2 = new User(token.principal()); + otherRealm.registerUser(lookupUser2); + + result = authenticate(token, pkiRealm); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), sameInstance(lookupUser2)); + } + static X509Certificate readCert(Path path) throws Exception { try (InputStream in = Files.newInputStream(path)) { CertificateFactory factory = CertificateFactory.getInstance("X.509"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java index 980abc46831c..2ecfdb50230b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlRealmTests.java @@ -14,18 +14,24 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.http.MockResponse; import org.elasticsearch.test.http.MockWebServer; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.Realm; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmSettings; import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; +import org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.core.ssl.PemUtils; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.TestsSSLService; +import org.elasticsearch.xpack.security.authc.support.MockLookupRealm; import org.elasticsearch.xpack.security.authc.support.UserRoleMapper; +import org.hamcrest.Matchers; import org.junit.Before; import org.mockito.Mockito; import org.opensaml.saml.common.xml.SAMLConstants; @@ -71,6 +77,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Basic unit tests for the SAMLRealm @@ -83,9 +90,16 @@ public class SamlRealmTests extends SamlTestCase { private static final String REALM_NAME = "my-saml"; private static final String REALM_SETTINGS_PREFIX = "xpack.security.authc.realms." + REALM_NAME; + private Settings globalSettings; + private Environment env; + private ThreadContext threadContext; + @Before - public void initRealm() throws PrivilegedActionException { + public void setupEnv() throws PrivilegedActionException { SamlUtils.initialize(logger); + globalSettings = Settings.builder().put("path.home", createTempDir()).build(); + env = TestEnvironment.newEnvironment(globalSettings); + threadContext = new ThreadContext(globalSettings); } public void testReadIdpMetadataFromFile() throws Exception { @@ -140,15 +154,70 @@ public void testReadIdpMetadataFromHttps() throws Exception { } public void testAuthenticateWithRoleMapping() throws Exception { + final UserRoleMapper roleMapper = mock(UserRoleMapper.class); + AtomicReference userData = new AtomicReference<>(); + Mockito.doAnswer(invocation -> { + assert invocation.getArguments().length == 2; + userData.set((UserRoleMapper.UserData) invocation.getArguments()[0]); + ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; + listener.onResponse(Collections.singleton("superuser")); + return null; + }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); + final boolean useNameId = randomBoolean(); final boolean principalIsEmailAddress = randomBoolean(); final Boolean populateUserMetadata = randomFrom(Boolean.TRUE, Boolean.FALSE, null); + + AuthenticationResult result = performAuthentication(roleMapper, useNameId, principalIsEmailAddress, populateUserMetadata, false); + assertThat(result.getUser().roles(), arrayContainingInAnyOrder("superuser")); + if (populateUserMetadata == Boolean.FALSE) { + // TODO : "saml_nameid" should be null too, but the logout code requires it for now. + assertThat(result.getUser().metadata().get("saml_uid"), nullValue()); + } else { + final String nameIdValue = principalIsEmailAddress ? "clint.barton@shield.gov" : "clint.barton"; + final String uidValue = principalIsEmailAddress ? "cbarton@shield.gov" : "cbarton"; + assertThat(result.getUser().metadata().get("saml_nameid"), equalTo(nameIdValue)); + assertThat(result.getUser().metadata().get("saml_uid"), instanceOf(Iterable.class)); + assertThat((Iterable) result.getUser().metadata().get("saml_uid"), contains(uidValue)); + } + + assertThat(userData.get().getUsername(), equalTo(useNameId ? "clint.barton" : "cbarton")); + assertThat(userData.get().getGroups(), containsInAnyOrder("avengers", "shield")); + } + + public void testAuthenticateWithAuthorizingRealm() throws Exception { final UserRoleMapper roleMapper = mock(UserRoleMapper.class); + Mockito.doAnswer(invocation -> { + assert invocation.getArguments().length == 2; + ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; + listener.onFailure(new RuntimeException("Role mapping should not be called")); + return null; + }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); + + final boolean useNameId = randomBoolean(); + final boolean principalIsEmailAddress = randomBoolean(); + + AuthenticationResult result = performAuthentication(roleMapper, useNameId, principalIsEmailAddress, null, true); + assertThat(result.getUser().roles(), arrayContainingInAnyOrder("lookup_user_role")); + assertThat(result.getUser().fullName(), equalTo("Clinton Barton")); + assertThat(result.getUser().metadata().entrySet(), Matchers.iterableWithSize(1)); + assertThat(result.getUser().metadata().get("is_lookup"), Matchers.equalTo(true)); + } + + private AuthenticationResult performAuthentication(UserRoleMapper roleMapper, boolean useNameId, boolean principalIsEmailAddress, + Boolean populateUserMetadata, boolean useAuthorizingRealm) throws Exception { final EntityDescriptor idp = mockIdp(); final SpConfiguration sp = new SpConfiguration("", "https://saml/", null, null, null, Collections.emptyList()); final SamlAuthenticator authenticator = mock(SamlAuthenticator.class); final SamlLogoutRequestHandler logoutHandler = mock(SamlLogoutRequestHandler.class); + final String userPrincipal = useNameId ? "clint.barton" : "cbarton"; + final String nameIdValue = principalIsEmailAddress ? "clint.barton@shield.gov" : "clint.barton"; + final String uidValue = principalIsEmailAddress ? "cbarton@shield.gov" : "cbarton"; + + final MockLookupRealm lookupRealm = new MockLookupRealm( + new RealmConfig("mock_lookup", Settings.EMPTY,globalSettings, env, threadContext)); + final Settings.Builder settingsBuilder = Settings.builder() .put(SamlRealmSettings.PRINCIPAL_ATTRIBUTE.name(), useNameId ? "nameid" : "uid") .put(SamlRealmSettings.GROUPS_ATTRIBUTE.name(), "groups") @@ -161,15 +230,20 @@ public void testAuthenticateWithRoleMapping() throws Exception { if (populateUserMetadata != null) { settingsBuilder.put(SamlRealmSettings.POPULATE_USER_METADATA.getKey(), populateUserMetadata.booleanValue()); } - final Settings realmSettings = settingsBuilder.build(); + if (useAuthorizingRealm) { + settingsBuilder.putList(DelegatedAuthorizationSettings.AUTHZ_REALMS.getKey(), lookupRealm.name()); + lookupRealm.registerUser(new User(userPrincipal, new String[]{ "lookup_user_role" }, "Clinton Barton", "cbarton@shield.gov", + Collections.singletonMap("is_lookup", true), true)); + } + final Settings realmSettings = settingsBuilder.build(); final RealmConfig config = realmConfigFromRealmSettings(realmSettings); - final SamlRealm realm = new SamlRealm(config, roleMapper, authenticator, logoutHandler, () -> idp, sp); + + initializeRealms(realm, lookupRealm); + final SamlToken token = new SamlToken(new byte[0], Collections.singletonList("")); - final String nameIdValue = principalIsEmailAddress ? "clint.barton@shield.gov" : "clint.barton"; - final String uidValue = principalIsEmailAddress ? "cbarton@shield.gov" : "cbarton"; final SamlAttributes attributes = new SamlAttributes( new SamlNameId(NameIDType.PERSISTENT, nameIdValue, idp.getEntityID(), sp.getEntityId(), null), randomAlphaOfLength(16), @@ -178,36 +252,27 @@ public void testAuthenticateWithRoleMapping() throws Exception { new SamlAttributes.SamlAttribute("urn:oid:1.3.6.1.4.1.5923.1.5.1.1", "groups", Arrays.asList("avengers", "shield")), new SamlAttributes.SamlAttribute("urn:oid:0.9.2342.19200300.100.1.3", "mail", Arrays.asList("cbarton@shield.gov")) )); - Mockito.when(authenticator.authenticate(token)).thenReturn(attributes); - - AtomicReference userData = new AtomicReference<>(); - Mockito.doAnswer(invocation -> { - assert invocation.getArguments().length == 2; - userData.set((UserRoleMapper.UserData) invocation.getArguments()[0]); - ActionListener> listener = (ActionListener>) invocation.getArguments()[1]; - listener.onResponse(Collections.singleton("superuser")); - return null; - }).when(roleMapper).resolveRoles(any(UserRoleMapper.UserData.class), any(ActionListener.class)); + when(authenticator.authenticate(token)).thenReturn(attributes); final PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(token, future); final AuthenticationResult result = future.get(); assertThat(result, notNullValue()); assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); - assertThat(result.getUser().principal(), equalTo(useNameId ? "clint.barton" : "cbarton")); + assertThat(result.getUser().principal(), equalTo(userPrincipal)); assertThat(result.getUser().email(), equalTo("cbarton@shield.gov")); - assertThat(result.getUser().roles(), arrayContainingInAnyOrder("superuser")); - if (populateUserMetadata == Boolean.FALSE) { - // TODO : "saml_nameid" should be null too, but the logout code requires it for now. - assertThat(result.getUser().metadata().get("saml_uid"), nullValue()); - } else { - assertThat(result.getUser().metadata().get("saml_nameid"), equalTo(nameIdValue)); - assertThat(result.getUser().metadata().get("saml_uid"), instanceOf(Iterable.class)); - assertThat((Iterable) result.getUser().metadata().get("saml_uid"), contains(uidValue)); - } - assertThat(userData.get().getUsername(), equalTo(useNameId ? "clint.barton" : "cbarton")); - assertThat(userData.get().getGroups(), containsInAnyOrder("avengers", "shield")); + return result; + } + + private void initializeRealms(Realm... realms) { + XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); + + final List realmList = Arrays.asList(realms); + for (Realm realm : realms) { + realm.initialize(realmList, licenseState); + } } public void testAttributeSelectionWithRegex() throws Exception { @@ -291,7 +356,7 @@ public void testNonMatchingPrincipalPatternThrowsSamlException() throws Exceptio Collections.singletonList( new SamlAttributes.SamlAttribute("urn:oid:0.9.2342.19200300.100.1.3", "mail", Collections.singletonList(mail)) )); - Mockito.when(authenticator.authenticate(token)).thenReturn(attributes); + when(authenticator.authenticate(token)).thenReturn(attributes); final PlainActionFuture future = new PlainActionFuture<>(); realm.authenticate(token, future); @@ -515,8 +580,8 @@ public void testBuildLogoutRequest() throws Exception { final EntityDescriptor idp = mockIdp(); final IDPSSODescriptor role = mock(IDPSSODescriptor.class); final SingleLogoutService slo = SamlUtils.buildObject(SingleLogoutService.class, SingleLogoutService.DEFAULT_ELEMENT_NAME); - Mockito.when(idp.getRoleDescriptors(IDPSSODescriptor.DEFAULT_ELEMENT_NAME)).thenReturn(Collections.singletonList(role)); - Mockito.when(role.getSingleLogoutServices()).thenReturn(Collections.singletonList(slo)); + when(idp.getRoleDescriptors(IDPSSODescriptor.DEFAULT_ELEMENT_NAME)).thenReturn(Collections.singletonList(role)); + when(role.getSingleLogoutServices()).thenReturn(Collections.singletonList(slo)); slo.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); slo.setLocation("https://logout.saml/"); @@ -553,7 +618,7 @@ public void testBuildLogoutRequest() throws Exception { private EntityDescriptor mockIdp() { final EntityDescriptor descriptor = mock(EntityDescriptor.class); - Mockito.when(descriptor.getEntityID()).thenReturn("https://idp.saml/"); + when(descriptor.getEntityID()).thenReturn("https://idp.saml/"); return descriptor; } @@ -585,9 +650,7 @@ private Settings.Builder buildSettings(String idpMetaDataPath) { } private RealmConfig realmConfigFromRealmSettings(Settings realmSettings) { - final Settings globalSettings = Settings.builder().put("path.home", createTempDir()).build(); - final Environment env = TestEnvironment.newEnvironment(globalSettings); - return new RealmConfig(REALM_NAME, realmSettings, globalSettings, env, new ThreadContext(globalSettings)); + return new RealmConfig(REALM_NAME, realmSettings, globalSettings, env, threadContext); } private RealmConfig realmConfigFromGlobalSettings(Settings globalSettings) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java index 052758d83718..e9e8908c584a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java @@ -31,6 +31,7 @@ import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.emptyMap; import static org.hamcrest.Matchers.arrayContaining; @@ -39,6 +40,7 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; @@ -341,6 +343,33 @@ public void testLookupContract() throws Exception { assertThat(e.getMessage(), containsString("lookup exception")); } + public void testReturnDifferentObjectFromCache() throws Exception { + final AtomicReference userArg = new AtomicReference<>(); + final AtomicReference result = new AtomicReference<>(); + Realm realm = new AlwaysAuthenticateCachingRealm(globalSettings, threadPool) { + @Override + protected void handleCachedAuthentication(User user, ActionListener listener) { + userArg.set(user); + listener.onResponse(result.get()); + } + }; + PlainActionFuture future = new PlainActionFuture<>(); + realm.authenticate(new UsernamePasswordToken("user", new SecureString("pass")), future); + final AuthenticationResult result1 = future.actionGet(); + assertThat(result1, notNullValue()); + assertThat(result1.getUser(), notNullValue()); + assertThat(result1.getUser().principal(), equalTo("user")); + + final AuthenticationResult result2 = AuthenticationResult.success(new User("user")); + result.set(result2); + + future = new PlainActionFuture<>(); + realm.authenticate(new UsernamePasswordToken("user", new SecureString("pass")), future); + final AuthenticationResult result3 = future.actionGet(); + assertThat(result3, sameInstance(result2)); + assertThat(userArg.get(), sameInstance(result1.getUser())); + } + public void testSingleAuthPerUserLimit() throws Exception { final String username = "username"; final SecureString password = SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupportTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupportTests.java new file mode 100644 index 000000000000..8f0d360b7597 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DelegatedAuthorizationSupportTests.java @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.security.authc.support; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.user.User; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.Strings.collectionToDelimitedString; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DelegatedAuthorizationSupportTests extends ESTestCase { + + private List realms; + private Settings globalSettings; + private ThreadContext threadContext; + private Environment env; + + @Before + public void setupRealms() { + globalSettings = Settings.builder() + .put("path.home", createTempDir()) + .build(); + env = TestEnvironment.newEnvironment(globalSettings); + threadContext = new ThreadContext(globalSettings); + + final int realmCount = randomIntBetween(5, 9); + realms = new ArrayList<>(realmCount); + for (int i = 1; i <= realmCount; i++) { + realms.add(new MockLookupRealm(buildRealmConfig("lookup-" + i, Settings.EMPTY))); + } + shuffle(realms); + } + + private List shuffle(List list) { + Collections.shuffle(list, random()); + return list; + } + + private RealmConfig buildRealmConfig(String name, Settings settings) { + return new RealmConfig(name, settings, globalSettings, env, threadContext); + } + + public void testEmptyDelegationList() throws ExecutionException, InterruptedException { + final XPackLicenseState license = getLicenseState(true); + final DelegatedAuthorizationSupport das = new DelegatedAuthorizationSupport(realms, buildRealmConfig("r", Settings.EMPTY), license); + assertThat(das.hasDelegation(), equalTo(false)); + final PlainActionFuture future = new PlainActionFuture<>(); + das.resolve("any", future); + final AuthenticationResult result = future.get(); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); + assertThat(result.getUser(), nullValue()); + assertThat(result.getMessage(), equalTo("No [authorization_realms] have been configured")); + } + + public void testMissingRealmInDelegationList() { + final XPackLicenseState license = getLicenseState(true); + final Settings settings = Settings.builder() + .putList("authorization_realms", "no-such-realm") + .build(); + final IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> + new DelegatedAuthorizationSupport(realms, buildRealmConfig("r", settings), license) + ); + assertThat(ex.getMessage(), equalTo("configured authorization realm [no-such-realm] does not exist (or is not enabled)")); + } + + public void testDelegationChainsAreRejected() { + final XPackLicenseState license = getLicenseState(true); + final Settings settings = Settings.builder() + .putList("authorization_realms", "lookup-1", "lookup-2", "lookup-3") + .build(); + globalSettings = Settings.builder() + .put(globalSettings) + .putList("xpack.security.authc.realms.lookup-2.authorization_realms", "lookup-1") + .build(); + final IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> + new DelegatedAuthorizationSupport(realms, buildRealmConfig("realm1", settings), license) + ); + assertThat(ex.getMessage(), + equalTo("cannot use realm [mock/lookup-2] as an authorization realm - it is already delegating authorization to [[lookup-1]]")); + } + + public void testMatchInDelegationList() throws Exception { + final XPackLicenseState license = getLicenseState(true); + final List useRealms = shuffle(randomSubsetOf(randomIntBetween(1, realms.size()), realms)); + final Settings settings = Settings.builder() + .putList("authorization_realms", useRealms.stream().map(Realm::name).collect(Collectors.toList())) + .build(); + final User user = new User("my_user"); + randomFrom(useRealms).registerUser(user); + final DelegatedAuthorizationSupport das = new DelegatedAuthorizationSupport(realms, buildRealmConfig("r", settings), license); + assertThat(das.hasDelegation(), equalTo(true)); + final PlainActionFuture future = new PlainActionFuture<>(); + das.resolve("my_user", future); + final AuthenticationResult result = future.get(); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), sameInstance(user)); + } + + public void testRealmsAreOrdered() throws Exception { + final XPackLicenseState license = getLicenseState(true); + final List useRealms = shuffle(randomSubsetOf(randomIntBetween(3, realms.size()), realms)); + final List names = useRealms.stream().map(Realm::name).collect(Collectors.toList()); + final Settings settings = Settings.builder() + .putList("authorization_realms", names) + .build(); + final List users = new ArrayList<>(names.size()); + final String username = randomAlphaOfLength(8); + for (MockLookupRealm r : useRealms) { + final User user = new User(username, "role_" + r.name()); + users.add(user); + r.registerUser(user); + } + + final DelegatedAuthorizationSupport das = new DelegatedAuthorizationSupport(realms, buildRealmConfig("r", settings), license); + assertThat(das.hasDelegation(), equalTo(true)); + final PlainActionFuture future = new PlainActionFuture<>(); + das.resolve(username, future); + final AuthenticationResult result = future.get(); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.SUCCESS)); + assertThat(result.getUser(), sameInstance(users.get(0))); + assertThat(result.getUser().roles(), arrayContaining("role_" + useRealms.get(0).name())); + } + + public void testNoMatchInDelegationList() throws Exception { + final XPackLicenseState license = getLicenseState(true); + final List useRealms = shuffle(randomSubsetOf(randomIntBetween(1, realms.size()), realms)); + final Settings settings = Settings.builder() + .putList("authorization_realms", useRealms.stream().map(Realm::name).collect(Collectors.toList())) + .build(); + final DelegatedAuthorizationSupport das = new DelegatedAuthorizationSupport(realms, buildRealmConfig("r", settings), license); + assertThat(das.hasDelegation(), equalTo(true)); + final PlainActionFuture future = new PlainActionFuture<>(); + das.resolve("my_user", future); + final AuthenticationResult result = future.get(); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); + assertThat(result.getUser(), nullValue()); + assertThat(result.getMessage(), equalTo("the principal [my_user] was authenticated, but no user could be found in realms [" + + collectionToDelimitedString(useRealms.stream().map(Realm::toString).collect(Collectors.toList()), ",") + "]")); + } + + public void testLicenseRejection() throws Exception { + final XPackLicenseState license = getLicenseState(false); + final Settings settings = Settings.builder() + .putList("authorization_realms", realms.get(0).name()) + .build(); + final DelegatedAuthorizationSupport das = new DelegatedAuthorizationSupport(realms, buildRealmConfig("r", settings), license); + assertThat(das.hasDelegation(), equalTo(true)); + final PlainActionFuture future = new PlainActionFuture<>(); + das.resolve("my_user", future); + final AuthenticationResult result = future.get(); + assertThat(result.getStatus(), equalTo(AuthenticationResult.Status.CONTINUE)); + assertThat(result.getUser(), nullValue()); + assertThat(result.getMessage(), equalTo("authorization_realms are not permitted")); + assertThat(result.getException(), instanceOf(ElasticsearchSecurityException.class)); + assertThat(result.getException().getMessage(), equalTo("current license is non-compliant for [authorization_realms]")); + } + + private XPackLicenseState getLicenseState(boolean authzRealmsAllowed) { + final XPackLicenseState license = mock(XPackLicenseState.class); + when(license.isAuthorizationRealmAllowed()).thenReturn(authzRealmsAllowed); + return license; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/MockLookupRealm.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/MockLookupRealm.java new file mode 100644 index 000000000000..01700347f509 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/MockLookupRealm.java @@ -0,0 +1,52 @@ +/* + * 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.security.authc.support; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.user.User; + +import java.util.HashMap; +import java.util.Map; + +public class MockLookupRealm extends Realm { + + private final Map lookup; + + public MockLookupRealm(RealmConfig config) { + super("mock", config); + lookup = new HashMap<>(); + } + + public void registerUser(User user) { + this.lookup.put(user.principal(), user); + } + + @Override + public boolean supports(AuthenticationToken token) { + return false; + } + + @Override + public AuthenticationToken token(ThreadContext context) { + return null; + } + + @Override + public void authenticate(AuthenticationToken token, ActionListener listener) { + listener.onResponse(AuthenticationResult.notHandled()); + } + + @Override + public void lookupUser(String username, ActionListener listener) { + listener.onResponse(lookup.get(username)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookupTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookupTests.java new file mode 100644 index 000000000000..78be4b3ddf4c --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/RealmUserLookupTests.java @@ -0,0 +1,128 @@ +/* + * 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.security.authc.support; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.Realm; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; +import org.elasticsearch.xpack.core.security.user.User; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; + +public class RealmUserLookupTests extends ESTestCase { + + private Settings globalSettings; + private ThreadContext threadContext; + private Environment env; + + @Before + public void setup() { + globalSettings = Settings.builder() + .put("path.home", createTempDir()) + .build(); + env = TestEnvironment.newEnvironment(globalSettings); + threadContext = new ThreadContext(globalSettings); + } + + public void testNoRealms() throws Exception { + final RealmUserLookup lookup = new RealmUserLookup(Collections.emptyList(), threadContext); + final PlainActionFuture> listener = new PlainActionFuture<>(); + lookup.lookup(randomAlphaOfLengthBetween(3, 12), listener); + final Tuple tuple = listener.get(); + assertThat(tuple, nullValue()); + } + + public void testUserFound() throws Exception { + final List realms = buildRealms(randomIntBetween(5, 9)); + final RealmUserLookup lookup = new RealmUserLookup(realms, threadContext); + + final MockLookupRealm matchRealm = randomFrom(realms); + final User user = new User(randomAlphaOfLength(5)); + matchRealm.registerUser(user); + + final PlainActionFuture> listener = new PlainActionFuture<>(); + lookup.lookup(user.principal(), listener); + final Tuple tuple = listener.get(); + assertThat(tuple, notNullValue()); + assertThat(tuple.v1(), notNullValue()); + assertThat(tuple.v1(), sameInstance(user)); + assertThat(tuple.v2(), notNullValue()); + assertThat(tuple.v2(), sameInstance(matchRealm)); + } + + public void testUserNotFound() throws Exception { + final List realms = buildRealms(randomIntBetween(5, 9)); + final RealmUserLookup lookup = new RealmUserLookup(realms, threadContext); + + final String username = randomAlphaOfLength(5); + + final PlainActionFuture> listener = new PlainActionFuture<>(); + lookup.lookup(username, listener); + final Tuple tuple = listener.get(); + assertThat(tuple, nullValue()); + } + + public void testRealmException() { + final Realm realm = new Realm("test", new RealmConfig("test", Settings.EMPTY, globalSettings, env, threadContext)) { + @Override + public boolean supports(AuthenticationToken token) { + return false; + } + + @Override + public AuthenticationToken token(ThreadContext context) { + return null; + } + + @Override + public void authenticate(AuthenticationToken token, ActionListener listener) { + listener.onResponse(AuthenticationResult.notHandled()); + } + + @Override + public void lookupUser(String username, ActionListener listener) { + listener.onFailure(new RuntimeException("FAILURE")); + } + }; + final RealmUserLookup lookup = new RealmUserLookup(Collections.singletonList(realm), threadContext); + final PlainActionFuture> listener = new PlainActionFuture<>(); + lookup.lookup("anyone", listener); + final RuntimeException e = expectThrows(RuntimeException.class, listener::actionGet); + assertThat(e.getMessage(), equalTo("FAILURE")); + } + + private List buildRealms(int realmCount) { + final List realms = new ArrayList<>(realmCount); + for (int i = 1; i <= realmCount; i++) { + final RealmConfig config = new RealmConfig("lookup-" + i, Settings.EMPTY, globalSettings, env, threadContext); + final MockLookupRealm realm = new MockLookupRealm(config); + for (int j = 0; j < 5; j++) { + realm.registerUser(new User(randomAlphaOfLengthBetween(6, 12))); + } + realms.add(realm); + } + Collections.shuffle(realms, random()); + return realms; + } +} diff --git a/x-pack/qa/saml-idp-tests/build.gradle b/x-pack/qa/saml-idp-tests/build.gradle index 9dd5d6d848f9..11e89d93c8e6 100644 --- a/x-pack/qa/saml-idp-tests/build.gradle +++ b/x-pack/qa/saml-idp-tests/build.gradle @@ -37,17 +37,30 @@ integTestCluster { setting 'xpack.security.authc.token.enabled', 'true' setting 'xpack.security.authc.realms.file.type', 'file' setting 'xpack.security.authc.realms.file.order', '0' + // SAML realm 1 (no authorization_realms) setting 'xpack.security.authc.realms.shibboleth.type', 'saml' setting 'xpack.security.authc.realms.shibboleth.order', '1' setting 'xpack.security.authc.realms.shibboleth.idp.entity_id', 'https://test.shibboleth.elastic.local/' setting 'xpack.security.authc.realms.shibboleth.idp.metadata.path', 'idp-metadata.xml' - setting 'xpack.security.authc.realms.shibboleth.sp.entity_id', 'http://mock.http.elastic.local/' + setting 'xpack.security.authc.realms.shibboleth.sp.entity_id', 'http://mock1.http.elastic.local/' // The port in the ACS URL is fake - the test will bind the mock webserver // to a random port and then whenever it needs to connect to a URL on the // mock webserver it will replace 54321 with the real port - setting 'xpack.security.authc.realms.shibboleth.sp.acs', 'http://localhost:54321/saml/acs' + setting 'xpack.security.authc.realms.shibboleth.sp.acs', 'http://localhost:54321/saml/acs1' setting 'xpack.security.authc.realms.shibboleth.attributes.principal', 'uid' setting 'xpack.security.authc.realms.shibboleth.attributes.name', 'urn:oid:2.5.4.3' + // SAML realm 2 (uses authorization_realms) + setting 'xpack.security.authc.realms.shibboleth_native.type', 'saml' + setting 'xpack.security.authc.realms.shibboleth_native.order', '2' + setting 'xpack.security.authc.realms.shibboleth_native.idp.entity_id', 'https://test.shibboleth.elastic.local/' + setting 'xpack.security.authc.realms.shibboleth_native.idp.metadata.path', 'idp-metadata.xml' + setting 'xpack.security.authc.realms.shibboleth_native.sp.entity_id', 'http://mock2.http.elastic.local/' + setting 'xpack.security.authc.realms.shibboleth_native.sp.acs', 'http://localhost:54321/saml/acs2' + setting 'xpack.security.authc.realms.shibboleth_native.attributes.principal', 'uid' + setting 'xpack.security.authc.realms.shibboleth_native.authorization_realms', 'native' + setting 'xpack.security.authc.realms.native.type', 'native' + setting 'xpack.security.authc.realms.native.order', '3' + setting 'xpack.ml.enabled', 'false' extraConfigFile 'idp-metadata.xml', idpFixtureProject.file("src/main/resources/provision/generated/idp-metadata.xml") diff --git a/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java b/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java index 031ee20ba0c7..b3fc7dd0c2fa 100644 --- a/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java +++ b/x-pack/qa/saml-idp-tests/src/test/java/org/elasticsearch/xpack/security/authc/saml/SamlAuthenticationIT.java @@ -42,6 +42,8 @@ import org.elasticsearch.client.Response; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.settings.SecureString; @@ -49,6 +51,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.mocksocket.MockHttpServer; import org.elasticsearch.test.rest.ESRestTestCase; @@ -65,7 +68,6 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509ExtendedTrustManager; - import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -102,13 +104,16 @@ public class SamlAuthenticationIT extends ESRestTestCase { private static final String SP_LOGIN_PATH = "/saml/login"; - private static final String SP_ACS_PATH = "/saml/acs"; + private static final String SP_ACS_PATH_1 = "/saml/acs1"; + private static final String SP_ACS_PATH_2 = "/saml/acs2"; private static final String SAML_RESPONSE_FIELD = "SAMLResponse"; private static final String REQUEST_ID_COOKIE = "saml-request-id"; private static final String KIBANA_PASSWORD = "K1b@na K1b@na K1b@na"; private static HttpServer httpServer; + private URI acs; + @BeforeClass public static void setupHttpServer() throws IOException { InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0); @@ -133,7 +138,8 @@ public static void shutdownHttpServer() { @Before public void setupHttpContext() { httpServer.createContext(SP_LOGIN_PATH, wrapFailures(this::httpLogin)); - httpServer.createContext(SP_ACS_PATH, wrapFailures(this::httpAcs)); + httpServer.createContext(SP_ACS_PATH_1, wrapFailures(this::httpAcs)); + httpServer.createContext(SP_ACS_PATH_2, wrapFailures(this::httpAcs)); } /** @@ -157,7 +163,8 @@ private HttpHandler wrapFailures(HttpHandler handler) { @After public void clearHttpContext() { httpServer.removeContext(SP_LOGIN_PATH); - httpServer.removeContext(SP_ACS_PATH); + httpServer.removeContext(SP_ACS_PATH_1); + httpServer.removeContext(SP_ACS_PATH_2); } @Override @@ -202,6 +209,21 @@ public void setupRoleMapping() throws IOException { adminClient().performRequest(request); } + /** + * Create a native user for "thor" that is used for user-lookup (authorizing realms) + */ + @Before + public void setupNativeUser() throws IOException { + final Map body = MapBuilder.newMapBuilder() + .put("roles", Collections.singletonList("kibana_dashboard_only_user")) + .put("full_name", "Thor Son of Odin") + .put("password", randomAlphaOfLengthBetween(8, 16)) + .put("metadata", Collections.singletonMap("is_native", true)) + .map(); + final Response response = adminClient().performRequest(buildRequest("PUT", "/_xpack/security/user/thor", body)); + assertOK(response); + } + /** * Tests that a user can login via a SAML idp: * It uses: @@ -218,7 +240,24 @@ public void setupRoleMapping() throws IOException { *

  • Uses that token to verify the user details
  • * */ - public void testLoginUser() throws Exception { + public void testLoginUserWithSamlRoleMapping() throws Exception { + // this ACS comes from the config in build.gradle + final Tuple authTokens = loginViaSaml("http://localhost:54321" + SP_ACS_PATH_1); + verifyElasticsearchAccessTokenForRoleMapping(authTokens.v1()); + final String accessToken = verifyElasticsearchRefreshToken(authTokens.v2()); + verifyElasticsearchAccessTokenForRoleMapping(accessToken); + } + + public void testLoginUserWithAuthorizingRealm() throws Exception { + // this ACS comes from the config in build.gradle + final Tuple authTokens = loginViaSaml("http://localhost:54321" + SP_ACS_PATH_2); + verifyElasticsearchAccessTokenForAuthorizingRealms(authTokens.v1()); + final String accessToken = verifyElasticsearchRefreshToken(authTokens.v2()); + verifyElasticsearchAccessTokenForAuthorizingRealms(accessToken); + } + + private Tuple loginViaSaml(String acs) throws Exception { + this.acs = new URI(acs); final BasicHttpContext context = new BasicHttpContext(); try (CloseableHttpClient client = getHttpClient()) { final URI loginUri = goToLoginPage(client, context); @@ -234,25 +273,21 @@ public void testLoginUser() throws Exception { final Object accessToken = result.get("access_token"); assertThat(accessToken, notNullValue()); assertThat(accessToken, instanceOf(String.class)); - verifyElasticsearchAccessToken((String) accessToken); final Object refreshToken = result.get("refresh_token"); assertThat(refreshToken, notNullValue()); assertThat(refreshToken, instanceOf(String.class)); - verifyElasticsearchRefreshToken((String) refreshToken); + + return new Tuple<>((String) accessToken, (String) refreshToken); } } /** * Verifies that the provided "Access Token" (see {@link org.elasticsearch.xpack.security.authc.TokenService}) - * is for the expected user with the expected name and roles. + * is for the expected user with the expected name and roles if the user was created from Role-Mapping */ - private void verifyElasticsearchAccessToken(String accessToken) throws IOException { - Request request = new Request("GET", "/_xpack/security/_authenticate"); - RequestOptions.Builder options = request.getOptions().toBuilder(); - options.addHeader("Authorization", "Bearer " + accessToken); - request.setOptions(options); - final Map map = entityAsMap(client().performRequest(request)); + private void verifyElasticsearchAccessTokenForRoleMapping(String accessToken) throws IOException { + final Map map = callAuthenticateApiUsingAccessToken(accessToken); assertThat(map.get("username"), equalTo("thor")); assertThat(map.get("full_name"), equalTo("Thor Odinson")); assertSingletonList(map.get("roles"), "kibana_user"); @@ -266,15 +301,37 @@ private void verifyElasticsearchAccessToken(String accessToken) throws IOExcepti } /** - * Verifies that the provided "Refresh Token" (see {@link org.elasticsearch.xpack.security.authc.TokenService}) - * can be used to get a new valid access token and refresh token. + * Verifies that the provided "Access Token" (see {@link org.elasticsearch.xpack.security.authc.TokenService}) + * is for the expected user with the expected name and roles if the user was retrieved from the native realm */ - private void verifyElasticsearchRefreshToken(String refreshToken) throws IOException { - Request request = new Request("POST", "/_xpack/security/oauth2/token"); - request.setJsonEntity("{ \"grant_type\":\"refresh_token\", \"refresh_token\":\"" + refreshToken + "\" }"); - kibanaAuth(request); + private void verifyElasticsearchAccessTokenForAuthorizingRealms(String accessToken) throws IOException { + final Map map = callAuthenticateApiUsingAccessToken(accessToken); + assertThat(map.get("username"), equalTo("thor")); + assertThat(map.get("full_name"), equalTo("Thor Son of Odin")); + assertSingletonList(map.get("roles"), "kibana_dashboard_only_user"); - final Map result = entityAsMap(client().performRequest(request)); + assertThat(map.get("metadata"), instanceOf(Map.class)); + final Map metadata = (Map) map.get("metadata"); + assertThat(metadata.get("is_native"), equalTo(true)); + } + + private Map callAuthenticateApiUsingAccessToken(String accessToken) throws IOException { + Request request = new Request("GET", "/_xpack/security/_authenticate"); + RequestOptions.Builder options = request.getOptions().toBuilder(); + options.addHeader("Authorization", "Bearer " + accessToken); + request.setOptions(options); + return entityAsMap(client().performRequest(request)); + } + + private String verifyElasticsearchRefreshToken(String refreshToken) throws IOException { + final Map body = MapBuilder.newMapBuilder() + .put("grant_type", "refresh_token") + .put("refresh_token", refreshToken) + .map(); + final Response response = client().performRequest(buildRequest("POST", "/_xpack/security/oauth2/token", body, kibanaAuth())); + assertOK(response); + + final Map result = entityAsMap(response); final Object newRefreshToken = result.get("refresh_token"); assertThat(newRefreshToken, notNullValue()); assertThat(newRefreshToken, instanceOf(String.class)); @@ -282,7 +339,7 @@ private void verifyElasticsearchRefreshToken(String refreshToken) throws IOExcep final Object accessToken = result.get("access_token"); assertThat(accessToken, notNullValue()); assertThat(accessToken, instanceOf(String.class)); - verifyElasticsearchAccessToken((String) accessToken); + return (String) accessToken; } /** @@ -348,7 +405,7 @@ private Tuple submitConsentForm(BasicHttpContext context, Closeable form.setEntity(new UrlEncodedFormEntity(params)); return execute(client, form, context, - response -> parseSamlSubmissionForm(response.getEntity().getContent())); + response -> parseSamlSubmissionForm(response.getEntity().getContent())); } /** @@ -358,14 +415,14 @@ private Tuple submitConsentForm(BasicHttpContext context, Closeable * @param saml The (deflated + base64 encoded) {@code SAMLResponse} parameter to post the ACS */ private Map submitSamlResponse(BasicHttpContext context, CloseableHttpClient client, URI acs, String saml) - throws IOException { + throws IOException { assertThat("SAML submission target", acs, notNullValue()); - assertThat(acs.getPath(), equalTo(SP_ACS_PATH)); + assertThat(acs, equalTo(this.acs)); assertThat("SAML submission content", saml, notNullValue()); // The ACS url provided from the SP is going to be wrong because the gradle // build doesn't know what the web server's port is, so it uses a fake one. - final HttpPost form = new HttpPost(getUrl(SP_ACS_PATH)); + final HttpPost form = new HttpPost(getUrl(this.acs.getPath())); List params = new ArrayList<>(); params.add(new BasicNameValuePair(SAML_RESPONSE_FIELD, saml)); form.setEntity(new UrlEncodedFormEntity(params)); @@ -460,13 +517,14 @@ private String getUrl(String path) { * sends a redirect to that page. */ private void httpLogin(HttpExchange http) throws IOException { - Request request = new Request("POST", "/_xpack/security/saml/prepare"); - request.setJsonEntity("{}"); - kibanaAuth(request); - final Map body = entityAsMap(client().performRequest(request)); - logger.info("Created SAML authentication request {}", body); - http.getResponseHeaders().add("Set-Cookie", REQUEST_ID_COOKIE + "=" + body.get("id")); - http.getResponseHeaders().add("Location", (String) body.get("redirect")); + final Map body = Collections.singletonMap("acs", this.acs.toString()); + Request request = buildRequest("POST", "/_xpack/security/saml/prepare", body, kibanaAuth()); + final Response prepare = client().performRequest(request); + assertOK(prepare); + final Map responseBody = parseResponseAsMap(prepare.getEntity()); + logger.info("Created SAML authentication request {}", responseBody); + http.getResponseHeaders().add("Set-Cookie", REQUEST_ID_COOKIE + "=" + responseBody.get("id")); + http.getResponseHeaders().add("Location", (String) responseBody.get("redirect")); http.sendResponseHeaders(302, 0); http.close(); } @@ -501,10 +559,11 @@ private Response samlAuthenticate(HttpExchange http) throws IOException { final String id = getCookie(REQUEST_ID_COOKIE, http); assertThat(id, notNullValue()); - Request request = new Request("POST", "/_xpack/security/saml/authenticate"); - request.setJsonEntity("{ \"content\" : \"" + saml + "\", \"ids\": [\"" + id + "\"] }"); - kibanaAuth(request); - return client().performRequest(request); + final Map body = MapBuilder.newMapBuilder() + .put("content", saml) + .put("ids", Collections.singletonList(id)) + .map(); + return client().performRequest(buildRequest("POST", "/_xpack/security/saml/authenticate", body, kibanaAuth())); } private List parseRequestForm(HttpExchange http) throws IOException { @@ -518,6 +577,7 @@ private String getCookie(String name, HttpExchange http) throws IOException { try { final String cookies = http.getRequestHeaders().getFirst("Cookie"); if (cookies == null) { + logger.warn("No cookies in: {}", http.getResponseHeaders()); return null; } Header header = new BasicHeader("Cookie", cookies); @@ -540,11 +600,23 @@ private static void assertSingletonList(Object value, String expectedElement) { assertThat(((List) value), contains(expectedElement)); } - private static void kibanaAuth(Request request) { - RequestOptions.Builder options = request.getOptions().toBuilder(); - options.addHeader("Authorization", - UsernamePasswordToken.basicAuthHeaderValue("kibana", new SecureString(KIBANA_PASSWORD.toCharArray()))); + private Request buildRequest(String method, String endpoint, Map body, Header... headers) throws IOException { + Request request = new Request(method, endpoint); + XContentBuilder builder = XContentFactory.jsonBuilder().map(body); + if (body != null) { + request.setJsonEntity(BytesReference.bytes(builder).utf8ToString()); + } + final RequestOptions.Builder options = request.getOptions().toBuilder(); + for (Header header : headers) { + options.addHeader(header.getName(), header.getValue()); + } request.setOptions(options); + return request; + } + + private static BasicHeader kibanaAuth() { + final String auth = UsernamePasswordToken.basicAuthHeaderValue("kibana", new SecureString(KIBANA_PASSWORD.toCharArray())); + return new BasicHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, auth); } private CloseableHttpClient getHttpClient() throws Exception { From 547de71d5991ecee65fcc2ae05cadf95fad717f9 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 30 Aug 2018 23:44:57 -0400 Subject: [PATCH 250/283] Revert "Integrates soft-deletes into Elasticsearch (#33222)" Revert to correct co-author tags. This reverts commit 6dd0aa54f6d0ad44219b3b416438b1be57cb37e5. --- .../percolator/CandidateQueryTests.java | 8 +- .../PercolatorFieldMapperTests.java | 30 +- .../elasticsearch/common/lucene/Lucene.java | 86 +-- .../uid/PerThreadIDVersionAndSeqNoLookup.java | 21 +- .../common/settings/IndexScopedSettings.java | 2 - .../elasticsearch/index/IndexSettings.java | 38 -- .../index/engine/CombinedDeletionPolicy.java | 12 +- .../elasticsearch/index/engine/Engine.java | 28 +- .../index/engine/EngineConfig.java | 27 +- .../index/engine/InternalEngine.java | 388 ++--------- .../index/engine/LuceneChangesSnapshot.java | 368 ----------- .../RecoverySourcePruneMergePolicy.java | 292 --------- .../index/engine/SoftDeletesPolicy.java | 120 ---- .../index/fieldvisitor/FieldsVisitor.java | 10 +- .../index/mapper/DocumentMapper.java | 34 +- .../index/mapper/DocumentParser.java | 33 +- .../index/mapper/FieldNamesFieldMapper.java | 5 +- .../index/mapper/ParseContext.java | 20 +- .../index/mapper/ParsedDocument.java | 11 - .../index/mapper/SeqNoFieldMapper.java | 7 +- .../index/mapper/SourceFieldMapper.java | 16 +- .../elasticsearch/index/shard/IndexShard.java | 47 +- .../index/shard/PrimaryReplicaSyncer.java | 2 +- .../index/shard/StoreRecovery.java | 1 - .../org/elasticsearch/index/store/Store.java | 2 +- .../index/translog/Translog.java | 3 - .../index/translog/TranslogWriter.java | 20 +- .../translog/TruncateTranslogCommand.java | 2 - .../recovery/RecoverySourceHandler.java | 59 +- .../blobstore/BlobStoreRepository.java | 1 - .../snapshots/RestoreService.java | 4 +- .../cluster/routing/PrimaryAllocationIT.java | 1 - .../common/lucene/LuceneTests.java | 91 --- .../discovery/AbstractDisruptionTestCase.java | 1 - .../gateway/RecoveryFromGatewayIT.java | 13 +- .../index/IndexServiceTests.java | 3 +- .../index/IndexSettingsTests.java | 8 - .../engine/CombinedDeletionPolicyTests.java | 69 +- .../index/engine/InternalEngineTests.java | 620 ++++++------------ .../engine/LuceneChangesSnapshotTests.java | 289 -------- .../RecoverySourcePruneMergePolicyTests.java | 161 ----- .../index/engine/SoftDeletesPolicyTests.java | 75 --- .../index/mapper/DocumentParserTests.java | 10 +- .../index/mapper/DynamicMappingTests.java | 6 +- .../IndexLevelReplicationTests.java | 29 +- .../RecoveryDuringReplicationTests.java | 11 +- .../index/shard/IndexShardTests.java | 58 +- .../shard/PrimaryReplicaSyncerTests.java | 21 +- .../index/shard/RefreshListenersTests.java | 4 +- .../indices/recovery/IndexRecoveryIT.java | 6 - .../PeerRecoveryTargetServiceTests.java | 2 - .../recovery/RecoverySourceHandlerTests.java | 6 + .../indices/recovery/RecoveryTests.java | 80 +-- .../indices/stats/IndexStatsIT.java | 37 +- .../AbstractSnapshotIntegTestCase.java | 6 - .../SharedClusterSnapshotRestoreIT.java | 13 +- .../versioning/SimpleVersioningIT.java | 23 - .../index/engine/EngineTestCase.java | 400 +---------- .../ESIndexLevelReplicationTestCase.java | 27 +- .../index/shard/IndexShardTestCase.java | 131 ++-- .../elasticsearch/test/ESIntegTestCase.java | 4 - .../test/ESSingleNodeTestCase.java | 9 - .../test/InternalTestCluster.java | 20 - 63 files changed, 499 insertions(+), 3432 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java delete mode 100644 server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java delete mode 100644 server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java delete mode 100644 server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java delete mode 100644 server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java delete mode 100644 server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java index 9c8979601e8d..e6d637aabb14 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java @@ -77,7 +77,6 @@ import org.apache.lucene.store.RAMDirectory; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -88,7 +87,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.IndexService; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; @@ -1111,11 +1109,7 @@ private void duelRun(PercolateQuery.QueryStore queryStore, MemoryIndex memoryInd } private void addQuery(Query query, List docs) { - IndexMetaData build = IndexMetaData.builder("") - .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1).numberOfReplicas(0).build(); - IndexSettings settings = new IndexSettings(build, Settings.EMPTY); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(query, parseContext); ParseContext.Document queryDocument = parseContext.doc(); diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java index 80524a2f862f..ecff48b344c5 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java @@ -42,7 +42,6 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -59,7 +58,6 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.DocumentMapperParser; import org.elasticsearch.index.mapper.MapperParsingException; @@ -184,11 +182,7 @@ public void testExtractTerms() throws Exception { DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - IndexMetaData build = IndexMetaData.builder("") - .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1).numberOfReplicas(0).build(); - IndexSettings settings = new IndexSettings(build, Settings.EMPTY); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); ParseContext.Document document = parseContext.doc(); @@ -210,7 +204,7 @@ public void testExtractTerms() throws Exception { bq.add(termQuery1, Occur.MUST); bq.add(termQuery2, Occur.MUST); - parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), + parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); document = parseContext.doc(); @@ -238,12 +232,8 @@ public void testExtractRanges() throws Exception { bq.add(rangeQuery2, Occur.MUST); DocumentMapper documentMapper = mapperService.documentMapper("doc"); - IndexMetaData build = IndexMetaData.builder("") - .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1).numberOfReplicas(0).build(); - IndexSettings settings = new IndexSettings(build, Settings.EMPTY); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); ParseContext.Document document = parseContext.doc(); @@ -269,7 +259,7 @@ public void testExtractRanges() throws Exception { .rangeQuery(15, 20, true, true, null, null, null, null); bq.add(rangeQuery2, Occur.MUST); - parseContext = new ParseContext.InternalParseContext(settings, + parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); document = parseContext.doc(); @@ -293,11 +283,7 @@ public void testExtractTermsAndRanges_failed() throws Exception { TermRangeQuery query = new TermRangeQuery("field1", new BytesRef("a"), new BytesRef("z"), true, true); DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - IndexMetaData build = IndexMetaData.builder("") - .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1).numberOfReplicas(0).build(); - IndexSettings settings = new IndexSettings(build, Settings.EMPTY); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(query, parseContext); ParseContext.Document document = parseContext.doc(); @@ -312,11 +298,7 @@ public void testExtractTermsAndRanges_partial() throws Exception { PhraseQuery phraseQuery = new PhraseQuery("field", "term"); DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - IndexMetaData build = IndexMetaData.builder("") - .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1).numberOfReplicas(0).build(); - IndexSettings settings = new IndexSettings(build, Settings.EMPTY); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(phraseQuery, parseContext); ParseContext.Document document = parseContext.doc(); diff --git a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java index 1c1e56878932..a24a6aea07fc 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java @@ -27,10 +27,8 @@ import org.apache.lucene.codecs.DocValuesFormat; import org.apache.lucene.codecs.PostingsFormat; import org.apache.lucene.document.LatLonDocValuesField; -import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.DirectoryReader; -import org.apache.lucene.index.FilterDirectoryReader; import org.apache.lucene.index.FilterLeafReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexFileNames; @@ -98,8 +96,6 @@ public class Lucene { assert annotation == null : "DocValuesFormat " + LATEST_DOC_VALUES_FORMAT + " is deprecated" ; } - public static final String SOFT_DELETES_FIELD = "__soft_deletes"; - public static final NamedAnalyzer STANDARD_ANALYZER = new NamedAnalyzer("_standard", AnalyzerScope.GLOBAL, new StandardAnalyzer()); public static final NamedAnalyzer KEYWORD_ANALYZER = new NamedAnalyzer("_keyword", AnalyzerScope.GLOBAL, new KeywordAnalyzer()); @@ -144,7 +140,7 @@ public static Iterable files(SegmentInfos infos) throws IOException { public static int getNumDocs(SegmentInfos info) { int numDocs = 0; for (SegmentCommitInfo si : info) { - numDocs += si.info.maxDoc() - si.getDelCount() - si.getSoftDelCount(); + numDocs += si.info.maxDoc() - si.getDelCount(); } return numDocs; } @@ -201,7 +197,6 @@ public static SegmentInfos pruneUnreferencedFiles(String segmentsFileName, Direc } final CommitPoint cp = new CommitPoint(si, directory); try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(Lucene.STANDARD_ANALYZER) - .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setIndexCommit(cp) .setCommitOnClose(false) .setMergePolicy(NoMergePolicy.INSTANCE) @@ -225,7 +220,6 @@ public static void cleanLuceneIndex(Directory directory) throws IOException { } } try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(Lucene.STANDARD_ANALYZER) - .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setMergePolicy(NoMergePolicy.INSTANCE) // no merges .setCommitOnClose(false) // no commits .setOpenMode(IndexWriterConfig.OpenMode.CREATE))) // force creation - don't append... @@ -835,82 +829,4 @@ public int length() { } }; } - - /** - * Wraps a directory reader to make all documents live except those were rolled back - * or hard-deleted due to non-aborting exceptions during indexing. - * The wrapped reader can be used to query all documents. - * - * @param in the input directory reader - * @return the wrapped reader - */ - public static DirectoryReader wrapAllDocsLive(DirectoryReader in) throws IOException { - return new DirectoryReaderWithAllLiveDocs(in); - } - - private static final class DirectoryReaderWithAllLiveDocs extends FilterDirectoryReader { - static final class LeafReaderWithLiveDocs extends FilterLeafReader { - final Bits liveDocs; - final int numDocs; - LeafReaderWithLiveDocs(LeafReader in, Bits liveDocs, int numDocs) { - super(in); - this.liveDocs = liveDocs; - this.numDocs = numDocs; - } - @Override - public Bits getLiveDocs() { - return liveDocs; - } - @Override - public int numDocs() { - return numDocs; - } - @Override - public CacheHelper getCoreCacheHelper() { - return in.getCoreCacheHelper(); - } - @Override - public CacheHelper getReaderCacheHelper() { - return null; // Modifying liveDocs - } - } - - DirectoryReaderWithAllLiveDocs(DirectoryReader in) throws IOException { - super(in, new SubReaderWrapper() { - @Override - public LeafReader wrap(LeafReader leaf) { - SegmentReader segmentReader = segmentReader(leaf); - Bits hardLiveDocs = segmentReader.getHardLiveDocs(); - if (hardLiveDocs == null) { - return new LeafReaderWithLiveDocs(leaf, null, leaf.maxDoc()); - } - // TODO: Can we avoid calculate numDocs by using SegmentReader#getSegmentInfo with LUCENE-8458? - int numDocs = 0; - for (int i = 0; i < hardLiveDocs.length(); i++) { - if (hardLiveDocs.get(i)) { - numDocs++; - } - } - return new LeafReaderWithLiveDocs(segmentReader, hardLiveDocs, numDocs); - } - }); - } - - @Override - protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException { - return wrapAllDocsLive(in); - } - - @Override - public CacheHelper getReaderCacheHelper() { - return null; // Modifying liveDocs - } - } - - /** - * Returns a numeric docvalues which can be used to soft-delete documents. - */ - public static NumericDocValuesField newSoftDeletesField() { - return new NumericDocValuesField(SOFT_DELETES_FIELD, 1); - } } diff --git a/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java b/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java index 3a037bed62b7..38fcdfe5f1b6 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java @@ -28,7 +28,6 @@ import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndSeqNo; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndVersion; import org.elasticsearch.index.mapper.SeqNoFieldMapper; @@ -67,22 +66,15 @@ final class PerThreadIDVersionAndSeqNoLookup { */ PerThreadIDVersionAndSeqNoLookup(LeafReader reader, String uidField) throws IOException { this.uidField = uidField; - final Terms terms = reader.terms(uidField); + Terms terms = reader.terms(uidField); if (terms == null) { - // If a segment contains only no-ops, it does not have _uid but has both _soft_deletes and _tombstone fields. - final NumericDocValues softDeletesDV = reader.getNumericDocValues(Lucene.SOFT_DELETES_FIELD); - final NumericDocValues tombstoneDV = reader.getNumericDocValues(SeqNoFieldMapper.TOMBSTONE_NAME); - if (softDeletesDV == null || tombstoneDV == null) { - throw new IllegalArgumentException("reader does not have _uid terms but not a no-op segment; " + - "_soft_deletes [" + softDeletesDV + "], _tombstone [" + tombstoneDV + "]"); - } - termsEnum = null; - } else { - termsEnum = terms.iterator(); + throw new IllegalArgumentException("reader misses the [" + uidField + "] field"); } + termsEnum = terms.iterator(); if (reader.getNumericDocValues(VersionFieldMapper.NAME) == null) { - throw new IllegalArgumentException("reader misses the [" + VersionFieldMapper.NAME + "] field; _uid terms [" + terms + "]"); + throw new IllegalArgumentException("reader misses the [" + VersionFieldMapper.NAME + "] field"); } + Object readerKey = null; assert (readerKey = reader.getCoreCacheHelper().getKey()) != null; this.readerKey = readerKey; @@ -119,8 +111,7 @@ public DocIdAndVersion lookupVersion(BytesRef id, LeafReaderContext context) * {@link DocIdSetIterator#NO_MORE_DOCS} is returned if not found * */ private int getDocID(BytesRef id, Bits liveDocs) throws IOException { - // termsEnum can possibly be null here if this leaf contains only no-ops. - if (termsEnum != null && termsEnum.seekExact(id)) { + if (termsEnum.seekExact(id)) { int docID = DocIdSetIterator.NO_MORE_DOCS; // there may be more than one matching docID, in the case of nested docs, so we want the last one: docsEnum = termsEnum.postings(docsEnum, 0); diff --git a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java index f3de294046c4..46e3867f7aea 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java @@ -129,8 +129,6 @@ public final class IndexScopedSettings extends AbstractScopedSettings { IndexSettings.MAX_REGEX_LENGTH_SETTING, ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING, IndexSettings.INDEX_GC_DELETES_SETTING, - IndexSettings.INDEX_SOFT_DELETES_SETTING, - IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING, IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED_SETTING, UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING, EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING, diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 3ea022bbebd4..44cd743bbd42 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -237,21 +237,6 @@ public final class IndexSettings { public static final Setting INDEX_GC_DELETES_SETTING = Setting.timeSetting("index.gc_deletes", DEFAULT_GC_DELETES, new TimeValue(-1, TimeUnit.MILLISECONDS), Property.Dynamic, Property.IndexScope); - - /** - * Specifies if the index should use soft-delete instead of hard-delete for update/delete operations. - */ - public static final Setting INDEX_SOFT_DELETES_SETTING = - Setting.boolSetting("index.soft_deletes.enabled", false, Property.IndexScope, Property.Final); - - /** - * Controls how many soft-deleted documents will be kept around before being merged away. Keeping more deleted - * documents increases the chance of operation-based recoveries and allows querying a longer history of documents. - * If soft-deletes is enabled, an engine by default will retain all operations up to the global checkpoint. - **/ - public static final Setting INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING = - Setting.longSetting("index.soft_deletes.retention.operations", 0, 0, Property.IndexScope, Property.Dynamic); - /** * The maximum number of refresh listeners allows on this shard. */ @@ -304,8 +289,6 @@ public final class IndexSettings { private final IndexSortConfig indexSortConfig; private final IndexScopedSettings scopedSettings; private long gcDeletesInMillis = DEFAULT_GC_DELETES.millis(); - private final boolean softDeleteEnabled; - private volatile long softDeleteRetentionOperations; private volatile boolean warmerEnabled; private volatile int maxResultWindow; private volatile int maxInnerResultWindow; @@ -417,8 +400,6 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti generationThresholdSize = scopedSettings.get(INDEX_TRANSLOG_GENERATION_THRESHOLD_SIZE_SETTING); mergeSchedulerConfig = new MergeSchedulerConfig(this); gcDeletesInMillis = scopedSettings.get(INDEX_GC_DELETES_SETTING).getMillis(); - softDeleteEnabled = version.onOrAfter(Version.V_7_0_0_alpha1) && scopedSettings.get(INDEX_SOFT_DELETES_SETTING); - softDeleteRetentionOperations = scopedSettings.get(INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING); warmerEnabled = scopedSettings.get(INDEX_WARMER_ENABLED_SETTING); maxResultWindow = scopedSettings.get(MAX_RESULT_WINDOW_SETTING); maxInnerResultWindow = scopedSettings.get(MAX_INNER_RESULT_WINDOW_SETTING); @@ -477,7 +458,6 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti scopedSettings.addSettingsUpdateConsumer(INDEX_SEARCH_IDLE_AFTER, this::setSearchIdleAfter); scopedSettings.addSettingsUpdateConsumer(MAX_REGEX_LENGTH_SETTING, this::setMaxRegexLength); scopedSettings.addSettingsUpdateConsumer(DEFAULT_PIPELINE, this::setDefaultPipeline); - scopedSettings.addSettingsUpdateConsumer(INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING, this::setSoftDeleteRetentionOperations); } private void setSearchIdleAfter(TimeValue searchIdleAfter) { this.searchIdleAfter = searchIdleAfter; } @@ -861,22 +841,4 @@ public String getDefaultPipeline() { public void setDefaultPipeline(String defaultPipeline) { this.defaultPipeline = defaultPipeline; } - - /** - * Returns true if soft-delete is enabled. - */ - public boolean isSoftDeleteEnabled() { - return softDeleteEnabled; - } - - private void setSoftDeleteRetentionOperations(long ops) { - this.softDeleteRetentionOperations = ops; - } - - /** - * Returns the number of extra operations (i.e. soft-deleted documents) to be kept for recoveries and history purpose. - */ - public long getSoftDeleteRetentionOperations() { - return this.softDeleteRetentionOperations; - } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java index d10690379edd..d0575c8a8c97 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java +++ b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java @@ -46,17 +46,14 @@ public final class CombinedDeletionPolicy extends IndexDeletionPolicy { private final Logger logger; private final TranslogDeletionPolicy translogDeletionPolicy; - private final SoftDeletesPolicy softDeletesPolicy; private final LongSupplier globalCheckpointSupplier; private final ObjectIntHashMap snapshottedCommits; // Number of snapshots held against each commit point. private volatile IndexCommit safeCommit; // the most recent safe commit point - its max_seqno at most the persisted global checkpoint. private volatile IndexCommit lastCommit; // the most recent commit point - CombinedDeletionPolicy(Logger logger, TranslogDeletionPolicy translogDeletionPolicy, - SoftDeletesPolicy softDeletesPolicy, LongSupplier globalCheckpointSupplier) { + CombinedDeletionPolicy(Logger logger, TranslogDeletionPolicy translogDeletionPolicy, LongSupplier globalCheckpointSupplier) { this.logger = logger; this.translogDeletionPolicy = translogDeletionPolicy; - this.softDeletesPolicy = softDeletesPolicy; this.globalCheckpointSupplier = globalCheckpointSupplier; this.snapshottedCommits = new ObjectIntHashMap<>(); } @@ -83,7 +80,7 @@ public synchronized void onCommit(List commits) throws IO deleteCommit(commits.get(i)); } } - updateRetentionPolicy(); + updateTranslogDeletionPolicy(); } private void deleteCommit(IndexCommit commit) throws IOException { @@ -93,7 +90,7 @@ private void deleteCommit(IndexCommit commit) throws IOException { assert commit.isDeleted() : "Deletion commit [" + commitDescription(commit) + "] was suppressed"; } - private void updateRetentionPolicy() throws IOException { + private void updateTranslogDeletionPolicy() throws IOException { assert Thread.holdsLock(this); logger.debug("Safe commit [{}], last commit [{}]", commitDescription(safeCommit), commitDescription(lastCommit)); assert safeCommit.isDeleted() == false : "The safe commit must not be deleted"; @@ -104,9 +101,6 @@ private void updateRetentionPolicy() throws IOException { assert minRequiredGen <= lastGen : "minRequiredGen must not be greater than lastGen"; translogDeletionPolicy.setTranslogGenerationOfLastCommit(lastGen); translogDeletionPolicy.setMinTranslogGenerationForRecovery(minRequiredGen); - - softDeletesPolicy.setLocalCheckpointOfSafeCommit( - Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY))); } /** 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 08724d6e7942..4d95cf89ef00 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -58,7 +58,6 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.index.VersionType; -import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.index.mapper.ParsedDocument; @@ -98,7 +97,6 @@ public abstract class Engine implements Closeable { public static final String SYNC_COMMIT_ID = "sync_id"; public static final String HISTORY_UUID_KEY = "history_uuid"; - public static final String MIN_RETAINED_SEQNO = "min_retained_seq_no"; protected final ShardId shardId; protected final String allocationId; @@ -587,32 +585,18 @@ public enum SearcherScope { public abstract void syncTranslog() throws IOException; - /** - * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed - */ - public abstract Closeable acquireRetentionLockForPeerRecovery(); - - /** - * Creates a new history snapshot from Lucene for reading operations whose seqno in the requesting seqno range (both inclusive) - */ - public abstract Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, - long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException; - - /** - * Creates a new history snapshot for reading operations since {@code startingSeqNo} (inclusive). - * The returned snapshot can be retrieved from either Lucene index or translog files. - */ - public abstract Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException; + public abstract Closeable acquireTranslogRetentionLock(); /** - * Returns the estimated number of history operations whose seq# at least {@code startingSeqNo}(inclusive) in this engine. + * Creates a new translog snapshot from this engine for reading translog operations whose seq# at least the provided seq#. + * The caller has to close the returned snapshot after finishing the reading. */ - public abstract int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException; + public abstract Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException; /** - * Checks if this engine has every operations since {@code startingSeqNo}(inclusive) in its history (either Lucene or translog) + * Returns the estimated number of translog operations in this engine whose seq# at least the provided seq#. */ - public abstract boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException; + public abstract int estimateTranslogOperationsFromMinSeq(long minSeqNo); public abstract TranslogStats getTranslogStats(); diff --git a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java index 23a90553f60a..2deae61bd52e 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java +++ b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java @@ -34,7 +34,6 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.codec.CodecService; -import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.store.Store; import org.elasticsearch.index.translog.Translog; @@ -81,7 +80,6 @@ public final class EngineConfig { private final CircuitBreakerService circuitBreakerService; private final LongSupplier globalCheckpointSupplier; private final LongSupplier primaryTermSupplier; - private final TombstoneDocSupplier tombstoneDocSupplier; /** * Index setting to change the low level lucene codec used for writing new segments. @@ -128,8 +126,7 @@ public EngineConfig(ShardId shardId, String allocationId, ThreadPool threadPool, List externalRefreshListener, List internalRefreshListener, Sort indexSort, TranslogRecoveryRunner translogRecoveryRunner, CircuitBreakerService circuitBreakerService, - LongSupplier globalCheckpointSupplier, LongSupplier primaryTermSupplier, - TombstoneDocSupplier tombstoneDocSupplier) { + LongSupplier globalCheckpointSupplier, LongSupplier primaryTermSupplier) { this.shardId = shardId; this.allocationId = allocationId; this.indexSettings = indexSettings; @@ -167,7 +164,6 @@ public EngineConfig(ShardId shardId, String allocationId, ThreadPool threadPool, this.circuitBreakerService = circuitBreakerService; this.globalCheckpointSupplier = globalCheckpointSupplier; this.primaryTermSupplier = primaryTermSupplier; - this.tombstoneDocSupplier = tombstoneDocSupplier; } /** @@ -377,25 +373,4 @@ public CircuitBreakerService getCircuitBreakerService() { public LongSupplier getPrimaryTermSupplier() { return primaryTermSupplier; } - - /** - * A supplier supplies tombstone documents which will be used in soft-update methods. - * The returned document consists only _uid, _seqno, _term and _version fields; other metadata fields are excluded. - */ - public interface TombstoneDocSupplier { - /** - * Creates a tombstone document for a delete operation. - */ - ParsedDocument newDeleteTombstoneDoc(String type, String id); - - /** - * Creates a tombstone document for a noop operation. - * @param reason the reason of an a noop - */ - ParsedDocument newNoopTombstoneDoc(String reason); - } - - public TombstoneDocSupplier getTombstoneDocSupplier() { - return tombstoneDocSupplier; - } } 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 da4decc93b1c..023e659ffabe 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -21,20 +21,16 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.lucene.document.Field; -import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; -import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LiveIndexWriterConfig; import org.apache.lucene.index.MergePolicy; import org.apache.lucene.index.SegmentCommitInfo; import org.apache.lucene.index.SegmentInfos; -import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ReferenceManager; @@ -46,7 +42,6 @@ import org.apache.lucene.store.LockObtainFailedException; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.InfoStream; -import org.elasticsearch.Assertions; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.common.Nullable; @@ -66,11 +61,7 @@ import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.mapper.IdFieldMapper; -import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.ParseContext; -import org.elasticsearch.index.mapper.ParsedDocument; -import org.elasticsearch.index.mapper.SeqNoFieldMapper; -import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.merge.MergeStats; import org.elasticsearch.index.merge.OnGoingMerge; import org.elasticsearch.index.seqno.LocalCheckpointTracker; @@ -149,10 +140,6 @@ public class InternalEngine extends Engine { private final CounterMetric numDocDeletes = new CounterMetric(); private final CounterMetric numDocAppends = new CounterMetric(); private final CounterMetric numDocUpdates = new CounterMetric(); - private final NumericDocValuesField softDeletesField = Lucene.newSoftDeletesField(); - private final boolean softDeleteEnabled; - private final SoftDeletesPolicy softDeletesPolicy; - private final LastRefreshedCheckpointListener lastRefreshedCheckpointListener; /** * How many bytes we are currently moving to disk, via either IndexWriter.flush or refresh. IndexingMemoryController polls this @@ -197,10 +184,8 @@ public InternalEngine(EngineConfig engineConfig) { assert translog.getGeneration() != null; this.translog = translog; this.localCheckpointTracker = createLocalCheckpointTracker(localCheckpointTrackerSupplier); - this.softDeleteEnabled = engineConfig.getIndexSettings().isSoftDeleteEnabled(); - this.softDeletesPolicy = newSoftDeletesPolicy(); this.combinedDeletionPolicy = - new CombinedDeletionPolicy(logger, translogDeletionPolicy, softDeletesPolicy, translog::getLastSyncedGlobalCheckpoint); + new CombinedDeletionPolicy(logger, translogDeletionPolicy, translog::getLastSyncedGlobalCheckpoint); writer = createWriter(); bootstrapAppendOnlyInfoFromWriter(writer); historyUUID = loadHistoryUUID(writer); @@ -230,8 +215,6 @@ public InternalEngine(EngineConfig engineConfig) { for (ReferenceManager.RefreshListener listener: engineConfig.getInternalRefreshListener()) { this.internalSearcherManager.addListener(listener); } - this.lastRefreshedCheckpointListener = new LastRefreshedCheckpointListener(localCheckpointTracker.getCheckpoint()); - this.internalSearcherManager.addListener(lastRefreshedCheckpointListener); success = true; } finally { if (success == false) { @@ -257,18 +240,6 @@ private LocalCheckpointTracker createLocalCheckpointTracker( return localCheckpointTrackerSupplier.apply(maxSeqNo, localCheckpoint); } - private SoftDeletesPolicy newSoftDeletesPolicy() throws IOException { - final Map commitUserData = store.readLastCommittedSegmentsInfo().userData; - final long lastMinRetainedSeqNo; - if (commitUserData.containsKey(Engine.MIN_RETAINED_SEQNO)) { - lastMinRetainedSeqNo = Long.parseLong(commitUserData.get(Engine.MIN_RETAINED_SEQNO)); - } else { - lastMinRetainedSeqNo = Long.parseLong(commitUserData.get(SequenceNumbers.MAX_SEQ_NO)) + 1; - } - return new SoftDeletesPolicy(translog::getLastSyncedGlobalCheckpoint, lastMinRetainedSeqNo, - engineConfig.getIndexSettings().getSoftDeleteRetentionOperations()); - } - /** * This reference manager delegates all it's refresh calls to another (internal) SearcherManager * The main purpose for this is that if we have external refreshes happening we don't issue extra @@ -480,31 +451,19 @@ public void syncTranslog() throws IOException { revisitIndexDeletionPolicyOnTranslogSynced(); } - /** - * Creates a new history snapshot for reading operations since the provided seqno. - * The returned snapshot can be retrieved from either Lucene index or translog files. - */ @Override - public Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - return newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false); - } else { - return getTranslog().newSnapshotFromMinSeqNo(startingSeqNo); - } + public Closeable acquireTranslogRetentionLock() { + return getTranslog().acquireRetentionLock(); } - /** - * Returns the estimated number of history operations whose seq# at least the provided seq# in this engine. - */ @Override - public int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - try (Translog.Snapshot snapshot = newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false)) { - return snapshot.totalOperations(); - } - } else { - return getTranslog().estimateTotalOperationsFromMinSeq(startingSeqNo); - } + public Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException { + return getTranslog().newSnapshotFromMinSeqNo(minSeqNo); + } + + @Override + public int estimateTranslogOperationsFromMinSeq(long minSeqNo) { + return getTranslog().estimateTotalOperationsFromMinSeq(minSeqNo); } @Override @@ -831,7 +790,7 @@ public IndexResult index(Index index) throws IOException { if (plan.earlyResultOnPreFlightError.isPresent()) { indexResult = plan.earlyResultOnPreFlightError.get(); assert indexResult.getResultType() == Result.Type.FAILURE : indexResult.getResultType(); - } else if (plan.indexIntoLucene || plan.addStaleOpToLucene) { + } else if (plan.indexIntoLucene) { indexResult = indexIntoLucene(index, plan); } else { indexResult = new IndexResult( @@ -842,10 +801,8 @@ public IndexResult index(Index index) throws IOException { if (indexResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Index(index, indexResult)); } else if (indexResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no - final NoOp noOp = new NoOp(indexResult.getSeqNo(), index.primaryTerm(), index.origin(), - index.startTime(), indexResult.getFailure().toString()); - location = innerNoOp(noOp).getTranslogLocation(); + // if we have document failure, record it as a no-op in the translog with the generated seq_no + location = translog.add(new Translog.NoOp(indexResult.getSeqNo(), index.primaryTerm(), indexResult.getFailure().toString())); } else { location = null; } @@ -897,6 +854,7 @@ private IndexingStrategy planIndexingAsNonPrimary(Index index) throws IOExceptio // unlike the primary, replicas don't really care to about creation status of documents // this allows to ignore the case where a document was found in the live version maps in // a delete state and return false for the created flag in favor of code simplicity + final OpVsLuceneDocStatus opVsLucene; if (index.seqNo() <= localCheckpointTracker.getCheckpoint()){ // the operation seq# is lower then the current local checkpoint and thus was already put into lucene // this can happen during recovery where older operations are sent from the translog that are already @@ -905,15 +863,16 @@ private IndexingStrategy planIndexingAsNonPrimary(Index index) throws IOExceptio // question may have been deleted in an out of order op that is not replayed. // See testRecoverFromStoreWithOutOfOrderDelete for an example of local recovery // See testRecoveryWithOutOfOrderDelete for an example of peer recovery + opVsLucene = OpVsLuceneDocStatus.OP_STALE_OR_EQUAL; + } else { + opVsLucene = compareOpToLuceneDocBasedOnSeqNo(index); + } + if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { plan = IndexingStrategy.processButSkipLucene(false, index.seqNo(), index.version()); } else { - final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(index); - if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { - plan = IndexingStrategy.processAsStaleOp(softDeleteEnabled, index.seqNo(), index.version()); - } else { - plan = IndexingStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, - index.seqNo(), index.version()); - } + plan = IndexingStrategy.processNormally( + opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, index.seqNo(), index.version() + ); } } return plan; @@ -962,7 +921,7 @@ private IndexResult indexIntoLucene(Index index, IndexingStrategy plan) throws IOException { assert plan.seqNoForIndexing >= 0 : "ops should have an assigned seq no.; origin: " + index.origin(); assert plan.versionForIndexing >= 0 : "version must be set. got " + plan.versionForIndexing; - assert plan.indexIntoLucene || plan.addStaleOpToLucene; + assert plan.indexIntoLucene; /* Update the document's sequence number and primary term; the sequence number here is derived here from either the sequence * number service if this is on the primary, or the existing document's sequence number if this is on the replica. The * primary term here has already been set, see IndexShard#prepareIndex where the Engine$Index operation is created. @@ -970,9 +929,7 @@ private IndexResult indexIntoLucene(Index index, IndexingStrategy plan) index.parsedDoc().updateSeqID(plan.seqNoForIndexing, index.primaryTerm()); index.parsedDoc().version().setLongValue(plan.versionForIndexing); try { - if (plan.addStaleOpToLucene) { - addStaleDocs(index.docs(), indexWriter); - } else if (plan.useLuceneUpdateDocument) { + if (plan.useLuceneUpdateDocument) { updateDocs(index.uid(), index.docs(), indexWriter); } else { // document does not exists, we can optimize for create, but double check if assertions are running @@ -1036,29 +993,16 @@ private void addDocs(final List docs, final IndexWriter i numDocAppends.inc(docs.size()); } - private void addStaleDocs(final List docs, final IndexWriter indexWriter) throws IOException { - assert softDeleteEnabled : "Add history documents but soft-deletes is disabled"; - for (ParseContext.Document doc : docs) { - doc.add(softDeletesField); // soft-deleted every document before adding to Lucene - } - if (docs.size() > 1) { - indexWriter.addDocuments(docs); - } else { - indexWriter.addDocument(docs.get(0)); - } - } - - protected static final class IndexingStrategy { + private static final class IndexingStrategy { final boolean currentNotFoundOrDeleted; final boolean useLuceneUpdateDocument; final long seqNoForIndexing; final long versionForIndexing; final boolean indexIntoLucene; - final boolean addStaleOpToLucene; final Optional earlyResultOnPreFlightError; private IndexingStrategy(boolean currentNotFoundOrDeleted, boolean useLuceneUpdateDocument, - boolean indexIntoLucene, boolean addStaleOpToLucene, long seqNoForIndexing, + boolean indexIntoLucene, long seqNoForIndexing, long versionForIndexing, IndexResult earlyResultOnPreFlightError) { assert useLuceneUpdateDocument == false || indexIntoLucene : "use lucene update is set to true, but we're not indexing into lucene"; @@ -1071,40 +1015,37 @@ private IndexingStrategy(boolean currentNotFoundOrDeleted, boolean useLuceneUpda this.seqNoForIndexing = seqNoForIndexing; this.versionForIndexing = versionForIndexing; this.indexIntoLucene = indexIntoLucene; - this.addStaleOpToLucene = addStaleOpToLucene; this.earlyResultOnPreFlightError = earlyResultOnPreFlightError == null ? Optional.empty() : Optional.of(earlyResultOnPreFlightError); } static IndexingStrategy optimizedAppendOnly(long seqNoForIndexing) { - return new IndexingStrategy(true, false, true, false, seqNoForIndexing, 1, null); + return new IndexingStrategy(true, false, true, seqNoForIndexing, 1, null); } static IndexingStrategy skipDueToVersionConflict( VersionConflictEngineException e, boolean currentNotFoundOrDeleted, long currentVersion, long term) { final IndexResult result = new IndexResult(e, currentVersion, term); return new IndexingStrategy( - currentNotFoundOrDeleted, false, false, false, SequenceNumbers.UNASSIGNED_SEQ_NO, Versions.NOT_FOUND, result); + currentNotFoundOrDeleted, false, false, SequenceNumbers.UNASSIGNED_SEQ_NO, Versions.NOT_FOUND, result); } static IndexingStrategy processNormally(boolean currentNotFoundOrDeleted, long seqNoForIndexing, long versionForIndexing) { return new IndexingStrategy(currentNotFoundOrDeleted, currentNotFoundOrDeleted == false, - true, false, seqNoForIndexing, versionForIndexing, null); + true, seqNoForIndexing, versionForIndexing, null); } static IndexingStrategy overrideExistingAsIfNotThere( long seqNoForIndexing, long versionForIndexing) { - return new IndexingStrategy(true, true, true, false, seqNoForIndexing, versionForIndexing, null); - } - - static IndexingStrategy processButSkipLucene(boolean currentNotFoundOrDeleted, long seqNoForIndexing, long versionForIndexing) { - return new IndexingStrategy(currentNotFoundOrDeleted, false, false, false, seqNoForIndexing, versionForIndexing, null); + return new IndexingStrategy(true, true, true, seqNoForIndexing, versionForIndexing, null); } - static IndexingStrategy processAsStaleOp(boolean addStaleOpToLucene, long seqNoForIndexing, long versionForIndexing) { - return new IndexingStrategy(false, false, false, addStaleOpToLucene, seqNoForIndexing, versionForIndexing, null); + static IndexingStrategy processButSkipLucene(boolean currentNotFoundOrDeleted, + long seqNoForIndexing, long versionForIndexing) { + return new IndexingStrategy(currentNotFoundOrDeleted, false, + false, seqNoForIndexing, versionForIndexing, null); } } @@ -1131,18 +1072,10 @@ private boolean assertDocDoesNotExist(final Index index, final boolean allowDele } private void updateDocs(final Term uid, final List docs, final IndexWriter indexWriter) throws IOException { - if (softDeleteEnabled) { - if (docs.size() > 1) { - indexWriter.softUpdateDocuments(uid, docs, softDeletesField); - } else { - indexWriter.softUpdateDocument(uid, docs.get(0), softDeletesField); - } + if (docs.size() > 1) { + indexWriter.updateDocuments(uid, docs); } else { - if (docs.size() > 1) { - indexWriter.updateDocuments(uid, docs); - } else { - indexWriter.updateDocument(uid, docs.get(0)); - } + indexWriter.updateDocument(uid, docs.get(0)); } numDocUpdates.inc(docs.size()); } @@ -1166,7 +1099,7 @@ public DeleteResult delete(Delete delete) throws IOException { if (plan.earlyResultOnPreflightError.isPresent()) { deleteResult = plan.earlyResultOnPreflightError.get(); - } else if (plan.deleteFromLucene || plan.addStaleOpToLucene) { + } else if (plan.deleteFromLucene) { deleteResult = deleteInLucene(delete, plan); } else { deleteResult = new DeleteResult( @@ -1177,10 +1110,8 @@ public DeleteResult delete(Delete delete) throws IOException { if (deleteResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Delete(delete, deleteResult)); } else if (deleteResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no - final NoOp noOp = new NoOp(deleteResult.getSeqNo(), delete.primaryTerm(), delete.origin(), - delete.startTime(), deleteResult.getFailure().toString()); - location = innerNoOp(noOp).getTranslogLocation(); + location = translog.add(new Translog.NoOp(deleteResult.getSeqNo(), + delete.primaryTerm(), deleteResult.getFailure().toString())); } else { location = null; } @@ -1211,7 +1142,7 @@ private DeletionStrategy planDeletionAsNonPrimary(Delete delete) throws IOExcept // unlike the primary, replicas don't really care to about found status of documents // this allows to ignore the case where a document was found in the live version maps in // a delete state and return true for the found flag in favor of code simplicity - final DeletionStrategy plan; + final OpVsLuceneDocStatus opVsLucene; if (delete.seqNo() <= localCheckpointTracker.getCheckpoint()) { // the operation seq# is lower then the current local checkpoint and thus was already put into lucene // this can happen during recovery where older operations are sent from the translog that are already @@ -1220,15 +1151,18 @@ private DeletionStrategy planDeletionAsNonPrimary(Delete delete) throws IOExcept // question may have been deleted in an out of order op that is not replayed. // See testRecoverFromStoreWithOutOfOrderDelete for an example of local recovery // See testRecoveryWithOutOfOrderDelete for an example of peer recovery + opVsLucene = OpVsLuceneDocStatus.OP_STALE_OR_EQUAL; + } else { + opVsLucene = compareOpToLuceneDocBasedOnSeqNo(delete); + } + + final DeletionStrategy plan; + if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { plan = DeletionStrategy.processButSkipLucene(false, delete.seqNo(), delete.version()); } else { - final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(delete); - if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { - plan = DeletionStrategy.processAsStaleOp(softDeleteEnabled, false, delete.seqNo(), delete.version()); - } else { - plan = DeletionStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, - delete.seqNo(), delete.version()); - } + plan = DeletionStrategy.processNormally( + opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, + delete.seqNo(), delete.version()); } return plan; } @@ -1263,31 +1197,15 @@ private DeletionStrategy planDeletionAsPrimary(Delete delete) throws IOException private DeleteResult deleteInLucene(Delete delete, DeletionStrategy plan) throws IOException { try { - if (softDeleteEnabled) { - final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newDeleteTombstoneDoc(delete.type(), delete.id()); - assert tombstone.docs().size() == 1 : "Tombstone doc should have single doc [" + tombstone + "]"; - tombstone.updateSeqID(plan.seqNoOfDeletion, delete.primaryTerm()); - tombstone.version().setLongValue(plan.versionOfDeletion); - final ParseContext.Document doc = tombstone.docs().get(0); - assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null : - "Delete tombstone document but _tombstone field is not set [" + doc + " ]"; - doc.add(softDeletesField); - if (plan.addStaleOpToLucene || plan.currentlyDeleted) { - indexWriter.addDocument(doc); - } else { - indexWriter.softUpdateDocument(delete.uid(), doc, softDeletesField); - } - } else if (plan.currentlyDeleted == false) { + if (plan.currentlyDeleted == false) { // any exception that comes from this is a either an ACE or a fatal exception there // can't be any document failures coming from this indexWriter.deleteDocuments(delete.uid()); - } - if (plan.deleteFromLucene) { numDocDeletes.inc(); - versionMap.putDeleteUnderLock(delete.uid().bytes(), - new DeleteVersionValue(plan.versionOfDeletion, plan.seqNoOfDeletion, delete.primaryTerm(), - engineConfig.getThreadPool().relativeTimeInMillis())); } + versionMap.putDeleteUnderLock(delete.uid().bytes(), + new DeleteVersionValue(plan.versionOfDeletion, plan.seqNoOfDeletion, delete.primaryTerm(), + engineConfig.getThreadPool().relativeTimeInMillis())); return new DeleteResult( plan.versionOfDeletion, getPrimaryTerm(), plan.seqNoOfDeletion, plan.currentlyDeleted == false); } catch (Exception ex) { @@ -1301,16 +1219,15 @@ private DeleteResult deleteInLucene(Delete delete, DeletionStrategy plan) } } - protected static final class DeletionStrategy { + private static final class DeletionStrategy { // of a rare double delete final boolean deleteFromLucene; - final boolean addStaleOpToLucene; final boolean currentlyDeleted; final long seqNoOfDeletion; final long versionOfDeletion; final Optional earlyResultOnPreflightError; - private DeletionStrategy(boolean deleteFromLucene, boolean addStaleOpToLucene, boolean currentlyDeleted, + private DeletionStrategy(boolean deleteFromLucene, boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion, DeleteResult earlyResultOnPreflightError) { assert (deleteFromLucene && earlyResultOnPreflightError != null) == false : @@ -1318,7 +1235,6 @@ private DeletionStrategy(boolean deleteFromLucene, boolean addStaleOpToLucene, b "deleteFromLucene: " + deleteFromLucene + " earlyResultOnPreFlightError:" + earlyResultOnPreflightError; this.deleteFromLucene = deleteFromLucene; - this.addStaleOpToLucene = addStaleOpToLucene; this.currentlyDeleted = currentlyDeleted; this.seqNoOfDeletion = seqNoOfDeletion; this.versionOfDeletion = versionOfDeletion; @@ -1330,22 +1246,16 @@ static DeletionStrategy skipDueToVersionConflict( VersionConflictEngineException e, long currentVersion, long term, boolean currentlyDeleted) { final long unassignedSeqNo = SequenceNumbers.UNASSIGNED_SEQ_NO; final DeleteResult deleteResult = new DeleteResult(e, currentVersion, term, unassignedSeqNo, currentlyDeleted == false); - return new DeletionStrategy(false, false, currentlyDeleted, unassignedSeqNo, Versions.NOT_FOUND, deleteResult); + return new DeletionStrategy(false, currentlyDeleted, unassignedSeqNo, Versions.NOT_FOUND, deleteResult); } static DeletionStrategy processNormally(boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion) { - return new DeletionStrategy(true, false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); - - } + return new DeletionStrategy(true, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); - public static DeletionStrategy processButSkipLucene(boolean currentlyDeleted, - long seqNoOfDeletion, long versionOfDeletion) { - return new DeletionStrategy(false, false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); } - static DeletionStrategy processAsStaleOp(boolean addStaleOpToLucene, boolean currentlyDeleted, - long seqNoOfDeletion, long versionOfDeletion) { - return new DeletionStrategy(false, addStaleOpToLucene, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); + public static DeletionStrategy processButSkipLucene(boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion) { + return new DeletionStrategy(false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); } } @@ -1374,28 +1284,7 @@ private NoOpResult innerNoOp(final NoOp noOp) throws IOException { assert noOp.seqNo() > SequenceNumbers.NO_OPS_PERFORMED; final long seqNo = noOp.seqNo(); try { - Exception failure = null; - if (softDeleteEnabled) { - try { - final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newNoopTombstoneDoc(noOp.reason()); - tombstone.updateSeqID(noOp.seqNo(), noOp.primaryTerm()); - // A noop tombstone does not require a _version but it's added to have a fully dense docvalues for the version field. - // 1L is selected to optimize the compression because it might probably be the most common value in version field. - tombstone.version().setLongValue(1L); - assert tombstone.docs().size() == 1 : "Tombstone should have a single doc [" + tombstone + "]"; - final ParseContext.Document doc = tombstone.docs().get(0); - assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null - : "Noop tombstone document but _tombstone field is not set [" + doc + " ]"; - doc.add(softDeletesField); - indexWriter.addDocument(doc); - } catch (Exception ex) { - if (maybeFailEngine("noop", ex)) { - throw ex; - } - failure = ex; - } - } - final NoOpResult noOpResult = failure != null ? new NoOpResult(getPrimaryTerm(), noOp.seqNo(), failure) : new NoOpResult(getPrimaryTerm(), noOp.seqNo()); + final NoOpResult noOpResult = new NoOpResult(getPrimaryTerm(), noOp.seqNo()); if (noOp.origin() != Operation.Origin.LOCAL_TRANSLOG_RECOVERY) { final Translog.Location location = translog.add(new Translog.NoOp(noOp.seqNo(), noOp.primaryTerm(), noOp.reason())); noOpResult.setTranslogLocation(location); @@ -1420,7 +1309,6 @@ final void refresh(String source, SearcherScope scope) throws EngineException { // since it flushes the index as well (though, in terms of concurrency, we are allowed to do it) // both refresh types will result in an internal refresh but only the external will also // 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(); @@ -1446,7 +1334,6 @@ final void refresh(String source, SearcherScope scope) throws EngineException { } finally { store.decRef(); } - lastRefreshedCheckpointListener.updateRefreshedCheckpoint(localCheckpointBeforeRefresh); } } catch (AlreadyClosedException e) { failOnTragicEvent(e); @@ -1461,8 +1348,7 @@ final void refresh(String source, SearcherScope scope) throws EngineException { } finally { writingBytes.addAndGet(-bytes); } - assert lastRefreshedCheckpoint() >= localCheckpointBeforeRefresh : "refresh checkpoint was not advanced; " + - "local_checkpoint=" + localCheckpointBeforeRefresh + " refresh_checkpoint=" + lastRefreshedCheckpoint(); + // TODO: maybe we should just put a scheduled job in threadPool? // We check for pruning in each delete request, but we also prune here e.g. in case a delete burst comes in and then no more deletes // for a long time: @@ -2044,11 +1930,7 @@ private IndexWriter createWriter() throws IOException { // pkg-private for testing IndexWriter createWriter(Directory directory, IndexWriterConfig iwc) throws IOException { - if (Assertions.ENABLED) { - return new AssertingIndexWriter(directory, iwc); - } else { - return new IndexWriter(directory, iwc); - } + return new IndexWriter(directory, iwc); } private IndexWriterConfig getIndexWriterConfig() { @@ -2064,15 +1946,11 @@ private IndexWriterConfig getIndexWriterConfig() { } iwc.setInfoStream(verbose ? InfoStream.getDefault() : new LoggerInfoStream(logger)); iwc.setMergeScheduler(mergeScheduler); + MergePolicy mergePolicy = config().getMergePolicy(); // Give us the opportunity to upgrade old segments while performing // background merges - MergePolicy mergePolicy = config().getMergePolicy(); - if (softDeleteEnabled) { - iwc.setSoftDeletesField(Lucene.SOFT_DELETES_FIELD); - mergePolicy = new RecoverySourcePruneMergePolicy(SourceFieldMapper.RECOVERY_SOURCE_NAME, softDeletesPolicy::getRetentionQuery, - new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, softDeletesPolicy::getRetentionQuery, mergePolicy)); - } - iwc.setMergePolicy(new ElasticsearchMergePolicy(mergePolicy)); + mergePolicy = new ElasticsearchMergePolicy(mergePolicy); + iwc.setMergePolicy(mergePolicy); iwc.setSimilarity(engineConfig.getSimilarity()); iwc.setRAMBufferSizeMB(engineConfig.getIndexingBufferSize().getMbFrac()); iwc.setCodec(engineConfig.getCodec()); @@ -2269,9 +2147,6 @@ protected void commitIndexWriter(final IndexWriter writer, final Translog transl commitData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(localCheckpointTracker.getMaxSeqNo())); commitData.put(MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID, Long.toString(maxUnsafeAutoIdTimestamp.get())); commitData.put(HISTORY_UUID_KEY, historyUUID); - if (softDeleteEnabled) { - commitData.put(Engine.MIN_RETAINED_SEQNO, Long.toString(softDeletesPolicy.getMinRetainedSeqNo())); - } logger.trace("committing writer with commit data [{}]", commitData); return commitData.entrySet().iterator(); }); @@ -2327,7 +2202,6 @@ public void onSettingsChanged() { final IndexSettings indexSettings = engineConfig.getIndexSettings(); translogDeletionPolicy.setRetentionAgeInMillis(indexSettings.getTranslogRetentionAge().getMillis()); translogDeletionPolicy.setRetentionSizeInBytes(indexSettings.getTranslogRetentionSize().getBytes()); - softDeletesPolicy.setRetentionOperations(indexSettings.getSoftDeleteRetentionOperations()); } public MergeStats getMergeStats() { @@ -2422,69 +2296,6 @@ long getNumDocUpdates() { return numDocUpdates.count(); } - @Override - public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, - long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { - // TODO: Should we defer the refresh until we really need it? - ensureOpen(); - if (lastRefreshedCheckpoint() < toSeqNo) { - refresh(source, SearcherScope.INTERNAL); - } - Searcher searcher = acquireSearcher(source, SearcherScope.INTERNAL); - try { - LuceneChangesSnapshot snapshot = new LuceneChangesSnapshot( - searcher, mapperService, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE, fromSeqNo, toSeqNo, requiredFullRange); - searcher = null; - return snapshot; - } catch (Exception e) { - try { - maybeFailEngine("acquire changes snapshot", e); - } catch (Exception inner) { - e.addSuppressed(inner); - } - throw e; - } finally { - IOUtils.close(searcher); - } - } - - @Override - public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException { - if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - return getMinRetainedSeqNo() <= startingSeqNo; - } else { - final long currentLocalCheckpoint = getLocalCheckpointTracker().getCheckpoint(); - final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); - try (Translog.Snapshot snapshot = getTranslog().newSnapshotFromMinSeqNo(startingSeqNo)) { - Translog.Operation operation; - while ((operation = snapshot.next()) != null) { - if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - tracker.markSeqNoAsCompleted(operation.seqNo()); - } - } - } - return tracker.getCheckpoint() >= currentLocalCheckpoint; - } - } - - /** - * Returns the minimum seqno that is retained in the Lucene index. - * Operations whose seq# are at least this value should exist in the Lucene index. - */ - final long getMinRetainedSeqNo() { - assert softDeleteEnabled : Thread.currentThread().getName(); - return softDeletesPolicy.getMinRetainedSeqNo(); - } - - @Override - public Closeable acquireRetentionLockForPeerRecovery() { - if (softDeleteEnabled) { - return softDeletesPolicy.acquireRetentionLock(); - } else { - return translog.acquireRetentionLock(); - } - } - @Override public boolean isRecovering() { return pendingTranslogRecovery.get(); @@ -2500,69 +2311,4 @@ private static Map commitDataAsMap(final IndexWriter indexWriter } return commitData; } - - private final class AssertingIndexWriter extends IndexWriter { - AssertingIndexWriter(Directory d, IndexWriterConfig conf) throws IOException { - super(d, conf); - } - @Override - public long updateDocument(Term term, Iterable doc) throws IOException { - assert softDeleteEnabled == false : "Call #updateDocument but soft-deletes is enabled"; - return super.updateDocument(term, doc); - } - @Override - public long updateDocuments(Term delTerm, Iterable> docs) throws IOException { - assert softDeleteEnabled == false : "Call #updateDocuments but soft-deletes is enabled"; - return super.updateDocuments(delTerm, docs); - } - @Override - public long deleteDocuments(Term... terms) throws IOException { - assert softDeleteEnabled == false : "Call #deleteDocuments but soft-deletes is enabled"; - return super.deleteDocuments(terms); - } - @Override - public long softUpdateDocument(Term term, Iterable doc, Field... softDeletes) throws IOException { - assert softDeleteEnabled : "Call #softUpdateDocument but soft-deletes is disabled"; - return super.softUpdateDocument(term, doc, softDeletes); - } - @Override - public long softUpdateDocuments(Term term, Iterable> docs, Field... softDeletes) throws IOException { - assert softDeleteEnabled : "Call #softUpdateDocuments but soft-deletes is disabled"; - return super.softUpdateDocuments(term, docs, softDeletes); - } - } - - /** - * Returned the last local checkpoint value has been refreshed internally. - */ - final long lastRefreshedCheckpoint() { - return lastRefreshedCheckpointListener.refreshedCheckpoint.get(); - } - - private final class LastRefreshedCheckpointListener implements ReferenceManager.RefreshListener { - final AtomicLong refreshedCheckpoint; - private long pendingCheckpoint; - - LastRefreshedCheckpointListener(long initialLocalCheckpoint) { - this.refreshedCheckpoint = new AtomicLong(initialLocalCheckpoint); - } - - @Override - public void beforeRefresh() { - // all changes until this point should be visible after refresh - pendingCheckpoint = localCheckpointTracker.getCheckpoint(); - } - - @Override - public void afterRefresh(boolean didRefresh) { - if (didRefresh) { - updateRefreshedCheckpoint(pendingCheckpoint); - } - } - - void updateRefreshedCheckpoint(long checkpoint) { - refreshedCheckpoint.updateAndGet(curr -> Math.max(curr, checkpoint)); - assert refreshedCheckpoint.get() >= checkpoint : refreshedCheckpoint.get() + " < " + checkpoint; - } - } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java deleted file mode 100644 index deebfba9ed42..000000000000 --- a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java +++ /dev/null @@ -1,368 +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.index.engine; - -import org.apache.lucene.document.LongPoint; -import org.apache.lucene.index.LeafReader; -import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.index.NumericDocValues; -import org.apache.lucene.index.Term; -import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.Query; -import org.apache.lucene.search.ScoreDoc; -import org.apache.lucene.search.Sort; -import org.apache.lucene.search.SortField; -import org.apache.lucene.search.TopDocs; -import org.apache.lucene.util.ArrayUtil; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.lucene.Lucene; -import org.elasticsearch.core.internal.io.IOUtils; -import org.elasticsearch.index.fieldvisitor.FieldsVisitor; -import org.elasticsearch.index.mapper.IdFieldMapper; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.mapper.SeqNoFieldMapper; -import org.elasticsearch.index.mapper.SourceFieldMapper; -import org.elasticsearch.index.mapper.Uid; -import org.elasticsearch.index.mapper.VersionFieldMapper; -import org.elasticsearch.index.translog.Translog; - -import java.io.Closeable; -import java.io.IOException; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * A {@link Translog.Snapshot} from changes in a Lucene index - */ -final class LuceneChangesSnapshot implements Translog.Snapshot { - static final int DEFAULT_BATCH_SIZE = 1024; - - private final int searchBatchSize; - private final long fromSeqNo, toSeqNo; - private long lastSeenSeqNo; - private int skippedOperations; - private final boolean requiredFullRange; - - private final IndexSearcher indexSearcher; - private final MapperService mapperService; - private int docIndex = 0; - private final int totalHits; - private ScoreDoc[] scoreDocs; - private final ParallelArray parallelArray; - private final Closeable onClose; - - /** - * Creates a new "translog" snapshot from Lucene for reading operations whose seq# in the specified range. - * - * @param engineSearcher the internal engine searcher which will be taken over if the snapshot is opened successfully - * @param mapperService the mapper service which will be mainly used to resolve the document's type and uid - * @param searchBatchSize the number of documents should be returned by each search - * @param fromSeqNo the min requesting seq# - inclusive - * @param toSeqNo the maximum requesting seq# - inclusive - * @param requiredFullRange if true, the snapshot will strictly check for the existence of operations between fromSeqNo and toSeqNo - */ - LuceneChangesSnapshot(Engine.Searcher engineSearcher, MapperService mapperService, int searchBatchSize, - long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { - if (fromSeqNo < 0 || toSeqNo < 0 || fromSeqNo > toSeqNo) { - throw new IllegalArgumentException("Invalid range; from_seqno [" + fromSeqNo + "], to_seqno [" + toSeqNo + "]"); - } - if (searchBatchSize <= 0) { - throw new IllegalArgumentException("Search_batch_size must be positive [" + searchBatchSize + "]"); - } - final AtomicBoolean closed = new AtomicBoolean(); - this.onClose = () -> { - if (closed.compareAndSet(false, true)) { - IOUtils.close(engineSearcher); - } - }; - this.mapperService = mapperService; - this.searchBatchSize = searchBatchSize; - this.fromSeqNo = fromSeqNo; - this.toSeqNo = toSeqNo; - this.lastSeenSeqNo = fromSeqNo - 1; - this.requiredFullRange = requiredFullRange; - this.indexSearcher = new IndexSearcher(Lucene.wrapAllDocsLive(engineSearcher.getDirectoryReader())); - this.indexSearcher.setQueryCache(null); - this.parallelArray = new ParallelArray(searchBatchSize); - final TopDocs topDocs = searchOperations(null); - this.totalHits = Math.toIntExact(topDocs.totalHits); - this.scoreDocs = topDocs.scoreDocs; - fillParallelArray(scoreDocs, parallelArray); - } - - @Override - public void close() throws IOException { - onClose.close(); - } - - @Override - public int totalOperations() { - return totalHits; - } - - @Override - public int skippedOperations() { - return skippedOperations; - } - - @Override - public Translog.Operation next() throws IOException { - Translog.Operation op = null; - for (int idx = nextDocIndex(); idx != -1; idx = nextDocIndex()) { - op = readDocAsOp(idx); - if (op != null) { - break; - } - } - if (requiredFullRange) { - rangeCheck(op); - } - if (op != null) { - lastSeenSeqNo = op.seqNo(); - } - return op; - } - - private void rangeCheck(Translog.Operation op) { - if (op == null) { - if (lastSeenSeqNo < toSeqNo) { - throw new IllegalStateException("Not all operations between from_seqno [" + fromSeqNo + "] " + - "and to_seqno [" + toSeqNo + "] found; prematurely terminated last_seen_seqno [" + lastSeenSeqNo + "]"); - } - } else { - final long expectedSeqNo = lastSeenSeqNo + 1; - if (op.seqNo() != expectedSeqNo) { - throw new IllegalStateException("Not all operations between from_seqno [" + fromSeqNo + "] " + - "and to_seqno [" + toSeqNo + "] found; expected seqno [" + expectedSeqNo + "]; found [" + op + "]"); - } - } - } - - private int nextDocIndex() throws IOException { - // we have processed all docs in the current search - fetch the next batch - if (docIndex == scoreDocs.length && docIndex > 0) { - final ScoreDoc prev = scoreDocs[scoreDocs.length - 1]; - scoreDocs = searchOperations(prev).scoreDocs; - fillParallelArray(scoreDocs, parallelArray); - docIndex = 0; - } - if (docIndex < scoreDocs.length) { - int idx = docIndex; - docIndex++; - return idx; - } - return -1; - } - - private void fillParallelArray(ScoreDoc[] scoreDocs, ParallelArray parallelArray) throws IOException { - if (scoreDocs.length > 0) { - for (int i = 0; i < scoreDocs.length; i++) { - scoreDocs[i].shardIndex = i; - } - // for better loading performance we sort the array by docID and - // then visit all leaves in order. - ArrayUtil.introSort(scoreDocs, Comparator.comparingInt(i -> i.doc)); - int docBase = -1; - int maxDoc = 0; - List leaves = indexSearcher.getIndexReader().leaves(); - int readerIndex = 0; - CombinedDocValues combinedDocValues = null; - LeafReaderContext leaf = null; - for (int i = 0; i < scoreDocs.length; i++) { - ScoreDoc scoreDoc = scoreDocs[i]; - if (scoreDoc.doc >= docBase + maxDoc) { - do { - leaf = leaves.get(readerIndex++); - docBase = leaf.docBase; - maxDoc = leaf.reader().maxDoc(); - } while (scoreDoc.doc >= docBase + maxDoc); - combinedDocValues = new CombinedDocValues(leaf.reader()); - } - final int segmentDocID = scoreDoc.doc - docBase; - final int index = scoreDoc.shardIndex; - parallelArray.leafReaderContexts[index] = leaf; - parallelArray.seqNo[index] = combinedDocValues.docSeqNo(segmentDocID); - parallelArray.primaryTerm[index] = combinedDocValues.docPrimaryTerm(segmentDocID); - parallelArray.version[index] = combinedDocValues.docVersion(segmentDocID); - parallelArray.isTombStone[index] = combinedDocValues.isTombstone(segmentDocID); - parallelArray.hasRecoverySource[index] = combinedDocValues.hasRecoverySource(segmentDocID); - } - // now sort back based on the shardIndex. we use this to store the previous index - ArrayUtil.introSort(scoreDocs, Comparator.comparingInt(i -> i.shardIndex)); - } - } - - private TopDocs searchOperations(ScoreDoc after) throws IOException { - final Query rangeQuery = LongPoint.newRangeQuery(SeqNoFieldMapper.NAME, lastSeenSeqNo + 1, toSeqNo); - final Sort sortedBySeqNoThenByTerm = new Sort( - new SortField(SeqNoFieldMapper.NAME, SortField.Type.LONG), - new SortField(SeqNoFieldMapper.PRIMARY_TERM_NAME, SortField.Type.LONG, true) - ); - return indexSearcher.searchAfter(after, rangeQuery, searchBatchSize, sortedBySeqNoThenByTerm); - } - - private Translog.Operation readDocAsOp(int docIndex) throws IOException { - final LeafReaderContext leaf = parallelArray.leafReaderContexts[docIndex]; - final int segmentDocID = scoreDocs[docIndex].doc - leaf.docBase; - final long primaryTerm = parallelArray.primaryTerm[docIndex]; - // We don't have to read the nested child documents - those docs don't have primary terms. - if (primaryTerm == -1) { - skippedOperations++; - return null; - } - final long seqNo = parallelArray.seqNo[docIndex]; - // Only pick the first seen seq# - if (seqNo == lastSeenSeqNo) { - skippedOperations++; - return null; - } - final long version = parallelArray.version[docIndex]; - final String sourceField = parallelArray.hasRecoverySource[docIndex] ? SourceFieldMapper.RECOVERY_SOURCE_NAME : - SourceFieldMapper.NAME; - final FieldsVisitor fields = new FieldsVisitor(true, sourceField); - leaf.reader().document(segmentDocID, fields); - fields.postProcess(mapperService); - - final Translog.Operation op; - final boolean isTombstone = parallelArray.isTombStone[docIndex]; - if (isTombstone && fields.uid() == null) { - op = new Translog.NoOp(seqNo, primaryTerm, fields.source().utf8ToString()); - assert version == 1L : "Noop tombstone should have version 1L; actual version [" + version + "]"; - assert assertDocSoftDeleted(leaf.reader(), segmentDocID) : "Noop but soft_deletes field is not set [" + op + "]"; - } else { - final String id = fields.uid().id(); - final String type = fields.uid().type(); - final Term uid = new Term(IdFieldMapper.NAME, Uid.encodeId(id)); - if (isTombstone) { - op = new Translog.Delete(type, id, uid, seqNo, primaryTerm, version); - assert assertDocSoftDeleted(leaf.reader(), segmentDocID) : "Delete op but soft_deletes field is not set [" + op + "]"; - } else { - final BytesReference source = fields.source(); - if (source == null) { - // TODO: Callers should ask for the range that source should be retained. Thus we should always - // check for the existence source once we make peer-recovery to send ops after the local checkpoint. - if (requiredFullRange) { - throw new IllegalStateException("source not found for seqno=" + seqNo + - " from_seqno=" + fromSeqNo + " to_seqno=" + toSeqNo); - } else { - skippedOperations++; - return null; - } - } - // TODO: pass the latest timestamp from engine. - final long autoGeneratedIdTimestamp = -1; - op = new Translog.Index(type, id, seqNo, primaryTerm, version, - source.toBytesRef().bytes, fields.routing(), autoGeneratedIdTimestamp); - } - } - assert fromSeqNo <= op.seqNo() && op.seqNo() <= toSeqNo && lastSeenSeqNo < op.seqNo() : "Unexpected operation; " + - "last_seen_seqno [" + lastSeenSeqNo + "], from_seqno [" + fromSeqNo + "], to_seqno [" + toSeqNo + "], op [" + op + "]"; - return op; - } - - private boolean assertDocSoftDeleted(LeafReader leafReader, int segmentDocId) throws IOException { - final NumericDocValues ndv = leafReader.getNumericDocValues(Lucene.SOFT_DELETES_FIELD); - if (ndv == null || ndv.advanceExact(segmentDocId) == false) { - throw new IllegalStateException("DocValues for field [" + Lucene.SOFT_DELETES_FIELD + "] is not found"); - } - return ndv.longValue() == 1; - } - - private static final class ParallelArray { - final LeafReaderContext[] leafReaderContexts; - final long[] version; - final long[] seqNo; - final long[] primaryTerm; - final boolean[] isTombStone; - final boolean[] hasRecoverySource; - - ParallelArray(int size) { - version = new long[size]; - seqNo = new long[size]; - primaryTerm = new long[size]; - isTombStone = new boolean[size]; - hasRecoverySource = new boolean[size]; - leafReaderContexts = new LeafReaderContext[size]; - } - } - - private static final class CombinedDocValues { - private final NumericDocValues versionDV; - private final NumericDocValues seqNoDV; - private final NumericDocValues primaryTermDV; - private final NumericDocValues tombstoneDV; - private final NumericDocValues recoverySource; - - CombinedDocValues(LeafReader leafReader) throws IOException { - this.versionDV = Objects.requireNonNull(leafReader.getNumericDocValues(VersionFieldMapper.NAME), "VersionDV is missing"); - this.seqNoDV = Objects.requireNonNull(leafReader.getNumericDocValues(SeqNoFieldMapper.NAME), "SeqNoDV is missing"); - this.primaryTermDV = Objects.requireNonNull( - leafReader.getNumericDocValues(SeqNoFieldMapper.PRIMARY_TERM_NAME), "PrimaryTermDV is missing"); - this.tombstoneDV = leafReader.getNumericDocValues(SeqNoFieldMapper.TOMBSTONE_NAME); - this.recoverySource = leafReader.getNumericDocValues(SourceFieldMapper.RECOVERY_SOURCE_NAME); - } - - long docVersion(int segmentDocId) throws IOException { - assert versionDV.docID() < segmentDocId; - if (versionDV.advanceExact(segmentDocId) == false) { - throw new IllegalStateException("DocValues for field [" + VersionFieldMapper.NAME + "] is not found"); - } - return versionDV.longValue(); - } - - long docSeqNo(int segmentDocId) throws IOException { - assert seqNoDV.docID() < segmentDocId; - if (seqNoDV.advanceExact(segmentDocId) == false) { - throw new IllegalStateException("DocValues for field [" + SeqNoFieldMapper.NAME + "] is not found"); - } - return seqNoDV.longValue(); - } - - long docPrimaryTerm(int segmentDocId) throws IOException { - if (primaryTermDV == null) { - return -1L; - } - assert primaryTermDV.docID() < segmentDocId; - // Use -1 for docs which don't have primary term. The caller considers those docs as nested docs. - if (primaryTermDV.advanceExact(segmentDocId) == false) { - return -1; - } - return primaryTermDV.longValue(); - } - - boolean isTombstone(int segmentDocId) throws IOException { - if (tombstoneDV == null) { - return false; - } - assert tombstoneDV.docID() < segmentDocId; - return tombstoneDV.advanceExact(segmentDocId) && tombstoneDV.longValue() > 0; - } - - boolean hasRecoverySource(int segmentDocId) throws IOException { - if (recoverySource == null) { - return false; - } - assert recoverySource.docID() < segmentDocId; - return recoverySource.advanceExact(segmentDocId); - } - } -} diff --git a/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java b/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java deleted file mode 100644 index fde97562de8f..000000000000 --- a/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java +++ /dev/null @@ -1,292 +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.index.engine; - -import org.apache.lucene.codecs.DocValuesProducer; -import org.apache.lucene.codecs.StoredFieldsReader; -import org.apache.lucene.index.BinaryDocValues; -import org.apache.lucene.index.CodecReader; -import org.apache.lucene.index.FieldInfo; -import org.apache.lucene.index.FilterCodecReader; -import org.apache.lucene.index.FilterNumericDocValues; -import org.apache.lucene.index.MergePolicy; -import org.apache.lucene.index.NumericDocValues; -import org.apache.lucene.index.OneMergeWrappingMergePolicy; -import org.apache.lucene.index.SortedDocValues; -import org.apache.lucene.index.SortedNumericDocValues; -import org.apache.lucene.index.SortedSetDocValues; -import org.apache.lucene.index.StoredFieldVisitor; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; -import org.apache.lucene.search.ConjunctionDISI; -import org.apache.lucene.search.DocIdSetIterator; -import org.apache.lucene.search.DocValuesFieldExistsQuery; -import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.Query; -import org.apache.lucene.search.Scorer; -import org.apache.lucene.search.Weight; -import org.apache.lucene.util.BitSet; -import org.apache.lucene.util.BitSetIterator; - -import java.io.IOException; -import java.util.Arrays; -import java.util.function.Supplier; - -final class RecoverySourcePruneMergePolicy extends OneMergeWrappingMergePolicy { - RecoverySourcePruneMergePolicy(String recoverySourceField, Supplier retainSourceQuerySupplier, MergePolicy in) { - super(in, toWrap -> new OneMerge(toWrap.segments) { - @Override - public CodecReader wrapForMerge(CodecReader reader) throws IOException { - CodecReader wrapped = toWrap.wrapForMerge(reader); - return wrapReader(recoverySourceField, wrapped, retainSourceQuerySupplier); - } - }); - } - - // pkg private for testing - static CodecReader wrapReader(String recoverySourceField, CodecReader reader, Supplier retainSourceQuerySupplier) - throws IOException { - NumericDocValues recoverySource = reader.getNumericDocValues(recoverySourceField); - if (recoverySource == null || recoverySource.nextDoc() == DocIdSetIterator.NO_MORE_DOCS) { - return reader; // early terminate - nothing to do here since non of the docs has a recovery source anymore. - } - BooleanQuery.Builder builder = new BooleanQuery.Builder(); - builder.add(new DocValuesFieldExistsQuery(recoverySourceField), BooleanClause.Occur.FILTER); - builder.add(retainSourceQuerySupplier.get(), BooleanClause.Occur.FILTER); - IndexSearcher s = new IndexSearcher(reader); - s.setQueryCache(null); - Weight weight = s.createWeight(s.rewrite(builder.build()), false, 1.0f); - Scorer scorer = weight.scorer(reader.getContext()); - if (scorer != null) { - return new SourcePruningFilterCodecReader(recoverySourceField, reader, BitSet.of(scorer.iterator(), reader.maxDoc())); - } else { - return new SourcePruningFilterCodecReader(recoverySourceField, reader, null); - } - } - - private static class SourcePruningFilterCodecReader extends FilterCodecReader { - private final BitSet recoverySourceToKeep; - private final String recoverySourceField; - - SourcePruningFilterCodecReader(String recoverySourceField, CodecReader reader, BitSet recoverySourceToKeep) { - super(reader); - this.recoverySourceField = recoverySourceField; - this.recoverySourceToKeep = recoverySourceToKeep; - } - - @Override - public DocValuesProducer getDocValuesReader() { - DocValuesProducer docValuesReader = super.getDocValuesReader(); - return new FilterDocValuesProducer(docValuesReader) { - @Override - public NumericDocValues getNumeric(FieldInfo field) throws IOException { - NumericDocValues numeric = super.getNumeric(field); - if (recoverySourceField.equals(field.name)) { - assert numeric != null : recoverySourceField + " must have numeric DV but was null"; - final DocIdSetIterator intersection; - if (recoverySourceToKeep == null) { - // we can't return null here lucenes DocIdMerger expects an instance - intersection = DocIdSetIterator.empty(); - } else { - intersection = ConjunctionDISI.intersectIterators(Arrays.asList(numeric, - new BitSetIterator(recoverySourceToKeep, recoverySourceToKeep.length()))); - } - return new FilterNumericDocValues(numeric) { - @Override - public int nextDoc() throws IOException { - return intersection.nextDoc(); - } - - @Override - public int advance(int target) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean advanceExact(int target) { - throw new UnsupportedOperationException(); - } - }; - - } - return numeric; - } - }; - } - - @Override - public StoredFieldsReader getFieldsReader() { - StoredFieldsReader fieldsReader = super.getFieldsReader(); - return new FilterStoredFieldsReader(fieldsReader) { - @Override - public void visitDocument(int docID, StoredFieldVisitor visitor) throws IOException { - if (recoverySourceToKeep != null && recoverySourceToKeep.get(docID)) { - super.visitDocument(docID, visitor); - } else { - super.visitDocument(docID, new FilterStoredFieldVisitor(visitor) { - @Override - public Status needsField(FieldInfo fieldInfo) throws IOException { - if (recoverySourceField.equals(fieldInfo.name)) { - return Status.NO; - } - return super.needsField(fieldInfo); - } - }); - } - } - }; - } - - @Override - public CacheHelper getCoreCacheHelper() { - return null; - } - - @Override - public CacheHelper getReaderCacheHelper() { - return null; - } - - private static class FilterDocValuesProducer extends DocValuesProducer { - private final DocValuesProducer in; - - FilterDocValuesProducer(DocValuesProducer in) { - this.in = in; - } - - @Override - public NumericDocValues getNumeric(FieldInfo field) throws IOException { - return in.getNumeric(field); - } - - @Override - public BinaryDocValues getBinary(FieldInfo field) throws IOException { - return in.getBinary(field); - } - - @Override - public SortedDocValues getSorted(FieldInfo field) throws IOException { - return in.getSorted(field); - } - - @Override - public SortedNumericDocValues getSortedNumeric(FieldInfo field) throws IOException { - return in.getSortedNumeric(field); - } - - @Override - public SortedSetDocValues getSortedSet(FieldInfo field) throws IOException { - return in.getSortedSet(field); - } - - @Override - public void checkIntegrity() throws IOException { - in.checkIntegrity(); - } - - @Override - public void close() throws IOException { - in.close(); - } - - @Override - public long ramBytesUsed() { - return in.ramBytesUsed(); - } - } - - private static class FilterStoredFieldsReader extends StoredFieldsReader { - - private final StoredFieldsReader fieldsReader; - - FilterStoredFieldsReader(StoredFieldsReader fieldsReader) { - this.fieldsReader = fieldsReader; - } - - @Override - public long ramBytesUsed() { - return fieldsReader.ramBytesUsed(); - } - - @Override - public void close() throws IOException { - fieldsReader.close(); - } - - @Override - public void visitDocument(int docID, StoredFieldVisitor visitor) throws IOException { - fieldsReader.visitDocument(docID, visitor); - } - - @Override - public StoredFieldsReader clone() { - return fieldsReader.clone(); - } - - @Override - public void checkIntegrity() throws IOException { - fieldsReader.checkIntegrity(); - } - } - - private static class FilterStoredFieldVisitor extends StoredFieldVisitor { - private final StoredFieldVisitor visitor; - - FilterStoredFieldVisitor(StoredFieldVisitor visitor) { - this.visitor = visitor; - } - - @Override - public void binaryField(FieldInfo fieldInfo, byte[] value) throws IOException { - visitor.binaryField(fieldInfo, value); - } - - @Override - public void stringField(FieldInfo fieldInfo, byte[] value) throws IOException { - visitor.stringField(fieldInfo, value); - } - - @Override - public void intField(FieldInfo fieldInfo, int value) throws IOException { - visitor.intField(fieldInfo, value); - } - - @Override - public void longField(FieldInfo fieldInfo, long value) throws IOException { - visitor.longField(fieldInfo, value); - } - - @Override - public void floatField(FieldInfo fieldInfo, float value) throws IOException { - visitor.floatField(fieldInfo, value); - } - - @Override - public void doubleField(FieldInfo fieldInfo, double value) throws IOException { - visitor.doubleField(fieldInfo, value); - } - - @Override - public Status needsField(FieldInfo fieldInfo) throws IOException { - return visitor.needsField(fieldInfo); - } - } - } -} diff --git a/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java deleted file mode 100644 index af2ded8c4662..000000000000 --- a/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java +++ /dev/null @@ -1,120 +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.index.engine; - -import org.apache.lucene.document.LongPoint; -import org.apache.lucene.search.Query; -import org.elasticsearch.common.lease.Releasable; -import org.elasticsearch.index.mapper.SeqNoFieldMapper; -import org.elasticsearch.index.seqno.SequenceNumbers; -import org.elasticsearch.index.translog.Translog; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.LongSupplier; - -/** - * A policy that controls how many soft-deleted documents should be retained for peer-recovery and querying history changes purpose. - */ -final class SoftDeletesPolicy { - private final LongSupplier globalCheckpointSupplier; - private long localCheckpointOfSafeCommit; - // This lock count is used to prevent `minRetainedSeqNo` from advancing. - private int retentionLockCount; - // The extra number of operations before the global checkpoint are retained - private long retentionOperations; - // The min seq_no value that is retained - ops after this seq# should exist in the Lucene index. - private long minRetainedSeqNo; - - SoftDeletesPolicy(LongSupplier globalCheckpointSupplier, long minRetainedSeqNo, long retentionOperations) { - this.globalCheckpointSupplier = globalCheckpointSupplier; - this.retentionOperations = retentionOperations; - this.minRetainedSeqNo = minRetainedSeqNo; - this.localCheckpointOfSafeCommit = SequenceNumbers.NO_OPS_PERFORMED; - this.retentionLockCount = 0; - } - - /** - * Updates the number of soft-deleted documents prior to the global checkpoint to be retained - * See {@link org.elasticsearch.index.IndexSettings#INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING} - */ - synchronized void setRetentionOperations(long retentionOperations) { - this.retentionOperations = retentionOperations; - } - - /** - * Sets the local checkpoint of the current safe commit - */ - synchronized void setLocalCheckpointOfSafeCommit(long newCheckpoint) { - if (newCheckpoint < this.localCheckpointOfSafeCommit) { - throw new IllegalArgumentException("Local checkpoint can't go backwards; " + - "new checkpoint [" + newCheckpoint + "]," + "current checkpoint [" + localCheckpointOfSafeCommit + "]"); - } - this.localCheckpointOfSafeCommit = newCheckpoint; - } - - /** - * Acquires a lock on soft-deleted documents to prevent them from cleaning up in merge processes. This is necessary to - * make sure that all operations that are being retained will be retained until the lock is released. - * This is a analogy to the translog's retention lock; see {@link Translog#acquireRetentionLock()} - */ - synchronized Releasable acquireRetentionLock() { - assert retentionLockCount >= 0 : "Invalid number of retention locks [" + retentionLockCount + "]"; - retentionLockCount++; - final AtomicBoolean released = new AtomicBoolean(); - return () -> { - if (released.compareAndSet(false, true)) { - releaseRetentionLock(); - } - }; - } - - private synchronized void releaseRetentionLock() { - assert retentionLockCount > 0 : "Invalid number of retention locks [" + retentionLockCount + "]"; - retentionLockCount--; - } - - /** - * Returns the min seqno that is retained in the Lucene index. - * Operations whose seq# is least this value should exist in the Lucene index. - */ - synchronized long getMinRetainedSeqNo() { - // Do not advance if the retention lock is held - if (retentionLockCount == 0) { - // This policy retains operations for two purposes: peer-recovery and querying changes history. - // - Peer-recovery is driven by the local checkpoint of the safe commit. In peer-recovery, the primary transfers a safe commit, - // then sends ops after the local checkpoint of that commit. This requires keeping all ops after localCheckpointOfSafeCommit; - // - Changes APIs are driven the combination of the global checkpoint and retention ops. Here we prefer using the global - // checkpoint instead of max_seqno because only operations up to the global checkpoint are exposed in the the changes APIs. - final long minSeqNoForQueryingChanges = globalCheckpointSupplier.getAsLong() - retentionOperations; - final long minSeqNoToRetain = Math.min(minSeqNoForQueryingChanges, localCheckpointOfSafeCommit) + 1; - // This can go backward as the retentionOperations value can be changed in settings. - minRetainedSeqNo = Math.max(minRetainedSeqNo, minSeqNoToRetain); - } - return minRetainedSeqNo; - } - - /** - * Returns a soft-deletes retention query that will be used in {@link org.apache.lucene.index.SoftDeletesRetentionMergePolicy} - * Documents including tombstones are soft-deleted and matched this query will be retained and won't cleaned up by merges. - */ - Query getRetentionQuery() { - return LongPoint.newRangeQuery(SeqNoFieldMapper.NAME, getMinRetainedSeqNo(), Long.MAX_VALUE); - } -} diff --git a/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java b/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java index 462f8ce8e68b..4c65635c61b3 100644 --- a/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java +++ b/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java @@ -54,19 +54,13 @@ public class FieldsVisitor extends StoredFieldVisitor { RoutingFieldMapper.NAME)); private final boolean loadSource; - private final String sourceFieldName; private final Set requiredFields; protected BytesReference source; protected String type, id; protected Map> fieldsValues; public FieldsVisitor(boolean loadSource) { - this(loadSource, SourceFieldMapper.NAME); - } - - public FieldsVisitor(boolean loadSource, String sourceFieldName) { this.loadSource = loadSource; - this.sourceFieldName = sourceFieldName; requiredFields = new HashSet<>(); reset(); } @@ -109,7 +103,7 @@ public void postProcess(MapperService mapperService) { @Override public void binaryField(FieldInfo fieldInfo, byte[] value) throws IOException { - if (sourceFieldName.equals(fieldInfo.name)) { + if (SourceFieldMapper.NAME.equals(fieldInfo.name)) { source = new BytesArray(value); } else if (IdFieldMapper.NAME.equals(fieldInfo.name)) { id = Uid.decodeId(value); @@ -181,7 +175,7 @@ public void reset() { requiredFields.addAll(BASE_REQUIRED_FIELDS); if (loadSource) { - requiredFields.add(sourceFieldName); + requiredFields.add(SourceFieldMapper.NAME); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java index 663aa7e6f9e1..a0640ac68a99 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java @@ -19,14 +19,11 @@ package org.elasticsearch.index.mapper; -import org.apache.lucene.document.StoredField; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.Weight; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchGenerationException; -import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.text.Text; @@ -42,15 +39,12 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Stream; public class DocumentMapper implements ToXContentFragment { @@ -127,8 +121,6 @@ public DocumentMapper build(MapperService mapperService) { private final Map objectMappers; private final boolean hasNestedObjects; - private final MetadataFieldMapper[] deleteTombstoneMetadataFieldMappers; - private final MetadataFieldMapper[] noopTombstoneMetadataFieldMappers; public DocumentMapper(MapperService mapperService, Mapping mapping) { this.mapperService = mapperService; @@ -179,15 +171,6 @@ public DocumentMapper(MapperService mapperService, Mapping mapping) { } catch (Exception e) { throw new ElasticsearchGenerationException("failed to serialize source for type [" + type + "]", e); } - - final Collection deleteTombstoneMetadataFields = Arrays.asList(VersionFieldMapper.NAME, IdFieldMapper.NAME, - TypeFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME); - this.deleteTombstoneMetadataFieldMappers = Stream.of(mapping.metadataMappers) - .filter(field -> deleteTombstoneMetadataFields.contains(field.name())).toArray(MetadataFieldMapper[]::new); - final Collection noopTombstoneMetadataFields = Arrays.asList( - VersionFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME); - this.noopTombstoneMetadataFieldMappers = Stream.of(mapping.metadataMappers) - .filter(field -> noopTombstoneMetadataFields.contains(field.name())).toArray(MetadataFieldMapper[]::new); } public Mapping mapping() { @@ -259,22 +242,7 @@ public Map objectMappers() { } public ParsedDocument parse(SourceToParse source) throws MapperParsingException { - return documentParser.parseDocument(source, mapping.metadataMappers); - } - - public ParsedDocument createDeleteTombstoneDoc(String index, String type, String id) throws MapperParsingException { - final SourceToParse emptySource = SourceToParse.source(index, type, id, new BytesArray("{}"), XContentType.JSON); - return documentParser.parseDocument(emptySource, deleteTombstoneMetadataFieldMappers).toTombstone(); - } - - public ParsedDocument createNoopTombstoneDoc(String index, String reason) throws MapperParsingException { - final String id = ""; // _id won't be used. - final SourceToParse sourceToParse = SourceToParse.source(index, type, id, new BytesArray("{}"), XContentType.JSON); - final ParsedDocument parsedDoc = documentParser.parseDocument(sourceToParse, noopTombstoneMetadataFieldMappers).toTombstone(); - // Store the reason of a noop as a raw string in the _source field - final BytesRef byteRef = new BytesRef(reason); - parsedDoc.rootDoc().add(new StoredField(SourceFieldMapper.NAME, byteRef.bytes, byteRef.offset, byteRef.length)); - return parsedDoc; + return documentParser.parseDocument(source); } /** diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java index 85123f602edf..0fd156c09053 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java @@ -55,7 +55,7 @@ final class DocumentParser { this.docMapper = docMapper; } - ParsedDocument parseDocument(SourceToParse source, MetadataFieldMapper[] metadataFieldsMappers) throws MapperParsingException { + ParsedDocument parseDocument(SourceToParse source) throws MapperParsingException { validateType(source); final Mapping mapping = docMapper.mapping(); @@ -64,9 +64,9 @@ ParsedDocument parseDocument(SourceToParse source, MetadataFieldMapper[] metadat try (XContentParser parser = XContentHelper.createParser(docMapperParser.getXContentRegistry(), LoggingDeprecationHandler.INSTANCE, source.source(), xContentType)) { - context = new ParseContext.InternalParseContext(indexSettings, docMapperParser, docMapper, source, parser); + context = new ParseContext.InternalParseContext(indexSettings.getSettings(), docMapperParser, docMapper, source, parser); validateStart(parser); - internalParseDocument(mapping, metadataFieldsMappers, context, parser); + internalParseDocument(mapping, context, parser); validateEnd(parser); } catch (Exception e) { throw wrapInMapperParsingException(source, e); @@ -81,11 +81,10 @@ ParsedDocument parseDocument(SourceToParse source, MetadataFieldMapper[] metadat return parsedDocument(source, context, createDynamicUpdate(mapping, docMapper, context.getDynamicMappers())); } - private static void internalParseDocument(Mapping mapping, MetadataFieldMapper[] metadataFieldsMappers, - ParseContext.InternalParseContext context, XContentParser parser) throws IOException { + private static void internalParseDocument(Mapping mapping, ParseContext.InternalParseContext context, XContentParser parser) throws IOException { final boolean emptyDoc = isEmptyDoc(mapping, parser); - for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) { + for (MetadataFieldMapper metadataMapper : mapping.metadataMappers) { metadataMapper.preParse(context); } @@ -96,7 +95,7 @@ private static void internalParseDocument(Mapping mapping, MetadataFieldMapper[] parseObjectOrNested(context, mapping.root); } - for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) { + for (MetadataFieldMapper metadataMapper : mapping.metadataMappers) { metadataMapper.postParse(context); } } @@ -496,7 +495,7 @@ private static void parseObject(final ParseContext context, ObjectMapper mapper, if (builder == null) { builder = new ObjectMapper.Builder(currentFieldName).enabled(true); } - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); objectMapper = builder.build(builderContext); context.addDynamicMapper(objectMapper); context.path().add(currentFieldName); @@ -539,7 +538,7 @@ private static void parseArray(ParseContext context, ObjectMapper parentMapper, if (builder == null) { parseNonDynamicArray(context, parentMapper, lastFieldName, arrayFieldName); } else { - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); mapper = builder.build(builderContext); assert mapper != null; if (mapper instanceof ArrayValueMapperParser) { @@ -697,13 +696,13 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont if (parseableAsLong && context.root().numericDetection()) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.LONG); if (builder == null) { - builder = newLongBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); + builder = newLongBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); } return builder; } else if (parseableAsDouble && context.root().numericDetection()) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.DOUBLE); if (builder == null) { - builder = newFloatBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); + builder = newFloatBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); } return builder; } else if (parseableAsLong == false && parseableAsDouble == false && context.root().dateDetection()) { @@ -719,7 +718,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont } Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.DATE); if (builder == null) { - builder = newDateBuilder(currentFieldName, dateTimeFormatter, context.indexSettings().getIndexVersionCreated()); + builder = newDateBuilder(currentFieldName, dateTimeFormatter, Version.indexCreated(context.indexSettings())); } if (builder instanceof DateFieldMapper.Builder) { DateFieldMapper.Builder dateBuilder = (DateFieldMapper.Builder) builder; @@ -742,7 +741,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont if (numberType == XContentParser.NumberType.INT || numberType == XContentParser.NumberType.LONG) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.LONG); if (builder == null) { - builder = newLongBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); + builder = newLongBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); } return builder; } else if (numberType == XContentParser.NumberType.FLOAT || numberType == XContentParser.NumberType.DOUBLE) { @@ -751,7 +750,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont // no templates are defined, we use float by default instead of double // since this is much more space-efficient and should be enough most of // the time - builder = newFloatBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); + builder = newFloatBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); } return builder; } @@ -786,7 +785,7 @@ private static void parseDynamicValue(final ParseContext context, ObjectMapper p return; } final String path = context.path().pathAsText(currentFieldName); - final Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); + final Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); final MappedFieldType existingFieldType = context.mapperService().fullName(path); final Mapper.Builder builder; if (existingFieldType != null) { @@ -884,8 +883,8 @@ private static Tuple getDynamicParentMapper(ParseContext if (builder == null) { builder = new ObjectMapper.Builder(paths[i]).enabled(true); } - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), - context.path()); mapper = (ObjectMapper) builder.build(builderContext); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + mapper = (ObjectMapper) builder.build(builderContext); if (mapper.nested() != ObjectMapper.Nested.NO) { throw new MapperParsingException("It is forbidden to create dynamic nested objects ([" + context.path().pathAsText(paths[i]) + "]) through `copy_to` or dots in field names"); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java index 8389a3062701..606777392dec 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java @@ -24,6 +24,7 @@ import org.apache.lucene.index.IndexableField; import org.apache.lucene.search.Query; import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.lucene.Lucene; @@ -204,12 +205,12 @@ public FieldNamesFieldType fieldType() { } @Override - public void preParse(ParseContext context) { + public void preParse(ParseContext context) throws IOException { } @Override public void postParse(ParseContext context) throws IOException { - if (context.indexSettings().getIndexVersionCreated().before(Version.V_6_1_0)) { + if (context.indexSettings().getAsVersion(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).before(Version.V_6_1_0)) { super.parse(context); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java b/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java index cf8cc4022fd8..b77ffee05caf 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java @@ -24,8 +24,9 @@ import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexableField; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.index.IndexSettings; import java.util.ArrayList; import java.util.Collection; @@ -195,7 +196,7 @@ public boolean isWithinMultiFields() { } @Override - public IndexSettings indexSettings() { + public Settings indexSettings() { return in.indexSettings(); } @@ -314,7 +315,8 @@ public static class InternalParseContext extends ParseContext { private final List documents; - private final IndexSettings indexSettings; + @Nullable + private final Settings indexSettings; private final SourceToParse sourceToParse; @@ -332,8 +334,8 @@ public static class InternalParseContext extends ParseContext { private final Set ignoredFields = new HashSet<>(); - public InternalParseContext(IndexSettings indexSettings, DocumentMapperParser docMapperParser, DocumentMapper docMapper, - SourceToParse source, XContentParser parser) { + public InternalParseContext(@Nullable Settings indexSettings, DocumentMapperParser docMapperParser, DocumentMapper docMapper, + SourceToParse source, XContentParser parser) { this.indexSettings = indexSettings; this.docMapper = docMapper; this.docMapperParser = docMapperParser; @@ -345,7 +347,7 @@ public InternalParseContext(IndexSettings indexSettings, DocumentMapperParser do this.version = null; this.sourceToParse = source; this.dynamicMappers = new ArrayList<>(); - this.maxAllowedNumNestedDocs = indexSettings.getValue(MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING); + this.maxAllowedNumNestedDocs = MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING.get(indexSettings); this.numNestedDocs = 0L; } @@ -355,7 +357,8 @@ public DocumentMapperParser docMapperParser() { } @Override - public IndexSettings indexSettings() { + @Nullable + public Settings indexSettings() { return this.indexSettings; } @@ -562,7 +565,8 @@ public boolean isWithinMultiFields() { return false; } - public abstract IndexSettings indexSettings(); + @Nullable + public abstract Settings indexSettings(); public abstract SourceToParse sourceToParse(); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java index d2cf17ddd350..414cb3a98eca 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java @@ -83,17 +83,6 @@ public void updateSeqID(long sequenceNumber, long primaryTerm) { this.seqID.primaryTerm.setLongValue(primaryTerm); } - /** - * Makes the processing document as a tombstone document rather than a regular document. - * Tombstone documents are stored in Lucene index to represent delete operations or Noops. - */ - ParsedDocument toTombstone() { - assert docs().size() == 1 : "Tombstone should have a single doc [" + docs() + "]"; - this.seqID.tombstoneField.setLongValue(1); - rootDoc().add(this.seqID.tombstoneField); - return this; - } - public String routing() { return this.routing; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java index 5a0db4163bf2..ac3ffe462723 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java @@ -69,29 +69,26 @@ public static class SequenceIDFields { public final Field seqNo; public final Field seqNoDocValue; public final Field primaryTerm; - public final Field tombstoneField; - public SequenceIDFields(Field seqNo, Field seqNoDocValue, Field primaryTerm, Field tombstoneField) { + public SequenceIDFields(Field seqNo, Field seqNoDocValue, Field primaryTerm) { Objects.requireNonNull(seqNo, "sequence number field cannot be null"); Objects.requireNonNull(seqNoDocValue, "sequence number dv field cannot be null"); Objects.requireNonNull(primaryTerm, "primary term field cannot be null"); this.seqNo = seqNo; this.seqNoDocValue = seqNoDocValue; this.primaryTerm = primaryTerm; - this.tombstoneField = tombstoneField; } public static SequenceIDFields emptySeqID() { return new SequenceIDFields(new LongPoint(NAME, SequenceNumbers.UNASSIGNED_SEQ_NO), new NumericDocValuesField(NAME, SequenceNumbers.UNASSIGNED_SEQ_NO), - new NumericDocValuesField(PRIMARY_TERM_NAME, 0), new NumericDocValuesField(TOMBSTONE_NAME, 0)); + new NumericDocValuesField(PRIMARY_TERM_NAME, 0)); } } public static final String NAME = "_seq_no"; public static final String CONTENT_TYPE = "_seq_no"; public static final String PRIMARY_TERM_NAME = "_primary_term"; - public static final String TOMBSTONE_NAME = "_tombstone"; public static class SeqNoDefaults { public static final String NAME = SeqNoFieldMapper.NAME; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java index 7bfe793bba4a..f2090613c096 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.mapper; -import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.StoredField; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexableField; @@ -50,7 +49,6 @@ public class SourceFieldMapper extends MetadataFieldMapper { public static final String NAME = "_source"; - public static final String RECOVERY_SOURCE_NAME = "_recovery_source"; public static final String CONTENT_TYPE = "_source"; private final Function, Map> filter; @@ -226,8 +224,7 @@ public Mapper parse(ParseContext context) throws IOException { @Override protected void parseCreateField(ParseContext context, List fields) throws IOException { - BytesReference originalSource = context.sourceToParse().source(); - BytesReference source = originalSource; + BytesReference source = context.sourceToParse().source(); if (enabled && fieldType().stored() && source != null) { // Percolate and tv APIs may not set the source and that is ok, because these APIs will not index any data if (filter != null) { @@ -243,17 +240,8 @@ protected void parseCreateField(ParseContext context, List field } BytesRef ref = source.toBytesRef(); fields.add(new StoredField(fieldType().name(), ref.bytes, ref.offset, ref.length)); - } else { - source = null; } - - if (originalSource != null && source != originalSource && context.indexSettings().isSoftDeleteEnabled()) { - // if we omitted source or modified it we add the _recovery_source to ensure we have it for ops based recovery - BytesRef ref = originalSource.toBytesRef(); - fields.add(new StoredField(RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); - fields.add(new NumericDocValuesField(RECOVERY_SOURCE_NAME, 1)); - } - } + } @Override protected String contentType() { 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 ef5f9ab0ef3e..e030c95b56e3 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -92,14 +92,12 @@ import org.elasticsearch.index.flush.FlushStats; import org.elasticsearch.index.get.GetStats; import org.elasticsearch.index.get.ShardGetService; -import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.DocumentMapperForType; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParsedDocument; -import org.elasticsearch.index.mapper.RootObjectMapper; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.merge.MergeStats; @@ -1622,33 +1620,25 @@ public void onSettingsChanged() { } /** - * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed + * Acquires a lock on the translog files, preventing them from being trimmed. */ - public Closeable acquireRetentionLockForPeerRecovery() { - return getEngine().acquireRetentionLockForPeerRecovery(); + public Closeable acquireTranslogRetentionLock() { + return getEngine().acquireTranslogRetentionLock(); } /** - * Returns the estimated number of history operations whose seq# at least the provided seq# in this shard. + * Creates a new translog snapshot for reading translog operations whose seq# at least the provided seq#. + * The caller has to close the returned snapshot after finishing the reading. */ - public int estimateNumberOfHistoryOperations(String source, long startingSeqNo) throws IOException { - return getEngine().estimateNumberOfHistoryOperations(source, mapperService, startingSeqNo); + public Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException { + return getEngine().newTranslogSnapshotFromMinSeqNo(minSeqNo); } /** - * Creates a new history snapshot for reading operations since the provided starting seqno (inclusive). - * The returned snapshot can be retrieved from either Lucene index or translog files. + * Returns the estimated number of operations in translog whose seq# at least the provided seq#. */ - public Translog.Snapshot getHistoryOperations(String source, long startingSeqNo) throws IOException { - return getEngine().readHistoryOperations(source, mapperService, startingSeqNo); - } - - /** - * Checks if we have a completed history of operations since the given starting seqno (inclusive). - * This method should be called after acquiring the retention lock; See {@link #acquireRetentionLockForPeerRecovery()} - */ - public boolean hasCompleteHistoryOperations(String source, long startingSeqNo) throws IOException { - return getEngine().hasCompleteOperationHistory(source, mapperService, startingSeqNo); + public int estimateTranslogOperationsFromMinSeq(long minSeqNo) { + return getEngine().estimateTranslogOperationsFromMinSeq(minSeqNo); } public List segments(boolean verbose) { @@ -2219,7 +2209,7 @@ private EngineConfig newEngineConfig() { IndexingMemoryController.SHARD_INACTIVE_TIME_SETTING.get(indexSettings.getSettings()), Collections.singletonList(refreshListeners), Collections.singletonList(new RefreshMetricUpdater(refreshMetric)), - indexSort, this::runTranslogRecovery, circuitBreakerService, replicationTracker, () -> operationPrimaryTerm, tombstoneDocSupplier()); + indexSort, this::runTranslogRecovery, circuitBreakerService, replicationTracker, () -> operationPrimaryTerm); } /** @@ -2658,19 +2648,4 @@ public void afterRefresh(boolean didRefresh) throws IOException { refreshMetric.inc(System.nanoTime() - currentRefreshStartTime); } } - - private EngineConfig.TombstoneDocSupplier tombstoneDocSupplier() { - final RootObjectMapper.Builder noopRootMapper = new RootObjectMapper.Builder("__noop"); - final DocumentMapper noopDocumentMapper = new DocumentMapper.Builder(noopRootMapper, mapperService).build(mapperService); - return new EngineConfig.TombstoneDocSupplier() { - @Override - public ParsedDocument newDeleteTombstoneDoc(String type, String id) { - return docMapper(type).getDocumentMapper().createDeleteTombstoneDoc(shardId.getIndexName(), type, id); - } - @Override - public ParsedDocument newNoopTombstoneDoc(String reason) { - return noopDocumentMapper.createNoopTombstoneDoc(shardId.getIndexName(), reason); - } - }; - } } diff --git a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java index 016a8afff696..1edc0eb5dcaf 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java +++ b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java @@ -89,7 +89,7 @@ public void resync(final IndexShard indexShard, final ActionListener // Wrap translog snapshot to make it synchronized as it is accessed by different threads through SnapshotSender. // Even though those calls are not concurrent, snapshot.next() uses non-synchronized state and is not multi-thread-compatible // Also fail the resync early if the shard is shutting down - snapshot = indexShard.getHistoryOperations("resync", startingSeqNo); + snapshot = indexShard.newTranslogSnapshotFromMinSeqNo(startingSeqNo); final Translog.Snapshot originalSnapshot = snapshot; final Translog.Snapshot wrappedSnapshot = new Translog.Snapshot() { @Override diff --git a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java index ae3f90e63e7d..e9acfe3d8b06 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java +++ b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java @@ -156,7 +156,6 @@ void addIndices(final RecoveryState.Index indexRecoveryStats, final Directory ta final Directory hardLinkOrCopyTarget = new org.apache.lucene.store.HardlinkCopyDirectoryWrapper(target); IndexWriterConfig iwc = new IndexWriterConfig(null) - .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) // we don't want merges to happen here - we call maybe merge on the engine // later once we stared it up otherwise we would need to wait for it here 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 85975bc68c85..001e263ea8ff 100644 --- a/server/src/main/java/org/elasticsearch/index/store/Store.java +++ b/server/src/main/java/org/elasticsearch/index/store/Store.java @@ -1009,6 +1009,7 @@ public RecoveryDiff recoveryDiff(MetadataSnapshot recoveryTargetSnapshot) { } final String segmentId = IndexFileNames.parseSegmentName(meta.name()); final String extension = IndexFileNames.getExtension(meta.name()); + assert FIELD_INFOS_FILE_EXTENSION.equals(extension) == false || IndexFileNames.stripExtension(IndexFileNames.stripSegmentName(meta.name())).isEmpty() : "FieldInfos are generational but updateable DV are not supported in elasticsearch"; if (IndexFileNames.SEGMENTS.equals(segmentId) || DEL_FILE_EXTENSION.equals(extension) || LIV_FILE_EXTENSION.equals(extension)) { // only treat del files as per-commit files fnm files are generational but only for upgradable DV perCommitStoreFiles.add(meta); @@ -1594,7 +1595,6 @@ private static IndexWriter newIndexWriter(final IndexWriterConfig.OpenMode openM throws IOException { assert openMode == IndexWriterConfig.OpenMode.APPEND || commit == null : "can't specify create flag with a commit"; IndexWriterConfig iwc = new IndexWriterConfig(null) - .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) .setIndexCommit(commit) // we don't want merges to happen here - we call maybe merge on the engine diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index f17acac37896..618aa546e425 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -1261,8 +1261,6 @@ public String toString() { ", type='" + type + '\'' + ", seqNo=" + seqNo + ", primaryTerm=" + primaryTerm + - ", version=" + version + - ", autoGeneratedIdTimestamp=" + autoGeneratedIdTimestamp + '}'; } @@ -1405,7 +1403,6 @@ public String toString() { "uid=" + uid + ", seqNo=" + seqNo + ", primaryTerm=" + primaryTerm + - ", version=" + version + '}'; } } diff --git a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java index e0cfe9eaaff0..f48f2ceb7927 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java @@ -40,7 +40,6 @@ import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.LongSupplier; @@ -193,24 +192,7 @@ private synchronized boolean assertNoSeqNumberConflict(long seqNo, BytesReferenc new BufferedChecksumStreamInput(data.streamInput(), "assertion")); Translog.Operation prvOp = Translog.readOperation( new BufferedChecksumStreamInput(previous.v1().streamInput(), "assertion")); - // TODO: We haven't had timestamp for Index operations in Lucene yet, we need to loosen this check without timestamp. - final boolean sameOp; - if (newOp instanceof Translog.Index && prvOp instanceof Translog.Index) { - final Translog.Index o1 = (Translog.Index) prvOp; - final Translog.Index o2 = (Translog.Index) newOp; - sameOp = Objects.equals(o1.id(), o2.id()) && Objects.equals(o1.type(), o2.type()) - && Objects.equals(o1.source(), o2.source()) && Objects.equals(o1.routing(), o2.routing()) - && o1.primaryTerm() == o2.primaryTerm() && o1.seqNo() == o2.seqNo() - && o1.version() == o2.version(); - } else if (newOp instanceof Translog.Delete && prvOp instanceof Translog.Delete) { - final Translog.Delete o1 = (Translog.Delete) newOp; - final Translog.Delete o2 = (Translog.Delete) prvOp; - sameOp = Objects.equals(o1.id(), o2.id()) && Objects.equals(o1.type(), o2.type()) - && o1.primaryTerm() == o2.primaryTerm() && o1.seqNo() == o2.seqNo() && o1.version() == o2.version(); - } else { - sameOp = false; - } - if (sameOp == false) { + if (newOp.equals(prvOp) == false) { throw new AssertionError( "seqNo [" + seqNo + "] was processed twice in generation [" + generation + "], with different data. " + "prvOp [" + prvOp + "], newOp [" + newOp + "]", previous.v2()); diff --git a/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java b/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java index a90f8af0af42..86995ae7c5a9 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java @@ -32,7 +32,6 @@ import org.apache.lucene.store.Lock; import org.apache.lucene.store.LockObtainFailedException; import org.apache.lucene.store.NativeFSLockFactory; -import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cli.EnvironmentAwareCommand; @@ -178,7 +177,6 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th terminal.println("Marking index with the new history uuid"); // commit the new histroy id IndexWriterConfig iwc = new IndexWriterConfig(null) - .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) // we don't want merges to happen here - we call maybe merge on the engine // later once we stared it up otherwise we would need to wait for it here diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 10f796e5e155..352f07d57649 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -146,11 +146,11 @@ public RecoveryResponse recoverToTarget() throws IOException { assert targetShardRouting.initializing() : "expected recovery target to be initializing but was " + targetShardRouting; }, shardId + " validating recovery target ["+ request.targetAllocationId() + "] registered ", shard, cancellableThreads, logger); - try (Closeable ignored = shard.acquireRetentionLockForPeerRecovery()) { + try (Closeable ignored = shard.acquireTranslogRetentionLock()) { final long startingSeqNo; final long requiredSeqNoRangeStart; final boolean isSequenceNumberBasedRecovery = request.startingSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && - isTargetSameHistory() && shard.hasCompleteHistoryOperations("peer-recovery", request.startingSeqNo()); + isTargetSameHistory() && isTranslogReadyForSequenceNumberBasedRecovery(); if (isSequenceNumberBasedRecovery) { logger.trace("performing sequence numbers based recovery. starting at [{}]", request.startingSeqNo()); startingSeqNo = request.startingSeqNo(); @@ -162,16 +162,14 @@ public RecoveryResponse recoverToTarget() throws IOException { } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "snapshot failed", e); } - // We must have everything above the local checkpoint in the commit + // we set this to 0 to create a translog roughly according to the retention policy + // on the target. Note that it will still filter out legacy operations with no sequence numbers + startingSeqNo = 0; + // but we must have everything above the local checkpoint in the commit requiredSeqNoRangeStart = Long.parseLong(phase1Snapshot.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; - // If soft-deletes enabled, we need to transfer only operations after the local_checkpoint of the commit to have - // the same history on the target. However, with translog, we need to set this to 0 to create a translog roughly - // according to the retention policy on the target. Note that it will still filter out legacy operations without seqNo. - startingSeqNo = shard.indexSettings().isSoftDeleteEnabled() ? requiredSeqNoRangeStart : 0; try { - final int estimateNumOps = shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo); - phase1(phase1Snapshot.getIndexCommit(), () -> estimateNumOps); + phase1(phase1Snapshot.getIndexCommit(), () -> shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "phase1 failed", e); } finally { @@ -188,8 +186,7 @@ public RecoveryResponse recoverToTarget() throws IOException { try { // For a sequence based recovery, the target can keep its local translog - prepareTargetForTranslog(isSequenceNumberBasedRecovery == false, - shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo)); + prepareTargetForTranslog(isSequenceNumberBasedRecovery == false, shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "prepare target for translog failed", e); } @@ -210,13 +207,11 @@ public RecoveryResponse recoverToTarget() throws IOException { */ cancellableThreads.execute(() -> shard.waitForOpsToComplete(endingSeqNo)); - if (logger.isTraceEnabled()) { - logger.trace("all operations up to [{}] completed, which will be used as an ending sequence number", endingSeqNo); - logger.trace("snapshot translog for recovery; current size is [{}]", - shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo)); - } + logger.trace("all operations up to [{}] completed, which will be used as an ending sequence number", endingSeqNo); + + logger.trace("snapshot translog for recovery; current size is [{}]", shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); final long targetLocalCheckpoint; - try (Translog.Snapshot snapshot = shard.getHistoryOperations("peer-recovery", startingSeqNo)) { + try(Translog.Snapshot snapshot = shard.newTranslogSnapshotFromMinSeqNo(startingSeqNo)) { targetLocalCheckpoint = phase2(startingSeqNo, requiredSeqNoRangeStart, endingSeqNo, snapshot); } catch (Exception e) { throw new RecoveryEngineException(shard.shardId(), 2, "phase2 failed", e); @@ -273,6 +268,36 @@ public void onFailure(Exception e) { }); } + /** + * Determines if the source translog is ready for a sequence-number-based peer recovery. The main condition here is that the source + * translog contains all operations above the local checkpoint on the target. We already know the that translog contains or will contain + * all ops above the source local checkpoint, so we can stop check there. + * + * @return {@code true} if the source is ready for a sequence-number-based recovery + * @throws IOException if an I/O exception occurred reading the translog snapshot + */ + boolean isTranslogReadyForSequenceNumberBasedRecovery() throws IOException { + final long startingSeqNo = request.startingSeqNo(); + assert startingSeqNo >= 0; + final long localCheckpoint = shard.getLocalCheckpoint(); + logger.trace("testing sequence numbers in range: [{}, {}]", startingSeqNo, localCheckpoint); + // the start recovery request is initialized with the starting sequence number set to the target shard's local checkpoint plus one + if (startingSeqNo - 1 <= localCheckpoint) { + final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); + try (Translog.Snapshot snapshot = shard.newTranslogSnapshotFromMinSeqNo(startingSeqNo)) { + Translog.Operation operation; + while ((operation = snapshot.next()) != null) { + if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { + tracker.markSeqNoAsCompleted(operation.seqNo()); + } + } + } + return tracker.getCheckpoint() >= localCheckpoint; + } else { + return false; + } + } + /** * Perform phase1 of the recovery operations. Once this {@link IndexCommit} * snapshot has been performed no commit operations (files being fsync'd) 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 9469f657c96b..a4d6518e9af9 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -1492,7 +1492,6 @@ public void restore() throws IOException { // empty shard would cause exceptions to be thrown. Since there is no data to restore from an empty // shard anyway, we just create the empty shard here and then exit. IndexWriter writer = new IndexWriter(store.directory(), new IndexWriterConfig(null) - .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setOpenMode(IndexWriterConfig.OpenMode.CREATE) .setCommitOnClose(true)); writer.close(); diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java index 6acdbad2ccec..702d63d0d940 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java @@ -64,7 +64,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.Index; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.repositories.IndexId; @@ -121,8 +120,7 @@ public class RestoreService extends AbstractComponent implements ClusterStateApp SETTING_NUMBER_OF_SHARDS, SETTING_VERSION_CREATED, SETTING_INDEX_UUID, - SETTING_CREATION_DATE, - IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey())); + SETTING_CREATION_DATE)); // It's OK to change some settings, but we shouldn't allow simply removing them private static final Set UNREMOVABLE_SETTINGS; diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java b/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java index 9786c0eaf529..90173455c3be 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java @@ -392,7 +392,6 @@ public void testPrimaryReplicaResyncFailed() throws Exception { assertThat(shard.getLocalCheckpoint(), equalTo(numDocs + moreDocs)); } }, 30, TimeUnit.SECONDS); - internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); } } diff --git a/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java b/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java index 890f6ef163b3..753aedea01e0 100644 --- a/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java +++ b/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java @@ -33,23 +33,18 @@ import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.SegmentInfos; -import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; -import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TermQuery; -import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.Weight; import org.apache.lucene.store.Directory; import org.apache.lucene.store.MMapDirectory; import org.apache.lucene.store.MockDirectoryWrapper; import org.apache.lucene.util.Bits; -import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.test.ESTestCase; import java.io.IOException; -import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -58,8 +53,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; -import static org.hamcrest.Matchers.equalTo; - public class LuceneTests extends ESTestCase { public void testWaitForIndex() throws Exception { final MockDirectoryWrapper dir = newMockDirectory(); @@ -413,88 +406,4 @@ public void testMMapHackSupported() throws Exception { // add assume's here if needed for certain platforms, but we should know if it does not work. assertTrue("MMapDirectory does not support unmapping: " + MMapDirectory.UNMAP_NOT_SUPPORTED_REASON, MMapDirectory.UNMAP_SUPPORTED); } - - public void testWrapAllDocsLive() throws Exception { - Directory dir = newDirectory(); - IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) - .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())); - IndexWriter writer = new IndexWriter(dir, config); - int numDocs = between(1, 10); - Set liveDocs = new HashSet<>(); - for (int i = 0; i < numDocs; i++) { - String id = Integer.toString(i); - Document doc = new Document(); - doc.add(new StringField("id", id, Store.YES)); - writer.addDocument(doc); - liveDocs.add(id); - } - for (int i = 0; i < numDocs; i++) { - if (randomBoolean()) { - String id = Integer.toString(i); - Document doc = new Document(); - doc.add(new StringField("id", "v2-" + id, Store.YES)); - if (randomBoolean()) { - doc.add(Lucene.newSoftDeletesField()); - } - writer.softUpdateDocument(new Term("id", id), doc, Lucene.newSoftDeletesField()); - liveDocs.add("v2-" + id); - } - } - try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { - DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); - assertThat(reader.numDocs(), equalTo(liveDocs.size())); - IndexSearcher searcher = new IndexSearcher(reader); - Set actualDocs = new HashSet<>(); - TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); - for (ScoreDoc scoreDoc : topDocs.scoreDocs) { - actualDocs.add(reader.document(scoreDoc.doc).get("id")); - } - assertThat(actualDocs, equalTo(liveDocs)); - } - IOUtils.close(writer, dir); - } - - public void testWrapLiveDocsNotExposeAbortedDocuments() throws Exception { - Directory dir = newDirectory(); - IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) - .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())); - IndexWriter writer = new IndexWriter(dir, config); - int numDocs = between(1, 10); - List liveDocs = new ArrayList<>(); - for (int i = 0; i < numDocs; i++) { - String id = Integer.toString(i); - Document doc = new Document(); - doc.add(new StringField("id", id, Store.YES)); - if (randomBoolean()) { - doc.add(Lucene.newSoftDeletesField()); - } - writer.addDocument(doc); - liveDocs.add(id); - } - int abortedDocs = between(1, 10); - for (int i = 0; i < abortedDocs; i++) { - try { - Document doc = new Document(); - doc.add(new StringField("id", "aborted-" + i, Store.YES)); - StringReader reader = new StringReader(""); - doc.add(new TextField("other", reader)); - reader.close(); // mark the indexing hit non-aborting error - writer.addDocument(doc); - fail("index should have failed"); - } catch (Exception ignored) { } - } - try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { - DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); - assertThat(reader.maxDoc(), equalTo(numDocs + abortedDocs)); - assertThat(reader.numDocs(), equalTo(liveDocs.size())); - IndexSearcher searcher = new IndexSearcher(reader); - List actualDocs = new ArrayList<>(); - TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); - for (ScoreDoc scoreDoc : topDocs.scoreDocs) { - actualDocs.add(reader.document(scoreDoc.doc).get("id")); - } - assertThat(actualDocs, equalTo(liveDocs)); - } - IOUtils.close(writer, dir); - } } diff --git a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java index ac2f2b0d4f32..6bdd8ea3f2e0 100644 --- a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java +++ b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java @@ -109,7 +109,6 @@ public void setDisruptionScheme(ServiceDisruptionScheme scheme) { protected void beforeIndexDeletion() throws Exception { if (disableBeforeIndexDeletion == false) { super.beforeIndexDeletion(); - internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); assertSeqNos(); } } diff --git a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java index b0b6c35f92a1..d098c4918a76 100644 --- a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java @@ -40,7 +40,6 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardPath; -import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; @@ -398,8 +397,7 @@ public void testReuseInFileBasedPeerRecovery() throws Exception { .get(); logger.info("--> indexing docs"); - int numDocs = randomIntBetween(1, 1024); - for (int i = 0; i < numDocs; i++) { + for (int i = 0; i < randomIntBetween(1, 1024); i++) { client(primaryNode).prepareIndex("test", "type").setSource("field", "value").execute().actionGet(); } @@ -421,15 +419,12 @@ public void testReuseInFileBasedPeerRecovery() throws Exception { } logger.info("--> restart replica node"); - boolean softDeleteEnabled = internalCluster().getInstance(IndicesService.class, primaryNode) - .indexServiceSafe(resolveIndex("test")).getShard(0).indexSettings().isSoftDeleteEnabled(); - int moreDocs = randomIntBetween(1, 1024); internalCluster().restartNode(replicaNode, new RestartCallback() { @Override public Settings onNodeStopped(String nodeName) throws Exception { // index some more documents; we expect to reuse the files that already exist on the replica - for (int i = 0; i < moreDocs; i++) { + for (int i = 0; i < randomIntBetween(1, 1024); i++) { client(primaryNode).prepareIndex("test", "type").setSource("field", "value").execute().actionGet(); } @@ -437,12 +432,8 @@ public Settings onNodeStopped(String nodeName) throws Exception { client(primaryNode).admin().indices().prepareUpdateSettings("test").setSettings(Settings.builder() .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), "-1") .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1") - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) ).get(); client(primaryNode).admin().indices().prepareFlush("test").setForce(true).get(); - if (softDeleteEnabled) { // We need an extra flush to advance the min_retained_seqno of the SoftDeletesPolicy - client(primaryNode).admin().indices().prepareFlush("test").setForce(true).get(); - } return super.onNodeStopped(nodeName); } }); diff --git a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java index b0b4ec3930ad..28fa440d96ac 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java @@ -32,7 +32,6 @@ import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.shard.IndexShard; -import org.elasticsearch.index.shard.IndexShardTestCase; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -307,7 +306,7 @@ public void testAsyncTranslogTrimActuallyWorks() throws Exception { .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), -1)) .get(); IndexShard shard = indexService.getShard(0); - assertBusy(() -> assertThat(IndexShardTestCase.getTranslog(shard).totalOperations(), equalTo(0))); + assertBusy(() -> assertThat(shard.estimateTranslogOperationsFromMinSeq(0L), equalTo(0))); } public void testIllegalFsyncInterval() { diff --git a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java index 64a2fa69bcbd..b7da5add2acf 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java @@ -553,12 +553,4 @@ public void testQueryDefaultField() { ); assertThat(index.getDefaultFields(), equalTo(Arrays.asList("body", "title"))); } - - public void testUpdateSoftDeletesFails() { - IndexScopedSettings settings = new IndexScopedSettings(Settings.EMPTY, IndexScopedSettings.BUILT_IN_INDEX_SETTINGS); - IllegalArgumentException error = expectThrows(IllegalArgumentException.class, () -> - settings.updateSettings(Settings.builder().put("index.soft_deletes.enabled", randomBoolean()).build(), - Settings.builder(), Settings.builder(), "index")); - assertThat(error.getMessage(), equalTo("final index setting [index.soft_deletes.enabled], not updateable")); - } } diff --git a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java index 3f9fc9a0429b..ea7de50b7b34 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java @@ -51,24 +51,20 @@ public class CombinedDeletionPolicyTests extends ESTestCase { public void testKeepCommitsAfterGlobalCheckpoint() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); - final int extraRetainedOps = between(0, 100); - final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, extraRetainedOps); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); final LongArrayList maxSeqNoList = new LongArrayList(); final LongArrayList translogGenList = new LongArrayList(); final List commitList = new ArrayList<>(); int totalCommits = between(2, 20); long lastMaxSeqNo = 0; - long lastCheckpoint = lastMaxSeqNo; long lastTranslogGen = 0; final UUID translogUUID = UUID.randomUUID(); for (int i = 0; i < totalCommits; i++) { lastMaxSeqNo += between(1, 10000); - lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); lastTranslogGen += between(1, 100); - commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); maxSeqNoList.add(lastMaxSeqNo); translogGenList.add(lastTranslogGen); } @@ -89,19 +85,14 @@ public void testKeepCommitsAfterGlobalCheckpoint() throws Exception { } assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(translogGenList.get(keptIndex))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(commitList.get(keptIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } public void testAcquireIndexCommit() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); - final int extraRetainedOps = between(0, 100); - final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, extraRetainedOps); final UUID translogUUID = UUID.randomUUID(); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); long lastMaxSeqNo = between(1, 1000); - long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); long lastTranslogGen = between(1, 20); int safeIndex = 0; List commitList = new ArrayList<>(); @@ -111,9 +102,8 @@ public void testAcquireIndexCommit() throws Exception { int newCommits = between(1, 10); for (int n = 0; n < newCommits; n++) { lastMaxSeqNo += between(1, 1000); - lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); lastTranslogGen += between(1, 20); - commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); } // Advance the global checkpoint to between [safeIndex, safeIndex + 1) safeIndex = randomIntBetween(safeIndex, commitList.size() - 1); @@ -124,9 +114,6 @@ public void testAcquireIndexCommit() throws Exception { globalCheckpoint.set(randomLongBetween(lower, upper)); commitList.forEach(this::resetDeletion); indexPolicy.onCommit(commitList); - IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); // Captures and releases some commits int captures = between(0, 5); for (int n = 0; n < captures; n++) { @@ -145,7 +132,7 @@ public void testAcquireIndexCommit() throws Exception { snapshottingCommits.remove(snapshot); final long pendingSnapshots = snapshottingCommits.stream().filter(snapshot::equals).count(); final IndexCommit lastCommit = commitList.get(commitList.size() - 1); - safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); + final IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); assertThat(indexPolicy.releaseCommit(snapshot), equalTo(pendingSnapshots == 0 && snapshot.equals(lastCommit) == false && snapshot.equals(safeCommit) == false)); } @@ -156,8 +143,6 @@ public void testAcquireIndexCommit() throws Exception { equalTo(Long.parseLong(commitList.get(safeIndex).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(Long.parseLong(commitList.get(commitList.size() - 1).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(commitList.get(safeIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } snapshottingCommits.forEach(indexPolicy::releaseCommit); globalCheckpoint.set(randomLongBetween(lastMaxSeqNo, Long.MAX_VALUE)); @@ -169,27 +154,25 @@ public void testAcquireIndexCommit() throws Exception { assertThat(commitList.get(commitList.size() - 1).isDeleted(), equalTo(false)); assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(lastTranslogGen)); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); - IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } public void testLegacyIndex() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); - final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); final UUID translogUUID = UUID.randomUUID(); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); long legacyTranslogGen = randomNonNegativeLong(); IndexCommit legacyCommit = mockLegacyIndexCommit(translogUUID, legacyTranslogGen); - assertThat(CombinedDeletionPolicy.findSafeCommitPoint(singletonList(legacyCommit), globalCheckpoint.get()), - equalTo(legacyCommit)); + indexPolicy.onCommit(singletonList(legacyCommit)); + verify(legacyCommit, never()).delete(); + assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(legacyTranslogGen)); + assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(legacyTranslogGen)); long safeTranslogGen = randomLongBetween(legacyTranslogGen, Long.MAX_VALUE); long maxSeqNo = randomLongBetween(1, Long.MAX_VALUE); - final IndexCommit freshCommit = mockIndexCommit(randomLongBetween(-1, maxSeqNo), maxSeqNo, translogUUID, safeTranslogGen); + final IndexCommit freshCommit = mockIndexCommit(maxSeqNo, translogUUID, safeTranslogGen); globalCheckpoint.set(randomLongBetween(0, maxSeqNo - 1)); indexPolicy.onCommit(Arrays.asList(legacyCommit, freshCommit)); @@ -206,32 +189,25 @@ public void testLegacyIndex() throws Exception { verify(freshCommit, times(0)).delete(); assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(safeTranslogGen)); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(safeTranslogGen)); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), equalTo(getLocalCheckpoint(freshCommit) + 1)); } public void testDeleteInvalidCommits() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(randomNonNegativeLong()); - final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); final int invalidCommits = between(1, 10); final List commitList = new ArrayList<>(); for (int i = 0; i < invalidCommits; i++) { - long maxSeqNo = randomNonNegativeLong(); - commitList.add(mockIndexCommit(randomLongBetween(-1, maxSeqNo), maxSeqNo, UUID.randomUUID(), randomNonNegativeLong())); + commitList.add(mockIndexCommit(randomNonNegativeLong(), UUID.randomUUID(), randomNonNegativeLong())); } final UUID expectedTranslogUUID = UUID.randomUUID(); long lastTranslogGen = 0; final int validCommits = between(1, 10); - long lastMaxSeqNo = between(1, 1000); - long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); for (int i = 0; i < validCommits; i++) { lastTranslogGen += between(1, 1000); - lastMaxSeqNo += between(1, 1000); - lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); - commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, expectedTranslogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(randomNonNegativeLong(), expectedTranslogUUID, lastTranslogGen)); } // We should never keep invalid commits regardless of the value of the global checkpoint. @@ -239,26 +215,21 @@ public void testDeleteInvalidCommits() throws Exception { for (int i = 0; i < invalidCommits - 1; i++) { verify(commitList.get(i), times(1)).delete(); } - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(getLocalCheckpoint(CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get())) + 1)); } public void testCheckUnreferencedCommits() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.UNASSIGNED_SEQ_NO); - final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); final UUID translogUUID = UUID.randomUUID(); final TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); final List commitList = new ArrayList<>(); int totalCommits = between(2, 20); long lastMaxSeqNo = between(1, 1000); - long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); long lastTranslogGen = between(1, 50); for (int i = 0; i < totalCommits; i++) { lastMaxSeqNo += between(1, 10000); lastTranslogGen += between(1, 100); - lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); - commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); } IndexCommit safeCommit = randomFrom(commitList); globalCheckpoint.set(Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.MAX_SEQ_NO))); @@ -285,9 +256,8 @@ public void testCheckUnreferencedCommits() throws Exception { } } - IndexCommit mockIndexCommit(long localCheckpoint, long maxSeqNo, UUID translogUUID, long translogGen) throws IOException { + IndexCommit mockIndexCommit(long maxSeqNo, UUID translogUUID, long translogGen) throws IOException { final Map userData = new HashMap<>(); - userData.put(SequenceNumbers.LOCAL_CHECKPOINT_KEY, Long.toString(localCheckpoint)); userData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(maxSeqNo)); userData.put(Translog.TRANSLOG_UUID_KEY, translogUUID.toString()); userData.put(Translog.TRANSLOG_GENERATION_KEY, Long.toString(translogGen)); @@ -308,10 +278,6 @@ void resetDeletion(IndexCommit commit) { }).when(commit).delete(); } - private long getLocalCheckpoint(IndexCommit commit) throws IOException { - return Long.parseLong(commit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); - } - IndexCommit mockLegacyIndexCommit(UUID translogUUID, long translogGen) throws IOException { final Map userData = new HashMap<>(); userData.put(Translog.TRANSLOG_UUID_KEY, translogUUID.toString()); @@ -321,5 +287,4 @@ IndexCommit mockLegacyIndexCommit(UUID translogUUID, long translogGen) throws IO resetDeletion(commit); return commit; } - } 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 d3aead9e44e1..76e05ba1e0b5 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.engine; -import java.io.Closeable; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.Charset; @@ -78,12 +77,10 @@ import org.apache.lucene.index.LiveIndexWriterConfig; import org.apache.lucene.index.LogByteSizeMergePolicy; import org.apache.lucene.index.LogDocMergePolicy; -import org.apache.lucene.index.MergePolicy; import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.index.NumericDocValues; import org.apache.lucene.index.PointValues; import org.apache.lucene.index.SegmentInfos; -import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.index.TieredMergePolicy; import org.apache.lucene.search.IndexSearcher; @@ -117,7 +114,6 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.Loggers; -import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver; @@ -137,7 +133,6 @@ import org.elasticsearch.index.mapper.ContentPath; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.Mapper.BuilderContext; -import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.MetadataFieldMapper; import org.elasticsearch.index.mapper.ParseContext; @@ -177,10 +172,8 @@ import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -254,13 +247,8 @@ public void testVersionMapAfterAutoIDDocument() throws IOException { } public void testSegments() throws Exception { - Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); - IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( - IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { + InternalEngine engine = createEngine(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE)) { List segments = engine.segments(false); assertThat(segments.isEmpty(), equalTo(true)); assertThat(engine.segmentsStats(false).getCount(), equalTo(0L)); @@ -1323,13 +1311,9 @@ public void testVersioningNewIndex() throws IOException { assertThat(indexResult.getVersion(), equalTo(1L)); } - public void testForceMergeWithoutSoftDeletes() throws IOException { - Settings settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); - IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + public void testForceMerge() throws IOException { try (Store store = createStore(); - Engine engine = createEngine(config(IndexSettingsModule.newIndexSettings(indexMetaData), store, createTempDir(), + Engine engine = createEngine(config(defaultSettings, store, createTempDir(), new LogByteSizeMergePolicy(), null))) { // use log MP here we test some behavior in ESMP int numDocs = randomIntBetween(10, 100); for (int i = 0; i < numDocs; i++) { @@ -1370,165 +1354,6 @@ public void testForceMergeWithoutSoftDeletes() throws IOException { } } - public void testForceMergeWithSoftDeletesRetention() throws Exception { - final long retainedExtraOps = randomLongBetween(0, 10); - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); - final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); - final MapperService mapperService = createMapperService("test"); - final Set liveDocs = new HashSet<>(); - try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get))) { - int numDocs = scaledRandomIntBetween(10, 100); - for (int i = 0; i < numDocs; i++) { - ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null); - engine.index(indexForDoc(doc)); - liveDocs.add(doc.id()); - } - for (int i = 0; i < numDocs; i++) { - ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null); - if (randomBoolean()) { - engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); - liveDocs.remove(doc.id()); - } - if (randomBoolean()) { - engine.index(indexForDoc(doc)); - liveDocs.add(doc.id()); - } - if (randomBoolean()) { - engine.flush(randomBoolean(), true); - } - } - engine.flush(); - - long localCheckpoint = engine.getLocalCheckpoint(); - globalCheckpoint.set(randomLongBetween(0, localCheckpoint)); - engine.syncTranslog(); - final long safeCommitCheckpoint; - try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { - safeCommitCheckpoint = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); - } - engine.forceMerge(true, 1, false, false, false); - assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); - Map ops = readAllOperationsInLucene(engine, mapperService) - .stream().collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); - for (long seqno = 0; seqno <= localCheckpoint; seqno++) { - long minSeqNoToRetain = Math.min(globalCheckpoint.get() + 1 - retainedExtraOps, safeCommitCheckpoint + 1); - String msg = "seq# [" + seqno + "], global checkpoint [" + globalCheckpoint + "], retained-ops [" + retainedExtraOps + "]"; - if (seqno < minSeqNoToRetain) { - Translog.Operation op = ops.get(seqno); - if (op != null) { - assertThat(op, instanceOf(Translog.Index.class)); - assertThat(msg, ((Translog.Index) op).id(), isIn(liveDocs)); - assertEquals(msg, ((Translog.Index) op).source(), B_1); - } - } else { - assertThat(msg, ops.get(seqno), notNullValue()); - } - } - settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); - indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - engine.onSettingsChanged(); - globalCheckpoint.set(localCheckpoint); - engine.syncTranslog(); - - engine.forceMerge(true, 1, false, false, false); - assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); - assertThat(readAllOperationsInLucene(engine, mapperService), hasSize(liveDocs.size())); - } - } - - public void testForceMergeWithSoftDeletesRetentionAndRecoverySource() throws Exception { - final long retainedExtraOps = randomLongBetween(0, 10); - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); - final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); - final MapperService mapperService = createMapperService("test"); - final boolean omitSourceAllTheTime = randomBoolean(); - final Set liveDocs = new HashSet<>(); - final Set liveDocsWithSource = new HashSet<>(); - try (Store store = createStore(); - InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, - globalCheckpoint::get))) { - int numDocs = scaledRandomIntBetween(10, 100); - for (int i = 0; i < numDocs; i++) { - boolean useRecoverySource = randomBoolean() || omitSourceAllTheTime; - ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null, useRecoverySource); - engine.index(indexForDoc(doc)); - liveDocs.add(doc.id()); - if (useRecoverySource == false) { - liveDocsWithSource.add(Integer.toString(i)); - } - } - for (int i = 0; i < numDocs; i++) { - boolean useRecoverySource = randomBoolean() || omitSourceAllTheTime; - ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null, useRecoverySource); - if (randomBoolean()) { - engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); - liveDocs.remove(doc.id()); - liveDocsWithSource.remove(doc.id()); - } - if (randomBoolean()) { - engine.index(indexForDoc(doc)); - liveDocs.add(doc.id()); - if (useRecoverySource == false) { - liveDocsWithSource.add(doc.id()); - } else { - liveDocsWithSource.remove(doc.id()); - } - } - if (randomBoolean()) { - engine.flush(randomBoolean(), true); - } - } - engine.flush(); - globalCheckpoint.set(randomLongBetween(0, engine.getLocalCheckpoint())); - engine.syncTranslog(); - final long minSeqNoToRetain; - try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { - long safeCommitLocalCheckpoint = Long.parseLong( - safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); - minSeqNoToRetain = Math.min(globalCheckpoint.get() + 1 - retainedExtraOps, safeCommitLocalCheckpoint + 1); - } - engine.forceMerge(true, 1, false, false, false); - assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); - Map ops = readAllOperationsInLucene(engine, mapperService) - .stream().collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); - for (long seqno = 0; seqno <= engine.getLocalCheckpoint(); seqno++) { - String msg = "seq# [" + seqno + "], global checkpoint [" + globalCheckpoint + "], retained-ops [" + retainedExtraOps + "]"; - if (seqno < minSeqNoToRetain) { - Translog.Operation op = ops.get(seqno); - if (op != null) { - assertThat(op, instanceOf(Translog.Index.class)); - assertThat(msg, ((Translog.Index) op).id(), isIn(liveDocs)); - } - } else { - Translog.Operation op = ops.get(seqno); - assertThat(msg, op, notNullValue()); - if (op instanceof Translog.Index) { - assertEquals(msg, ((Translog.Index) op).source(), B_1); - } - } - } - settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); - indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - engine.onSettingsChanged(); - globalCheckpoint.set(engine.getLocalCheckpoint()); - engine.syncTranslog(); - engine.forceMerge(true, 1, false, false, false); - assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); - assertThat(readAllOperationsInLucene(engine, mapperService), hasSize(liveDocsWithSource.size())); - } - } - public void testForceMergeAndClose() throws IOException, InterruptedException { int numIters = randomIntBetween(2, 10); for (int j = 0; j < numIters; j++) { @@ -1597,10 +1422,126 @@ public void testVersioningCreateExistsException() throws IOException { assertThat(indexResult.getFailure(), instanceOf(VersionConflictEngineException.class)); } + protected List generateSingleDocHistory(boolean forReplica, VersionType versionType, + long primaryTerm, + int minOpCount, int maxOpCount, String docId) { + final int numOfOps = randomIntBetween(minOpCount, maxOpCount); + final List ops = new ArrayList<>(); + final Term id = newUid(docId); + final int startWithSeqNo = 0; + final String valuePrefix = (forReplica ? "r_" : "p_" ) + docId + "_"; + final boolean incrementTermWhenIntroducingSeqNo = randomBoolean(); + for (int i = 0; i < numOfOps; i++) { + final Engine.Operation op; + final long version; + switch (versionType) { + case INTERNAL: + version = forReplica ? i : Versions.MATCH_ANY; + break; + case EXTERNAL: + version = i; + break; + case EXTERNAL_GTE: + version = randomBoolean() ? Math.max(i - 1, 0) : i; + break; + case FORCE: + version = randomNonNegativeLong(); + break; + default: + throw new UnsupportedOperationException("unknown version type: " + versionType); + } + if (randomBoolean()) { + op = new Engine.Index(id, testParsedDocument(docId, null, testDocumentWithTextField(valuePrefix + i), B_1, null), + forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, + forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, + version, + forReplica ? null : versionType, + forReplica ? REPLICA : PRIMARY, + System.currentTimeMillis(), -1, false + ); + } else { + op = new Engine.Delete("test", docId, id, + forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, + forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, + version, + forReplica ? null : versionType, + forReplica ? REPLICA : PRIMARY, + System.currentTimeMillis()); + } + ops.add(op); + } + return ops; + } + public void testOutOfOrderDocsOnReplica() throws IOException { final List ops = generateSingleDocHistory(true, randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL, VersionType.EXTERNAL_GTE, VersionType.FORCE), 2, 2, 20, "1"); - assertOpsOnReplica(ops, replicaEngine, true, logger); + assertOpsOnReplica(ops, replicaEngine, true); + } + + private void assertOpsOnReplica(List ops, InternalEngine replicaEngine, boolean shuffleOps) throws IOException { + final Engine.Operation lastOp = ops.get(ops.size() - 1); + final String lastFieldValue; + if (lastOp instanceof Engine.Index) { + Engine.Index index = (Engine.Index) lastOp; + lastFieldValue = index.docs().get(0).get("value"); + } else { + // delete + lastFieldValue = null; + } + if (shuffleOps) { + int firstOpWithSeqNo = 0; + while (firstOpWithSeqNo < ops.size() && ops.get(firstOpWithSeqNo).seqNo() < 0) { + firstOpWithSeqNo++; + } + // shuffle ops but make sure legacy ops are first + shuffle(ops.subList(0, firstOpWithSeqNo), random()); + shuffle(ops.subList(firstOpWithSeqNo, ops.size()), random()); + } + boolean firstOp = true; + for (Engine.Operation op : ops) { + logger.info("performing [{}], v [{}], seq# [{}], term [{}]", + op.operationType().name().charAt(0), op.version(), op.seqNo(), op.primaryTerm()); + if (op instanceof Engine.Index) { + Engine.IndexResult result = replicaEngine.index((Engine.Index) op); + // replicas don't really care to about creation status of documents + // this allows to ignore the case where a document was found in the live version maps in + // a delete state and return false for the created flag in favor of code simplicity + // as deleted or not. This check is just signal regression so a decision can be made if it's + // intentional + assertThat(result.isCreated(), equalTo(firstOp)); + assertThat(result.getVersion(), equalTo(op.version())); + assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); + + } else { + Engine.DeleteResult result = replicaEngine.delete((Engine.Delete) op); + // Replicas don't really care to about found status of documents + // this allows to ignore the case where a document was found in the live version maps in + // a delete state and return true for the found flag in favor of code simplicity + // his check is just signal regression so a decision can be made if it's + // intentional + assertThat(result.isFound(), equalTo(firstOp == false)); + assertThat(result.getVersion(), equalTo(op.version())); + assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); + } + if (randomBoolean()) { + engine.refresh("test"); + } + if (randomBoolean()) { + engine.flush(); + engine.refresh("test"); + } + firstOp = false; + } + + assertVisibleCount(replicaEngine, lastFieldValue == null ? 0 : 1); + if (lastFieldValue != null) { + try (Searcher searcher = replicaEngine.acquireSearcher("test")) { + final TotalHitCountCollector collector = new TotalHitCountCollector(); + searcher.searcher().search(new TermQuery(new Term("value", lastFieldValue)), collector); + assertThat(collector.getTotalHits(), equalTo(1)); + } + } } public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, InterruptedException { @@ -1628,12 +1569,11 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup } // randomly interleave final AtomicLong seqNoGenerator = new AtomicLong(); - BiFunction seqNoUpdater = (operation, newSeqNo) -> { + Function seqNoUpdater = operation -> { + final long newSeqNo = seqNoGenerator.getAndIncrement(); if (operation instanceof Engine.Index) { Engine.Index index = (Engine.Index) operation; - Document doc = testDocumentWithTextField(index.docs().get(0).get("value")); - ParsedDocument parsedDocument = testParsedDocument(index.id(), index.routing(), doc, index.source(), null); - return new Engine.Index(index.uid(), parsedDocument, newSeqNo, index.primaryTerm(), index.version(), + return new Engine.Index(index.uid(), index.parsedDoc(), newSeqNo, index.primaryTerm(), index.version(), index.versionType(), index.origin(), index.startTime(), index.getAutoGeneratedIdTimestamp(), index.isRetry()); } else { Engine.Delete delete = (Engine.Delete) operation; @@ -1646,12 +1586,12 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup Iterator iter2 = opsDoc2.iterator(); while (iter1.hasNext() && iter2.hasNext()) { final Engine.Operation next = randomBoolean() ? iter1.next() : iter2.next(); - allOps.add(seqNoUpdater.apply(next, seqNoGenerator.getAndIncrement())); + allOps.add(seqNoUpdater.apply(next)); } - iter1.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o, seqNoGenerator.getAndIncrement()))); - iter2.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o, seqNoGenerator.getAndIncrement()))); + iter1.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o))); + iter2.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o))); // insert some duplicates - randomSubsetOf(allOps).forEach(op -> allOps.add(seqNoUpdater.apply(op, op.seqNo()))); + allOps.addAll(randomSubsetOf(allOps)); shuffle(allOps, random()); concurrentlyApplyOps(allOps, engine); @@ -1683,6 +1623,42 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup assertVisibleCount(engine, totalExpectedOps); } + private void concurrentlyApplyOps(List ops, InternalEngine engine) throws InterruptedException { + Thread[] thread = new Thread[randomIntBetween(3, 5)]; + CountDownLatch startGun = new CountDownLatch(thread.length); + AtomicInteger offset = new AtomicInteger(-1); + for (int i = 0; i < thread.length; i++) { + thread[i] = new Thread(() -> { + startGun.countDown(); + try { + startGun.await(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + int docOffset; + while ((docOffset = offset.incrementAndGet()) < ops.size()) { + try { + final Engine.Operation op = ops.get(docOffset); + if (op instanceof Engine.Index) { + engine.index((Engine.Index) op); + } else { + engine.delete((Engine.Delete) op); + } + if ((docOffset + 1) % 4 == 0) { + engine.refresh("test"); + } + } catch (IOException e) { + throw new AssertionError(e); + } + } + }); + thread[i].start(); + } + for (int i = 0; i < thread.length; i++) { + thread[i].join(); + } + } + public void testInternalVersioningOnPrimary() throws IOException { final List ops = generateSingleDocHistory(false, VersionType.INTERNAL, 2, 2, 20, "1"); assertOpsOnPrimary(ops, Versions.NOT_FOUND, true, engine); @@ -1893,7 +1869,7 @@ public void testVersioningPromotedReplica() throws IOException { final boolean deletedOnReplica = lastReplicaOp instanceof Engine.Delete; final long finalReplicaVersion = lastReplicaOp.version(); final long finalReplicaSeqNo = lastReplicaOp.seqNo(); - assertOpsOnReplica(replicaOps, replicaEngine, true, logger); + assertOpsOnReplica(replicaOps, replicaEngine, true); final int opsOnPrimary = assertOpsOnPrimary(primaryOps, finalReplicaVersion, deletedOnReplica, replicaEngine); final long currentSeqNo = getSequenceID(replicaEngine, new Engine.Get(false, false, "type", lastReplicaOp.uid().text(), lastReplicaOp.uid())).v1(); @@ -2698,16 +2674,14 @@ public void testSkipTranslogReplay() throws IOException { Engine.IndexResult indexResult = engine.index(firstIndexRequest); assertThat(indexResult.getVersion(), equalTo(1L)); } - EngineConfig config = engine.config(); assertVisibleCount(engine, numDocs); engine.close(); - trimUnsafeCommits(config); - try (InternalEngine engine = new InternalEngine(config)) { - engine.skipTranslogRecovery(); - try (Engine.Searcher searcher = engine.acquireSearcher("test")) { - TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), randomIntBetween(numDocs, numDocs + 10)); - assertThat(topDocs.totalHits, equalTo(0L)); - } + trimUnsafeCommits(engine.config()); + engine = new InternalEngine(engine.config()); + engine.skipTranslogRecovery(); + try (Engine.Searcher searcher = engine.acquireSearcher("test")) { + TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), randomIntBetween(numDocs, numDocs + 10)); + assertThat(topDocs.totalHits, equalTo(0L)); } } @@ -2837,7 +2811,7 @@ public void testRecoverFromForeignTranslog() throws IOException { new CodecService(null, logger), config.getEventListener(), IndexSearcher.getDefaultQueryCache(), IndexSearcher.getDefaultQueryCachingPolicy(), translogConfig, TimeValue.timeValueMinutes(5), config.getExternalRefreshListener(), config.getInternalRefreshListener(), null, config.getTranslogRecoveryRunner(), - new NoneCircuitBreakerService(), () -> SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm::get, tombstoneDocSupplier()); + new NoneCircuitBreakerService(), () -> SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm::get); try { InternalEngine internalEngine = new InternalEngine(brokenConfig); fail("translog belongs to a different engine"); @@ -2966,12 +2940,6 @@ private void maybeThrowFailure() throws IOException { } } - @Override - public long softUpdateDocument(Term term, Iterable doc, Field... softDeletes) throws IOException { - maybeThrowFailure(); - return super.softUpdateDocument(term, doc, softDeletes); - } - @Override public long deleteDocuments(Term... terms) throws IOException { maybeThrowFailure(); @@ -3172,10 +3140,10 @@ public void testDoubleDeliveryReplicaAppendingAndDeleteOnly() throws IOException } public void testDoubleDeliveryReplicaAppendingOnly() throws IOException { - final Supplier doc = () -> testParsedDocument("1", null, testDocumentWithTextField(), + final ParsedDocument doc = testParsedDocument("1", null, testDocumentWithTextField(), new BytesArray("{}".getBytes(Charset.defaultCharset())), null); - Engine.Index operation = appendOnlyReplica(doc.get(), false, 1, randomIntBetween(0, 5)); - Engine.Index retry = appendOnlyReplica(doc.get(), true, 1, randomIntBetween(0, 5)); + Engine.Index operation = appendOnlyReplica(doc, false, 1, randomIntBetween(0, 5)); + Engine.Index retry = appendOnlyReplica(doc, true, 1, randomIntBetween(0, 5)); // operations with a seq# equal or lower to the local checkpoint are not indexed to lucene // and the version lookup is skipped final boolean belowLckp = operation.seqNo() == 0 && retry.seqNo() == 0; @@ -3214,8 +3182,8 @@ public void testDoubleDeliveryReplicaAppendingOnly() throws IOException { TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), 10); assertEquals(1, topDocs.totalHits); } - operation = randomAppendOnly(doc.get(), false, 1); - retry = randomAppendOnly(doc.get(), true, 1); + operation = randomAppendOnly(doc, false, 1); + retry = randomAppendOnly(doc, true, 1); if (randomBoolean()) { Engine.IndexResult indexResult = engine.index(operation); assertNotNull(indexResult.getTranslogLocation()); @@ -3280,8 +3248,6 @@ public void testDoubleDeliveryReplica() throws IOException { TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), 10); assertEquals(1, topDocs.totalHits); } - List ops = readAllOperationsInLucene(engine, createMapperService("test")); - assertThat(ops.stream().map(o -> o.seqNo()).collect(Collectors.toList()), hasItem(20L)); } public void testRetryWithAutogeneratedIdWorksAndNoDuplicateDocs() throws IOException { @@ -3750,22 +3716,20 @@ public void testOutOfOrderSequenceNumbersWithVersionConflict() throws IOExceptio final List operations = new ArrayList<>(); final int numberOfOperations = randomIntBetween(16, 32); + final Document document = testDocumentWithTextField(); final AtomicLong sequenceNumber = new AtomicLong(); final Engine.Operation.Origin origin = randomFrom(LOCAL_TRANSLOG_RECOVERY, PEER_RECOVERY, PRIMARY, REPLICA); final LongSupplier sequenceNumberSupplier = origin == PRIMARY ? () -> SequenceNumbers.UNASSIGNED_SEQ_NO : sequenceNumber::getAndIncrement; - final Supplier doc = () -> { - final Document document = testDocumentWithTextField(); - document.add(new Field(SourceFieldMapper.NAME, BytesReference.toBytes(B_1), SourceFieldMapper.Defaults.FIELD_TYPE)); - return testParsedDocument("1", null, document, B_1, null); - }; - final Term uid = newUid("1"); + document.add(new Field(SourceFieldMapper.NAME, BytesReference.toBytes(B_1), SourceFieldMapper.Defaults.FIELD_TYPE)); + final ParsedDocument doc = testParsedDocument("1", null, document, B_1, null); + final Term uid = newUid(doc); final BiFunction searcherFactory = engine::acquireSearcher; for (int i = 0; i < numberOfOperations; i++) { if (randomBoolean()) { final Engine.Index index = new Engine.Index( uid, - doc.get(), + doc, sequenceNumberSupplier.getAsLong(), 1, i, @@ -3841,9 +3805,7 @@ public void testNoOps() throws IOException { maxSeqNo, localCheckpoint); trimUnsafeCommits(engine.config()); - EngineConfig noopEngineConfig = copy(engine.config(), new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, - () -> new MatchAllDocsQuery(), engine.config().getMergePolicy())); - noOpEngine = new InternalEngine(noopEngineConfig, supplier) { + noOpEngine = new InternalEngine(engine.config(), supplier) { @Override protected long doGenerateSeqNoForOperation(Operation operation) { throw new UnsupportedOperationException(); @@ -3851,7 +3813,7 @@ protected long doGenerateSeqNoForOperation(Operation operation) { }; noOpEngine.recoverFromTranslog(Long.MAX_VALUE); final int gapsFilled = noOpEngine.fillSeqNoGaps(primaryTerm.get()); - final String reason = "filling gaps"; + final String reason = randomAlphaOfLength(16); noOpEngine.noOp(new Engine.NoOp(maxSeqNo + 1, primaryTerm.get(), LOCAL_TRANSLOG_RECOVERY, System.nanoTime(), reason)); assertThat(noOpEngine.getLocalCheckpoint(), equalTo((long) (maxSeqNo + 1))); assertThat(noOpEngine.getTranslog().stats().getUncommittedOperations(), equalTo(gapsFilled)); @@ -3873,77 +3835,11 @@ protected long doGenerateSeqNoForOperation(Operation operation) { assertThat(noOp.seqNo(), equalTo((long) (maxSeqNo + 2))); assertThat(noOp.primaryTerm(), equalTo(primaryTerm.get())); assertThat(noOp.reason(), equalTo(reason)); - if (engine.engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - MapperService mapperService = createMapperService("test"); - List operationsFromLucene = readAllOperationsInLucene(noOpEngine, mapperService); - assertThat(operationsFromLucene, hasSize(maxSeqNo + 2 - localCheckpoint)); // fills n gap and 2 manual noop. - for (int i = 0; i < operationsFromLucene.size(); i++) { - assertThat(operationsFromLucene.get(i), equalTo(new Translog.NoOp(localCheckpoint + 1 + i, primaryTerm.get(), "filling gaps"))); - } - assertConsistentHistoryBetweenTranslogAndLuceneIndex(noOpEngine, mapperService); - } } finally { IOUtils.close(noOpEngine); } } - /** - * Verifies that a segment containing only no-ops can be used to look up _version and _seqno. - */ - public void testSegmentContainsOnlyNoOps() throws Exception { - Engine.NoOpResult noOpResult = engine.noOp(new Engine.NoOp(1, primaryTerm.get(), - randomFrom(Engine.Operation.Origin.values()), randomNonNegativeLong(), "test")); - assertThat(noOpResult.getFailure(), nullValue()); - engine.refresh("test"); - Engine.DeleteResult deleteResult = engine.delete(replicaDeleteForDoc("id", 1, 2, randomNonNegativeLong())); - assertThat(deleteResult.getFailure(), nullValue()); - engine.refresh("test"); - } - - /** - * A simple test to check that random combination of operations can coexist in segments and be lookup. - * This is needed as some fields in Lucene may not exist if a segment misses operation types and this code is to check for that. - * For example, a segment containing only no-ops does not have neither _uid or _version. - */ - public void testRandomOperations() throws Exception { - int numOps = between(10, 100); - for (int i = 0; i < numOps; i++) { - String id = Integer.toString(randomIntBetween(1, 10)); - ParsedDocument doc = createParsedDoc(id, null); - Engine.Operation.TYPE type = randomFrom(Engine.Operation.TYPE.values()); - switch (type) { - case INDEX: - Engine.IndexResult index = engine.index(replicaIndexForDoc(doc, between(1, 100), i, randomBoolean())); - assertThat(index.getFailure(), nullValue()); - break; - case DELETE: - Engine.DeleteResult delete = engine.delete(replicaDeleteForDoc(doc.id(), between(1, 100), i, randomNonNegativeLong())); - assertThat(delete.getFailure(), nullValue()); - break; - case NO_OP: - Engine.NoOpResult noOp = engine.noOp(new Engine.NoOp(i, primaryTerm.get(), - randomFrom(Engine.Operation.Origin.values()), randomNonNegativeLong(), "")); - assertThat(noOp.getFailure(), nullValue()); - break; - default: - throw new IllegalStateException("Invalid op [" + type + "]"); - } - if (randomBoolean()) { - engine.refresh("test"); - } - if (randomBoolean()) { - engine.flush(); - } - if (randomBoolean()) { - engine.forceMerge(randomBoolean(), between(1, 10), randomBoolean(), false, false); - } - } - if (engine.engineConfig.getIndexSettings().isSoftDeleteEnabled()) { - List operations = readAllOperationsInLucene(engine, createMapperService("test")); - assertThat(operations, hasSize(numOps)); - } - } - public void testMinGenerationForSeqNo() throws IOException, BrokenBarrierException, InterruptedException { engine.close(); final int numberOfTriplets = randomIntBetween(1, 32); @@ -4509,7 +4405,7 @@ public void testCleanUpCommitsWhenGlobalCheckpointAdvanced() throws Exception { globalCheckpoint.set(randomLongBetween(engine.getLocalCheckpoint(), Long.MAX_VALUE)); engine.syncTranslog(); assertThat(DirectoryReader.listCommits(store.directory()), contains(commits.get(commits.size() - 1))); - assertThat(engine.getTranslog().totalOperations(), equalTo(0)); + assertThat(engine.estimateTranslogOperationsFromMinSeq(0L), equalTo(0)); } } @@ -4872,154 +4768,6 @@ public void testTrimUnsafeCommits() throws Exception { } } - public void testLuceneHistoryOnPrimary() throws Exception { - final List operations = generateSingleDocHistory(false, - randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "1"); - assertOperationHistoryInLucene(operations); - } - - public void testLuceneHistoryOnReplica() throws Exception { - final List operations = generateSingleDocHistory(true, - randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "2"); - Randomness.shuffle(operations); - assertOperationHistoryInLucene(operations); - } - - private void assertOperationHistoryInLucene(List operations) throws IOException { - final MergePolicy keepSoftDeleteDocsMP = new SoftDeletesRetentionMergePolicy( - Lucene.SOFT_DELETES_FIELD, () -> new MatchAllDocsQuery(), engine.config().getMergePolicy()); - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); - Set expectedSeqNos = new HashSet<>(); - try (Store store = createStore(); - Engine engine = createEngine(config(indexSettings, store, createTempDir(), keepSoftDeleteDocsMP, null))) { - for (Engine.Operation op : operations) { - if (op instanceof Engine.Index) { - Engine.IndexResult indexResult = engine.index((Engine.Index) op); - assertThat(indexResult.getFailure(), nullValue()); - expectedSeqNos.add(indexResult.getSeqNo()); - } else { - Engine.DeleteResult deleteResult = engine.delete((Engine.Delete) op); - assertThat(deleteResult.getFailure(), nullValue()); - expectedSeqNos.add(deleteResult.getSeqNo()); - } - if (rarely()) { - engine.refresh("test"); - } - if (rarely()) { - engine.flush(); - } - if (rarely()) { - engine.forceMerge(true); - } - } - MapperService mapperService = createMapperService("test"); - List actualOps = readAllOperationsInLucene(engine, mapperService); - assertThat(actualOps.stream().map(o -> o.seqNo()).collect(Collectors.toList()), containsInAnyOrder(expectedSeqNos.toArray())); - assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); - } - } - - public void testKeepMinRetainedSeqNoByMergePolicy() throws IOException { - IOUtils.close(engine, store); - Settings.Builder settings = Settings.builder() - .put(defaultSettings.getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); - final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); - final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); - final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); - final List operations = generateSingleDocHistory(true, - randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "2"); - Randomness.shuffle(operations); - Set existingSeqNos = new HashSet<>(); - store = createStore(); - engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get)); - assertThat(engine.getMinRetainedSeqNo(), equalTo(0L)); - long lastMinRetainedSeqNo = engine.getMinRetainedSeqNo(); - for (Engine.Operation op : operations) { - final Engine.Result result; - if (op instanceof Engine.Index) { - result = engine.index((Engine.Index) op); - } else { - result = engine.delete((Engine.Delete) op); - } - existingSeqNos.add(result.getSeqNo()); - if (randomBoolean()) { - globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getLocalCheckpointTracker().getCheckpoint())); - } - if (rarely()) { - settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); - indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); - engine.onSettingsChanged(); - } - if (rarely()) { - engine.refresh("test"); - } - if (rarely()) { - engine.flush(true, true); - assertThat(Long.parseLong(engine.getLastCommittedSegmentInfos().userData.get(Engine.MIN_RETAINED_SEQNO)), - equalTo(engine.getMinRetainedSeqNo())); - } - if (rarely()) { - engine.forceMerge(randomBoolean()); - } - try (Closeable ignored = engine.acquireRetentionLockForPeerRecovery()) { - long minRetainSeqNos = engine.getMinRetainedSeqNo(); - assertThat(minRetainSeqNos, lessThanOrEqualTo(globalCheckpoint.get() + 1)); - Long[] expectedOps = existingSeqNos.stream().filter(seqno -> seqno >= minRetainSeqNos).toArray(Long[]::new); - Set actualOps = readAllOperationsInLucene(engine, createMapperService("test")).stream() - .map(Translog.Operation::seqNo).collect(Collectors.toSet()); - assertThat(actualOps, containsInAnyOrder(expectedOps)); - } - try (Engine.IndexCommitRef commitRef = engine.acquireSafeIndexCommit()) { - IndexCommit safeCommit = commitRef.getIndexCommit(); - if (safeCommit.getUserData().containsKey(Engine.MIN_RETAINED_SEQNO)) { - lastMinRetainedSeqNo = Long.parseLong(safeCommit.getUserData().get(Engine.MIN_RETAINED_SEQNO)); - } - } - } - if (randomBoolean()) { - engine.close(); - } else { - engine.flushAndClose(); - } - trimUnsafeCommits(engine.config()); - try (InternalEngine recoveringEngine = new InternalEngine(engine.config())) { - assertThat(recoveringEngine.getMinRetainedSeqNo(), equalTo(lastMinRetainedSeqNo)); - } - } - - public void testLastRefreshCheckpoint() throws Exception { - AtomicBoolean done = new AtomicBoolean(); - Thread[] refreshThreads = new Thread[between(1, 8)]; - CountDownLatch latch = new CountDownLatch(refreshThreads.length); - for (int i = 0; i < refreshThreads.length; i++) { - latch.countDown(); - refreshThreads[i] = new Thread(() -> { - while (done.get() == false) { - long checkPointBeforeRefresh = engine.getLocalCheckpoint(); - engine.refresh("test", randomFrom(Engine.SearcherScope.values())); - assertThat(engine.lastRefreshedCheckpoint(), greaterThanOrEqualTo(checkPointBeforeRefresh)); - } - }); - refreshThreads[i].start(); - } - latch.await(); - List ops = generateSingleDocHistory(true, VersionType.EXTERNAL, 1, 10, 1000, "1"); - concurrentlyApplyOps(ops, engine); - done.set(true); - for (Thread thread : refreshThreads) { - thread.join(); - } - engine.refresh("test"); - assertThat(engine.lastRefreshedCheckpoint(), equalTo(engine.getLocalCheckpoint())); - } - private static void trimUnsafeCommits(EngineConfig config) throws IOException { final Store store = config.getStore(); final TranslogConfig translogConfig = config.getTranslogConfig(); diff --git a/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java b/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java deleted file mode 100644 index 2d097366a272..000000000000 --- a/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java +++ /dev/null @@ -1,289 +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.index.engine; - -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.internal.io.IOUtils; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.VersionType; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.mapper.ParsedDocument; -import org.elasticsearch.index.store.Store; -import org.elasticsearch.index.translog.SnapshotMatchers; -import org.elasticsearch.index.translog.Translog; -import org.elasticsearch.test.IndexSettingsModule; -import org.junit.Before; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; - -public class LuceneChangesSnapshotTests extends EngineTestCase { - private MapperService mapperService; - - @Before - public void createMapper() throws Exception { - mapperService = createMapperService("test"); - } - - @Override - protected Settings indexSettings() { - return Settings.builder().put(super.indexSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) // always enable soft-deletes - .build(); - } - - public void testBasics() throws Exception { - long fromSeqNo = randomNonNegativeLong(); - long toSeqNo = randomLongBetween(fromSeqNo, Long.MAX_VALUE); - // Empty engine - try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, true)) { - IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); - assertThat(error.getMessage(), - containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); - } - try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, false)) { - assertThat(snapshot, SnapshotMatchers.size(0)); - } - int numOps = between(1, 100); - int refreshedSeqNo = -1; - for (int i = 0; i < numOps; i++) { - String id = Integer.toString(randomIntBetween(i, i + 5)); - ParsedDocument doc = createParsedDoc(id, null, randomBoolean()); - if (randomBoolean()) { - engine.index(indexForDoc(doc)); - } else { - engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); - } - if (rarely()) { - if (randomBoolean()) { - engine.flush(); - } else { - engine.refresh("test"); - } - refreshedSeqNo = i; - } - } - if (refreshedSeqNo == -1) { - fromSeqNo = between(0, numOps); - toSeqNo = randomLongBetween(fromSeqNo, numOps * 2); - - Engine.Searcher searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); - try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( - searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, false)) { - searcher = null; - assertThat(snapshot, SnapshotMatchers.size(0)); - } finally { - IOUtils.close(searcher); - } - - searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); - try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( - searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { - searcher = null; - IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); - assertThat(error.getMessage(), - containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); - }finally { - IOUtils.close(searcher); - } - } else { - fromSeqNo = randomLongBetween(0, refreshedSeqNo); - toSeqNo = randomLongBetween(refreshedSeqNo + 1, numOps * 2); - Engine.Searcher searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); - try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( - searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, false)) { - searcher = null; - assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, refreshedSeqNo)); - } finally { - IOUtils.close(searcher); - } - searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); - try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( - searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { - searcher = null; - IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); - assertThat(error.getMessage(), - containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); - }finally { - IOUtils.close(searcher); - } - toSeqNo = randomLongBetween(fromSeqNo, refreshedSeqNo); - searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); - try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( - searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { - searcher = null; - assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, toSeqNo)); - } finally { - IOUtils.close(searcher); - } - } - // Get snapshot via engine will auto refresh - fromSeqNo = randomLongBetween(0, numOps - 1); - toSeqNo = randomLongBetween(fromSeqNo, numOps - 1); - try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, randomBoolean())) { - assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, toSeqNo)); - } - } - - public void testDedupByPrimaryTerm() throws Exception { - Map latestOperations = new HashMap<>(); - List terms = Arrays.asList(between(1, 1000), between(1000, 2000)); - int totalOps = 0; - for (long term : terms) { - final List ops = generateSingleDocHistory(true, - randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL, VersionType.EXTERNAL_GTE), term, 2, 20, "1"); - primaryTerm.set(Math.max(primaryTerm.get(), term)); - engine.rollTranslogGeneration(); - for (Engine.Operation op : ops) { - // We need to simulate a rollback here as only ops after local checkpoint get into the engine - if (op.seqNo() <= engine.getLocalCheckpointTracker().getCheckpoint()) { - engine.getLocalCheckpointTracker().resetCheckpoint(randomLongBetween(-1, op.seqNo() - 1)); - engine.rollTranslogGeneration(); - } - if (op instanceof Engine.Index) { - engine.index((Engine.Index) op); - } else if (op instanceof Engine.Delete) { - engine.delete((Engine.Delete) op); - } - latestOperations.put(op.seqNo(), op.primaryTerm()); - if (rarely()) { - engine.refresh("test"); - } - if (rarely()) { - engine.flush(); - } - totalOps++; - } - } - long maxSeqNo = engine.getLocalCheckpointTracker().getMaxSeqNo(); - try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, 0, maxSeqNo, false)) { - Translog.Operation op; - while ((op = snapshot.next()) != null) { - assertThat(op.toString(), op.primaryTerm(), equalTo(latestOperations.get(op.seqNo()))); - } - assertThat(snapshot.skippedOperations(), equalTo(totalOps - latestOperations.size())); - } - } - - public void testUpdateAndReadChangesConcurrently() throws Exception { - Follower[] followers = new Follower[between(1, 3)]; - CountDownLatch readyLatch = new CountDownLatch(followers.length + 1); - AtomicBoolean isDone = new AtomicBoolean(); - for (int i = 0; i < followers.length; i++) { - followers[i] = new Follower(engine, isDone, readyLatch); - followers[i].start(); - } - boolean onPrimary = randomBoolean(); - List operations = new ArrayList<>(); - int numOps = scaledRandomIntBetween(1, 1000); - for (int i = 0; i < numOps; i++) { - String id = Integer.toString(randomIntBetween(1, 10)); - ParsedDocument doc = createParsedDoc(id, randomAlphaOfLengthBetween(1, 5), randomBoolean()); - final Engine.Operation op; - if (onPrimary) { - if (randomBoolean()) { - op = new Engine.Index(newUid(doc), primaryTerm.get(), doc); - } else { - op = new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get()); - } - } else { - if (randomBoolean()) { - op = replicaIndexForDoc(doc, randomNonNegativeLong(), i, randomBoolean()); - } else { - op = replicaDeleteForDoc(doc.id(), randomNonNegativeLong(), i, randomNonNegativeLong()); - } - } - operations.add(op); - } - readyLatch.countDown(); - concurrentlyApplyOps(operations, engine); - assertThat(engine.getLocalCheckpointTracker().getCheckpoint(), equalTo(operations.size() - 1L)); - isDone.set(true); - for (Follower follower : followers) { - follower.join(); - } - } - - class Follower extends Thread { - private final Engine leader; - private final TranslogHandler translogHandler; - private final AtomicBoolean isDone; - private final CountDownLatch readLatch; - - Follower(Engine leader, AtomicBoolean isDone, CountDownLatch readLatch) { - this.leader = leader; - this.isDone = isDone; - this.readLatch = readLatch; - this.translogHandler = new TranslogHandler(xContentRegistry(), IndexSettingsModule.newIndexSettings(shardId.getIndexName(), - engine.engineConfig.getIndexSettings().getSettings())); - } - - void pullOperations(Engine follower) throws IOException { - long leaderCheckpoint = leader.getLocalCheckpoint(); - long followerCheckpoint = follower.getLocalCheckpoint(); - if (followerCheckpoint < leaderCheckpoint) { - long fromSeqNo = followerCheckpoint + 1; - long batchSize = randomLongBetween(0, 100); - long toSeqNo = Math.min(fromSeqNo + batchSize, leaderCheckpoint); - try (Translog.Snapshot snapshot = leader.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, true)) { - translogHandler.run(follower, snapshot); - } - } - } - - @Override - public void run() { - try (Store store = createStore(); - InternalEngine follower = createEngine(store, createTempDir())) { - readLatch.countDown(); - readLatch.await(); - while (isDone.get() == false || - follower.getLocalCheckpointTracker().getCheckpoint() < leader.getLocalCheckpoint()) { - pullOperations(follower); - } - assertConsistentHistoryBetweenTranslogAndLuceneIndex(follower, mapperService); - assertThat(getDocIds(follower, true), equalTo(getDocIds(leader, true))); - } catch (Exception ex) { - throw new AssertionError(ex); - } - } - } - - private List drainAll(Translog.Snapshot snapshot) throws IOException { - List operations = new ArrayList<>(); - Translog.Operation op; - while ((op = snapshot.next()) != null) { - final Translog.Operation newOp = op; - logger.error("Reading [{}]", op); - assert operations.stream().allMatch(o -> o.seqNo() < newOp.seqNo()) : "Operations [" + operations + "], op [" + op + "]"; - operations.add(newOp); - } - return operations; - } -} diff --git a/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java deleted file mode 100644 index c46b47b87d06..000000000000 --- a/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java +++ /dev/null @@ -1,161 +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.index.engine; - -import org.apache.lucene.document.Document; -import org.apache.lucene.document.Field; -import org.apache.lucene.document.NumericDocValuesField; -import org.apache.lucene.document.StoredField; -import org.apache.lucene.document.StringField; -import org.apache.lucene.index.CodecReader; -import org.apache.lucene.index.DirectoryReader; -import org.apache.lucene.index.IndexWriter; -import org.apache.lucene.index.IndexWriterConfig; -import org.apache.lucene.index.IndexableField; -import org.apache.lucene.index.LeafReader; -import org.apache.lucene.index.MergePolicy; -import org.apache.lucene.index.NumericDocValues; -import org.apache.lucene.index.SegmentCommitInfo; -import org.apache.lucene.index.SegmentInfos; -import org.apache.lucene.index.StandardDirectoryReader; -import org.apache.lucene.index.Term; -import org.apache.lucene.search.DocIdSetIterator; -import org.apache.lucene.search.MatchNoDocsQuery; -import org.apache.lucene.search.TermQuery; -import org.apache.lucene.store.Directory; -import org.apache.lucene.util.InfoStream; -import org.apache.lucene.util.NullInfoStream; -import org.elasticsearch.test.ESTestCase; - -import java.io.IOException; -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; - -public class RecoverySourcePruneMergePolicyTests extends ESTestCase { - - public void testPruneAll() throws IOException { - try (Directory dir = newDirectory()) { - IndexWriterConfig iwc = newIndexWriterConfig(); - RecoverySourcePruneMergePolicy mp = new RecoverySourcePruneMergePolicy("extra_source", MatchNoDocsQuery::new, - newLogMergePolicy()); - iwc.setMergePolicy(mp); - try (IndexWriter writer = new IndexWriter(dir, iwc)) { - for (int i = 0; i < 20; i++) { - if (i > 0 && randomBoolean()) { - writer.flush(); - } - Document doc = new Document(); - doc.add(new StoredField("source", "hello world")); - doc.add(new StoredField("extra_source", "hello world")); - doc.add(new NumericDocValuesField("extra_source", 1)); - writer.addDocument(doc); - } - writer.forceMerge(1); - writer.commit(); - try (DirectoryReader reader = DirectoryReader.open(writer)) { - for (int i = 0; i < reader.maxDoc(); i++) { - Document document = reader.document(i); - assertEquals(1, document.getFields().size()); - assertEquals("source", document.getFields().get(0).name()); - } - assertEquals(1, reader.leaves().size()); - LeafReader leafReader = reader.leaves().get(0).reader(); - NumericDocValues extra_source = leafReader.getNumericDocValues("extra_source"); - if (extra_source != null) { - assertEquals(DocIdSetIterator.NO_MORE_DOCS, extra_source.nextDoc()); - } - if (leafReader instanceof CodecReader && reader instanceof StandardDirectoryReader) { - CodecReader codecReader = (CodecReader) leafReader; - StandardDirectoryReader sdr = (StandardDirectoryReader) reader; - SegmentInfos segmentInfos = sdr.getSegmentInfos(); - MergePolicy.MergeSpecification forcedMerges = mp.findForcedDeletesMerges(segmentInfos, - new MergePolicy.MergeContext() { - @Override - public int numDeletesToMerge(SegmentCommitInfo info) { - return info.info.maxDoc() - 1; - } - - @Override - public int numDeletedDocs(SegmentCommitInfo info) { - return info.info.maxDoc() - 1; - } - - @Override - public InfoStream getInfoStream() { - return new NullInfoStream(); - } - - @Override - public Set getMergingSegments() { - return Collections.emptySet(); - } - }); - // don't wrap if there is nothing to do - assertSame(codecReader, forcedMerges.merges.get(0).wrapForMerge(codecReader)); - } - } - } - } - } - - - public void testPruneSome() throws IOException { - try (Directory dir = newDirectory()) { - IndexWriterConfig iwc = newIndexWriterConfig(); - iwc.setMergePolicy(new RecoverySourcePruneMergePolicy("extra_source", - () -> new TermQuery(new Term("even", "true")), iwc.getMergePolicy())); - try (IndexWriter writer = new IndexWriter(dir, iwc)) { - for (int i = 0; i < 20; i++) { - if (i > 0 && randomBoolean()) { - writer.flush(); - } - Document doc = new Document(); - doc.add(new StringField("even", Boolean.toString(i % 2 == 0), Field.Store.YES)); - doc.add(new StoredField("source", "hello world")); - doc.add(new StoredField("extra_source", "hello world")); - doc.add(new NumericDocValuesField("extra_source", 1)); - writer.addDocument(doc); - } - writer.forceMerge(1); - writer.commit(); - try (DirectoryReader reader = DirectoryReader.open(writer)) { - assertEquals(1, reader.leaves().size()); - NumericDocValues extra_source = reader.leaves().get(0).reader().getNumericDocValues("extra_source"); - assertNotNull(extra_source); - for (int i = 0; i < reader.maxDoc(); i++) { - Document document = reader.document(i); - Set collect = document.getFields().stream().map(IndexableField::name).collect(Collectors.toSet()); - assertTrue(collect.contains("source")); - assertTrue(collect.contains("even")); - if (collect.size() == 3) { - assertTrue(collect.contains("extra_source")); - assertEquals("true", document.getField("even").stringValue()); - assertEquals(i, extra_source.nextDoc()); - } else { - assertEquals(2, document.getFields().size()); - } - } - assertEquals(DocIdSetIterator.NO_MORE_DOCS, extra_source.nextDoc()); - } - } - } - } -} diff --git a/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java deleted file mode 100644 index f35901003828..000000000000 --- a/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.index.engine; - -import org.elasticsearch.common.lease.Releasable; -import org.elasticsearch.index.seqno.SequenceNumbers; -import org.elasticsearch.test.ESTestCase; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - -import static org.hamcrest.Matchers.equalTo; - -public class SoftDeletesPolicyTests extends ESTestCase { - /** - * Makes sure we won't advance the retained seq# if the retention lock is held - */ - public void testSoftDeletesRetentionLock() { - long retainedOps = between(0, 10000); - AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); - long safeCommitCheckpoint = globalCheckpoint.get(); - SoftDeletesPolicy policy = new SoftDeletesPolicy(globalCheckpoint::get, between(1, 10000), retainedOps); - long minRetainedSeqNo = policy.getMinRetainedSeqNo(); - List locks = new ArrayList<>(); - int iters = scaledRandomIntBetween(10, 1000); - for (int i = 0; i < iters; i++) { - if (randomBoolean()) { - locks.add(policy.acquireRetentionLock()); - } - // Advances the global checkpoint and the local checkpoint of a safe commit - globalCheckpoint.addAndGet(between(0, 1000)); - safeCommitCheckpoint = randomLongBetween(safeCommitCheckpoint, globalCheckpoint.get()); - policy.setLocalCheckpointOfSafeCommit(safeCommitCheckpoint); - if (rarely()) { - retainedOps = between(0, 10000); - policy.setRetentionOperations(retainedOps); - } - // Release some locks - List releasingLocks = randomSubsetOf(locks); - locks.removeAll(releasingLocks); - releasingLocks.forEach(Releasable::close); - - // We only expose the seqno to the merge policy if the retention lock is not held. - policy.getRetentionQuery(); - if (locks.isEmpty()) { - long retainedSeqNo = Math.min(safeCommitCheckpoint, globalCheckpoint.get() - retainedOps) + 1; - minRetainedSeqNo = Math.max(minRetainedSeqNo, retainedSeqNo); - } - assertThat(policy.getMinRetainedSeqNo(), equalTo(minRetainedSeqNo)); - } - - locks.forEach(Releasable::close); - long retainedSeqNo = Math.min(safeCommitCheckpoint, globalCheckpoint.get() - retainedOps) + 1; - minRetainedSeqNo = Math.max(minRetainedSeqNo, retainedSeqNo); - assertThat(policy.getMinRetainedSeqNo(), equalTo(minRetainedSeqNo)); - } -} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java index 5a46b9a889fd..76ca6aa7ea8d 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java @@ -31,7 +31,6 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -312,18 +311,15 @@ DocumentMapper createDummyMapping(MapperService mapperService) throws Exception // creates an object mapper, which is about 100x harder than it should be.... ObjectMapper createObjectMapper(MapperService mapperService, String name) throws Exception { - IndexMetaData build = IndexMetaData.builder("") - .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1).numberOfReplicas(0).build(); - IndexSettings settings = new IndexSettings(build, Settings.EMPTY); - ParseContext context = new ParseContext.InternalParseContext(settings, + ParseContext context = new ParseContext.InternalParseContext( + Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(), mapperService.documentMapperParser(), mapperService.documentMapper("type"), null, null); String[] nameParts = name.split("\\."); for (int i = 0; i < nameParts.length - 1; ++i) { context.path().add(nameParts[i]); } Mapper.Builder builder = new ObjectMapper.Builder(nameParts[nameParts.length - 1]).enabled(true); - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); return (ObjectMapper)builder.build(builderContext); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java index b11e4876f9ea..cb2ed785699c 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java @@ -34,7 +34,6 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.IndexService; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.BooleanFieldMapper.BooleanFieldType; import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType; import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType; @@ -216,10 +215,7 @@ private String serialize(ToXContent mapper) throws Exception { } private Mapper parse(DocumentMapper mapper, DocumentMapperParser parser, XContentBuilder builder) throws Exception { - IndexMetaData build = IndexMetaData.builder("") - .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) - .numberOfShards(1).numberOfReplicas(0).build(); - IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(); SourceToParse source = SourceToParse.source("test", mapper.type(), "some_id", BytesReference.bytes(builder), builder.contentType()); try (XContentParser xContentParser = createParser(JsonXContent.jsonXContent, source.source())) { ParseContext.InternalParseContext ctx = new ParseContext.InternalParseContext(settings, parser, mapper, source, xContentParser); 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 fba71dd1e529..1d1e423afc1b 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.index.replication; -import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.Term; @@ -42,7 +41,6 @@ import org.elasticsearch.index.engine.InternalEngineTests; import org.elasticsearch.index.engine.SegmentsStats; import org.elasticsearch.index.engine.VersionConflictEngineException; -import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; @@ -142,9 +140,7 @@ public void cleanFiles(int totalTranslogOps, Store.MetadataSnapshot sourceMetaDa } public void testInheritMaxValidAutoIDTimestampOnRecovery() throws Exception { - //TODO: Enables this test with soft-deletes once we have timestamp - Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); - try (ReplicationGroup shards = createGroup(0, settings)) { + try (ReplicationGroup shards = createGroup(0)) { shards.startAll(); final IndexRequest indexRequest = new IndexRequest(index.getName(), "type").source("{}", XContentType.JSON); indexRequest.onRetry(); // force an update of the timestamp @@ -350,13 +346,7 @@ public void testDocumentFailureReplication() throws Exception { final AtomicBoolean throwAfterIndexedOneDoc = new AtomicBoolean(); // need one document to trigger delete in IW. @Override public long addDocument(Iterable doc) throws IOException { - boolean isTombstone = false; - for (IndexableField field : doc) { - if (SeqNoFieldMapper.TOMBSTONE_NAME.equals(field.name())) { - isTombstone = true; - } - } - if (isTombstone == false && throwAfterIndexedOneDoc.getAndSet(true)) { + if (throwAfterIndexedOneDoc.getAndSet(true)) { throw indexException; } else { return super.addDocument(doc); @@ -366,10 +356,6 @@ public long addDocument(Iterable doc) throws IOExcepti public long deleteDocuments(Term... terms) throws IOException { throw deleteException; } - @Override - public long softUpdateDocument(Term term, Iterable doc, Field...fields) throws IOException { - throw deleteException; // a delete uses softUpdateDocument API if soft-deletes enabled - } }, null, null, config); try (ReplicationGroup shards = new ReplicationGroup(buildIndexMetaData(0)) { @Override @@ -404,9 +390,6 @@ public long softUpdateDocument(Term term, Iterable doc try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } - try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { - assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); - } } // unlike previous failures, these two failures replicated directly from the replication channel. indexResp = shards.index(new IndexRequest(index.getName(), "type", "any").source("{}", XContentType.JSON)); @@ -421,9 +404,6 @@ public long softUpdateDocument(Term term, Iterable doc try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } - try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { - assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); - } } shards.assertAllEqual(1); } @@ -521,9 +501,8 @@ public void testSeqNoCollision() throws Exception { recoverReplica(replica3, replica2, true); try (Translog.Snapshot snapshot = getTranslog(replica3).newSnapshot()) { assertThat(snapshot.totalOperations(), equalTo(initDocs + 1)); - final List expectedOps = new ArrayList<>(initOperations); - expectedOps.add(op2); - assertThat(snapshot, containsOperationsInAnyOrder(expectedOps)); + assertThat(snapshot.next(), equalTo(op2)); + assertThat("Remaining of snapshot should contain init operations", snapshot, containsOperationsInAnyOrder(initOperations)); assertThat("Peer-recovery should not send overridden operations", snapshot.skippedOperations(), equalTo(0)); } // TODO: We should assert the content of shards in the ReplicationGroup. 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 28122665e9bb..2d198c32ba74 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java @@ -98,8 +98,7 @@ public void testIndexingDuringFileRecovery() throws Exception { } public void testRecoveryOfDisconnectedReplica() throws Exception { - Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); - try (ReplicationGroup shards = createGroup(1, settings)) { + try (ReplicationGroup shards = createGroup(1)) { shards.startAll(); int docs = shards.indexDocs(randomInt(50)); shards.flush(); @@ -267,7 +266,6 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { builder.settings(Settings.builder().put(newPrimary.indexSettings().getSettings()) .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), "-1") .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1") - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) ); newPrimary.indexSettings().updateIndexMetaData(builder.build()); newPrimary.onSettingsChanged(); @@ -277,12 +275,7 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { shards.syncGlobalCheckpoint(); assertThat(newPrimary.getLastSyncedGlobalCheckpoint(), equalTo(newPrimary.seqNoStats().getMaxSeqNo())); }); - newPrimary.flush(new FlushRequest().force(true)); - if (replica.indexSettings().isSoftDeleteEnabled()) { - // We need an extra flush to advance the min_retained_seqno on the new primary so ops-based won't happen. - // The min_retained_seqno only advances when a merge asks for the retention query. - newPrimary.flush(new FlushRequest().force(true)); - } + newPrimary.flush(new FlushRequest()); uncommittedOpsOnPrimary = shards.indexDocs(randomIntBetween(0, 10)); totalDocs += uncommittedOpsOnPrimary; } 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 50f95bf4d473..2228e1b017fd 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -22,7 +22,6 @@ import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexCommit; -import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TermQuery; @@ -31,7 +30,6 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.FilterDirectory; import org.apache.lucene.store.IOContext; -import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.Constants; import org.elasticsearch.Assertions; import org.elasticsearch.Version; @@ -91,13 +89,8 @@ import org.elasticsearch.index.fielddata.IndexFieldDataService; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; -import org.elasticsearch.index.mapper.ParseContext; -import org.elasticsearch.index.mapper.ParsedDocument; -import org.elasticsearch.index.mapper.SeqNoFieldMapper; -import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.Uid; -import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; @@ -167,7 +160,6 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThan; @@ -245,8 +237,7 @@ public void testFailShard() throws Exception { assertNotNull(shardPath); // fail shard shard.failShard("test shard fail", new CorruptIndexException("", "")); - shard.close("do not assert history", false); - shard.store().close(); + closeShards(shard); // check state file still exists ShardStateMetaData shardStateMetaData = load(logger, shardPath.getShardStatePath()); assertEquals(shardStateMetaData, getShardStateMetadata(shard)); @@ -2403,8 +2394,7 @@ public void testRecoverFromLocalShard() throws IOException { public void testDocStats() throws IOException, InterruptedException { IndexShard indexShard = null; try { - indexShard = newStartedShard( - Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0).build()); + indexShard = newStartedShard(); final long numDocs = randomIntBetween(2, 32); // at least two documents so we have docs to delete final long numDocsToDelete = randomLongBetween(1, numDocs); for (int i = 0; i < numDocs; i++) { @@ -2434,16 +2424,7 @@ public void testDocStats() throws IOException, InterruptedException { deleteDoc(indexShard, "_doc", id); indexDoc(indexShard, "_doc", id); } - // Need to update and sync the global checkpoint as the soft-deletes retention MergePolicy depends on it. - if (indexShard.indexSettings.isSoftDeleteEnabled()) { - if (indexShard.routingEntry().primary()) { - indexShard.updateGlobalCheckpointForShard(indexShard.routingEntry().allocationId().getId(), - indexShard.getLocalCheckpoint()); - } else { - indexShard.updateGlobalCheckpointOnReplica(indexShard.getLocalCheckpoint(), "test"); - } - indexShard.sync(); - } + // flush the buffered deletes final FlushRequest flushRequest = new FlushRequest(); flushRequest.force(false); @@ -2981,7 +2962,6 @@ public void testSegmentMemoryTrackedInBreaker() throws Exception { assertThat(breaker.getUsed(), greaterThan(preRefreshBytes)); indexDoc(primary, "_doc", "4", "{\"foo\": \"potato\"}"); - indexDoc(primary, "_doc", "5", "{\"foo\": \"potato\"}"); // Forces a refresh with the INTERNAL scope ((InternalEngine) primary.getEngine()).writeIndexingBuffer(); @@ -2993,13 +2973,6 @@ public void testSegmentMemoryTrackedInBreaker() throws Exception { // Deleting a doc causes its memory to be freed from the breaker deleteDoc(primary, "_doc", "0"); - // Here we are testing that a fully deleted segment should be dropped and its memory usage is freed. - // In order to instruct the merge policy not to keep a fully deleted segment, - // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. - if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { - primary.sync(); - flushShard(primary); - } primary.refresh("force refresh"); ss = primary.segmentStats(randomBoolean()); @@ -3091,7 +3064,6 @@ public void testSegmentMemoryTrackedWithRandomSearchers() throws Exception { // Close remaining searchers IOUtils.close(searchers); - primary.refresh("test"); SegmentsStats ss = primary.segmentStats(randomBoolean()); CircuitBreaker breaker = primary.circuitBreakerService.getBreaker(CircuitBreaker.ACCOUNTING); @@ -3209,28 +3181,4 @@ public void testOnCloseStats() throws IOException { } - public void testSupplyTombstoneDoc() throws Exception { - IndexShard shard = newStartedShard(); - String id = randomRealisticUnicodeOfLengthBetween(1, 10); - ParsedDocument deleteTombstone = shard.getEngine().config().getTombstoneDocSupplier().newDeleteTombstoneDoc("doc", id); - assertThat(deleteTombstone.docs(), hasSize(1)); - ParseContext.Document deleteDoc = deleteTombstone.docs().get(0); - assertThat(deleteDoc.getFields().stream().map(IndexableField::name).collect(Collectors.toList()), - containsInAnyOrder(IdFieldMapper.NAME, VersionFieldMapper.NAME, - SeqNoFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME)); - assertThat(deleteDoc.getField(IdFieldMapper.NAME).binaryValue(), equalTo(Uid.encodeId(id))); - assertThat(deleteDoc.getField(SeqNoFieldMapper.TOMBSTONE_NAME).numericValue().longValue(), equalTo(1L)); - - final String reason = randomUnicodeOfLength(200); - ParsedDocument noopTombstone = shard.getEngine().config().getTombstoneDocSupplier().newNoopTombstoneDoc(reason); - assertThat(noopTombstone.docs(), hasSize(1)); - ParseContext.Document noopDoc = noopTombstone.docs().get(0); - assertThat(noopDoc.getFields().stream().map(IndexableField::name).collect(Collectors.toList()), - containsInAnyOrder(VersionFieldMapper.NAME, SourceFieldMapper.NAME, SeqNoFieldMapper.TOMBSTONE_NAME, - SeqNoFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME)); - assertThat(noopDoc.getField(SeqNoFieldMapper.TOMBSTONE_NAME).numericValue().longValue(), equalTo(1L)); - assertThat(noopDoc.getField(SourceFieldMapper.NAME).binaryValue(), equalTo(new BytesRef(reason))); - - closeShards(shard); - } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java index 29b16ca28f4d..ae2cc84e4870 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java @@ -106,22 +106,17 @@ public void testSyncerSendsOffCorrectDocuments() throws Exception { .isPresent(), is(false)); } + + assertEquals(globalCheckPoint == numDocs - 1 ? 0 : numDocs, resyncTask.getTotalOperations()); if (syncNeeded && globalCheckPoint < numDocs - 1) { - if (shard.indexSettings.isSoftDeleteEnabled()) { - assertThat(resyncTask.getSkippedOperations(), equalTo(0)); - assertThat(resyncTask.getResyncedOperations(), equalTo(resyncTask.getTotalOperations())); - assertThat(resyncTask.getTotalOperations(), equalTo(Math.toIntExact(numDocs - 1 - globalCheckPoint))); - } else { - int skippedOps = Math.toIntExact(globalCheckPoint + 1); // everything up to global checkpoint included - assertThat(resyncTask.getSkippedOperations(), equalTo(skippedOps)); - assertThat(resyncTask.getResyncedOperations(), equalTo(numDocs - skippedOps)); - assertThat(resyncTask.getTotalOperations(), equalTo(globalCheckPoint == numDocs - 1 ? 0 : numDocs)); - } + long skippedOps = globalCheckPoint + 1; // everything up to global checkpoint included + assertEquals(skippedOps, resyncTask.getSkippedOperations()); + assertEquals(numDocs - skippedOps, resyncTask.getResyncedOperations()); } else { - assertThat(resyncTask.getSkippedOperations(), equalTo(0)); - assertThat(resyncTask.getResyncedOperations(), equalTo(0)); - assertThat(resyncTask.getTotalOperations(), equalTo(0)); + assertEquals(0, resyncTask.getSkippedOperations()); + assertEquals(0, resyncTask.getResyncedOperations()); } + closeShards(shard); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java index b93f170174c3..774b272121a5 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java @@ -42,7 +42,6 @@ import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.engine.EngineConfig; -import org.elasticsearch.index.engine.EngineTestCase; import org.elasticsearch.index.engine.InternalEngine; import org.elasticsearch.index.fieldvisitor.SingleFieldsVisitor; import org.elasticsearch.index.mapper.IdFieldMapper; @@ -131,8 +130,7 @@ public void onFailedEngine(String reason, @Nullable Exception e) { indexSettings, null, store, newMergePolicy(), iwc.getAnalyzer(), iwc.getSimilarity(), new CodecService(null, logger), eventListener, IndexSearcher.getDefaultQueryCache(), IndexSearcher.getDefaultQueryCachingPolicy(), translogConfig, TimeValue.timeValueMinutes(5), Collections.singletonList(listeners), Collections.emptyList(), null, - (e, s) -> 0, new NoneCircuitBreakerService(), () -> SequenceNumbers.NO_OPS_PERFORMED, () -> primaryTerm, - EngineTestCase.tombstoneDocSupplier()); + (e, s) -> 0, new NoneCircuitBreakerService(), () -> SequenceNumbers.NO_OPS_PERFORMED, () -> primaryTerm); engine = new InternalEngine(config); engine.recoverFromTranslog(Long.MAX_VALUE); listeners.setCurrentRefreshLocationSupplier(engine::getTranslogLastWriteLocation); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java index 81afab4bb8f7..89a8813e3e07 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java @@ -67,7 +67,6 @@ import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; -import org.junit.After; import java.io.IOException; import java.util.ArrayList; @@ -111,11 +110,6 @@ protected Collection> nodePlugins() { RecoverySettingsChunkSizePlugin.class); } - @After - public void assertConsistentHistoryInLuceneIndex() throws Exception { - internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); - } - private void assertRecoveryStateWithoutStage(RecoveryState state, int shardId, RecoverySource recoverySource, boolean primary, String sourceNode, String targetNode) { assertThat(state.getShardId().getId(), equalTo(shardId)); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java index b6f5a7b64516..4b1419375e6e 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java @@ -25,7 +25,6 @@ import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.NoMergePolicy; import org.elasticsearch.common.UUIDs; -import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTestCase; @@ -92,7 +91,6 @@ public void testGetStartingSeqNo() throws Exception { replica.close("test", false); final List commits = DirectoryReader.listCommits(replica.store().directory()); IndexWriterConfig iwc = new IndexWriterConfig(null) - .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) .setMergePolicy(NoMergePolicy.INSTANCE) .setOpenMode(IndexWriterConfig.OpenMode.APPEND); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index 0351111c305c..f0644b029c3d 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -411,6 +411,12 @@ public void testThrowExceptionOnPrimaryRelocatedBeforePhase1Started() throws IOE recoverySettings.getChunkSize().bytesAsInt(), Settings.EMPTY) { + + @Override + boolean isTranslogReadyForSequenceNumberBasedRecovery() throws IOException { + return randomBoolean(); + } + @Override public void phase1(final IndexCommit snapshot, final Supplier translogOps) { phase1Called.set(true); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java index 45535e19672c..5547a629ab2a 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java @@ -34,7 +34,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.mapper.SourceToParse; @@ -64,13 +63,13 @@ public void testTranslogHistoryTransferred() throws Exception { int docs = shards.indexDocs(10); getTranslog(shards.getPrimary()).rollGeneration(); shards.flush(); - int moreDocs = shards.indexDocs(randomInt(10)); + if (randomBoolean()) { + docs += shards.indexDocs(10); + } shards.addReplica(); shards.startAll(); final IndexShard replica = shards.getReplicas().get(0); - boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); - assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? moreDocs : docs + moreDocs)); - shards.assertAllEqual(docs + moreDocs); + assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(docs)); } } @@ -102,12 +101,12 @@ public void testRetentionPolicyChangeDuringRecovery() throws Exception { // rolling/flushing is async assertBusy(() -> { assertThat(replica.getLastSyncedGlobalCheckpoint(), equalTo(19L)); - assertThat(getTranslog(replica).totalOperations(), equalTo(0)); + assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(0)); }); } } - public void testRecoveryWithOutOfOrderDeleteWithTranslog() throws Exception { + public void testRecoveryWithOutOfOrderDelete() throws Exception { /* * The flow of this test: * - delete #1 @@ -119,8 +118,7 @@ public void testRecoveryWithOutOfOrderDeleteWithTranslog() throws Exception { * - index #5 * - If flush and the translog retention disabled, delete #1 will be removed while index #0 is still retained and replayed. */ - Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); - try (ReplicationGroup shards = createGroup(1, settings)) { + try (ReplicationGroup shards = createGroup(1)) { shards.startAll(); // create out of order delete and index op on replica final IndexShard orgReplica = shards.getReplicas().get(0); @@ -172,63 +170,7 @@ public void testRecoveryWithOutOfOrderDeleteWithTranslog() throws Exception { shards.recoverReplica(newReplica); shards.assertAllEqual(3); - assertThat(getTranslog(newReplica).totalOperations(), equalTo(translogOps)); - } - } - - public void testRecoveryWithOutOfOrderDeleteWithSoftDeletes() throws Exception { - Settings settings = Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 10) - // If soft-deletes is enabled, delete#1 will be reclaimed because its segment (segment_1) is fully deleted - // index#0 will be retained if merge is disabled; otherwise it will be reclaimed because gcp=3 and retained_ops=0 - .put(MergePolicyConfig.INDEX_MERGE_ENABLED, false).build(); - try (ReplicationGroup shards = createGroup(1, settings)) { - shards.startAll(); - // create out of order delete and index op on replica - final IndexShard orgReplica = shards.getReplicas().get(0); - final String indexName = orgReplica.shardId().getIndexName(); - - // delete #1 - orgReplica.applyDeleteOperationOnReplica(1, 2, "type", "id"); - orgReplica.flush(new FlushRequest().force(true)); // isolate delete#1 in its own translog generation and lucene segment - // index #0 - orgReplica.applyIndexOperationOnReplica(0, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, - SourceToParse.source(indexName, "type", "id", new BytesArray("{}"), XContentType.JSON)); - // index #3 - orgReplica.applyIndexOperationOnReplica(3, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, - SourceToParse.source(indexName, "type", "id-3", new BytesArray("{}"), XContentType.JSON)); - // Flushing a new commit with local checkpoint=1 allows to delete the translog gen #1. - orgReplica.flush(new FlushRequest().force(true).waitIfOngoing(true)); - // index #2 - orgReplica.applyIndexOperationOnReplica(2, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, - SourceToParse.source(indexName, "type", "id-2", new BytesArray("{}"), XContentType.JSON)); - orgReplica.updateGlobalCheckpointOnReplica(3L, "test"); - // index #5 -> force NoOp #4. - orgReplica.applyIndexOperationOnReplica(5, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, - SourceToParse.source(indexName, "type", "id-5", new BytesArray("{}"), XContentType.JSON)); - - if (randomBoolean()) { - if (randomBoolean()) { - logger.info("--> flushing shard (translog/soft-deletes will be trimmed)"); - IndexMetaData.Builder builder = IndexMetaData.builder(orgReplica.indexSettings().getIndexMetaData()); - builder.settings(Settings.builder().put(orgReplica.indexSettings().getSettings()) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0)); - orgReplica.indexSettings().updateIndexMetaData(builder.build()); - orgReplica.onSettingsChanged(); - } - flushShard(orgReplica); - } - - final IndexShard orgPrimary = shards.getPrimary(); - shards.promoteReplicaToPrimary(orgReplica).get(); // wait for primary/replica sync to make sure seq# gap is closed. - - IndexShard newReplica = shards.addReplicaWithExistingPath(orgPrimary.shardPath(), orgPrimary.routingEntry().currentNodeId()); - shards.recoverReplica(newReplica); - shards.assertAllEqual(3); - try (Translog.Snapshot snapshot = newReplica.getHistoryOperations("test", 0)) { - assertThat(snapshot, SnapshotMatchers.size(6)); - } + assertThat(newReplica.estimateTranslogOperationsFromMinSeq(0), equalTo(translogOps)); } } @@ -280,8 +222,7 @@ public void testDifferentHistoryUUIDDisablesOPsRecovery() throws Exception { shards.recoverReplica(newReplica); // file based recovery should be made assertThat(newReplica.recoveryState().getIndex().fileDetails(), not(empty())); - boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); - assertThat(getTranslog(newReplica).totalOperations(), equalTo(softDeletesEnabled ? nonFlushedDocs : numDocs)); + assertThat(newReplica.estimateTranslogOperationsFromMinSeq(0), equalTo(numDocs)); // history uuid was restored assertThat(newReplica.getHistoryUUID(), equalTo(historyUUID)); @@ -385,8 +326,7 @@ public void testShouldFlushAfterPeerRecovery() throws Exception { shards.recoverReplica(replica); // Make sure the flushing will eventually be completed (eg. `shouldPeriodicallyFlush` is false) assertBusy(() -> assertThat(getEngine(replica).shouldPeriodicallyFlush(), equalTo(false))); - boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); - assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? 0 : numDocs)); + assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(numDocs)); shards.assertAllEqual(numDocs); } } diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index ce162b9600cf..fa591411bba1 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -43,7 +43,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexModule; -import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.MergeSchedulerConfig; @@ -51,7 +50,6 @@ import org.elasticsearch.index.cache.query.QueryCacheStats; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.indices.IndicesQueryCache; import org.elasticsearch.indices.IndicesRequestCache; @@ -71,7 +69,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Random; -import java.util.Set; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -118,7 +115,6 @@ public Settings indexSettings() { return Settings.builder().put(super.indexSettings()) .put(IndexModule.INDEX_QUERY_CACHE_EVERYTHING_SETTING.getKey(), true) .put(IndexModule.INDEX_QUERY_CACHE_ENABLED_SETTING.getKey(), true) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) .build(); } @@ -1010,15 +1006,10 @@ private void assertCumulativeQueryCacheStats(IndicesStatsResponse response) { @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32506") public void testFilterCacheStats() throws Exception { - Settings settings = Settings.builder().put(indexSettings()).put("number_of_replicas", 0).build(); - assertAcked(prepareCreate("index").setSettings(settings).get()); - indexRandom(false, true, + assertAcked(prepareCreate("index").setSettings(Settings.builder().put(indexSettings()).put("number_of_replicas", 0).build()).get()); + indexRandom(true, client().prepareIndex("index", "type", "1").setSource("foo", "bar"), client().prepareIndex("index", "type", "2").setSource("foo", "baz")); - if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { - persistGlobalCheckpoint("index"); // Need to persist the global checkpoint for the soft-deletes retention MP. - } - refresh(); ensureGreen(); IndicesStatsResponse response = client().admin().indices().prepareStats("index").setQueryCache(true).get(); @@ -1049,13 +1040,6 @@ public void testFilterCacheStats() throws Exception { assertEquals(DocWriteResponse.Result.DELETED, client().prepareDelete("index", "type", "1").get().getResult()); assertEquals(DocWriteResponse.Result.DELETED, client().prepareDelete("index", "type", "2").get().getResult()); - // Here we are testing that a fully deleted segment should be dropped and its cached is evicted. - // In order to instruct the merge policy not to keep a fully deleted segment, - // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. - if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { - persistGlobalCheckpoint("index"); - flush("index"); - } refresh(); response = client().admin().indices().prepareStats("index").setQueryCache(true).get(); assertCumulativeQueryCacheStats(response); @@ -1189,21 +1173,4 @@ public void testConcurrentIndexingAndStatsRequests() throws BrokenBarrierExcepti assertThat(executionFailures.get(), emptyCollectionOf(Exception.class)); } - - /** - * Persist the global checkpoint on all shards of the given index into disk. - * This makes sure that the persisted global checkpoint on those shards will equal to the in-memory value. - */ - private void persistGlobalCheckpoint(String index) throws Exception { - final Set nodes = internalCluster().nodesInclude(index); - for (String node : nodes) { - final IndicesService indexServices = internalCluster().getInstance(IndicesService.class, node); - for (IndexService indexService : indexServices) { - for (IndexShard indexShard : indexService) { - indexShard.sync(); - assertThat(indexShard.getLastSyncedGlobalCheckpoint(), equalTo(indexShard.getGlobalCheckpoint())); - } - } - } - } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java b/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java index c25cad61e074..23c56688e00b 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java +++ b/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java @@ -27,7 +27,6 @@ import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.snapshots.mockstore.MockRepository; import org.elasticsearch.test.ESIntegTestCase; -import org.junit.After; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -59,11 +58,6 @@ protected Collection> nodePlugins() { return Arrays.asList(MockRepository.Plugin.class); } - @After - public void assertConsistentHistoryInLuceneIndex() throws Exception { - internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); - } - public static long getFailureCount(String repository) { long failureCount = 0; for (RepositoriesService repositoriesService : diff --git a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java index 632a1ecbee1a..1230d594b98a 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java @@ -122,7 +122,6 @@ import static org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider.SETTING_ALLOCATION_MAX_RETRY; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.IndexSettings.INDEX_REFRESH_INTERVAL_SETTING; -import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.elasticsearch.index.shard.IndexShardTests.getEngineFromShard; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -2049,9 +2048,7 @@ public void testSnapshotMoreThanOnce() throws ExecutionException, InterruptedExc .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); // only one shard - final Settings indexSettings = Settings.builder() - .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1).build(); - assertAcked(prepareCreate("test").setSettings(indexSettings)); + assertAcked(prepareCreate("test").setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1))); ensureGreen(); logger.info("--> indexing"); @@ -2097,13 +2094,7 @@ public void testSnapshotMoreThanOnce() throws ExecutionException, InterruptedExc SnapshotStatus snapshotStatus = client.admin().cluster().prepareSnapshotStatus("test-repo").setSnapshots("test-2").get().getSnapshots().get(0); List shards = snapshotStatus.getShards(); for (SnapshotIndexShardStatus status : shards) { - // we flush before the snapshot such that we have to process the segments_N files plus the .del file - if (INDEX_SOFT_DELETES_SETTING.get(indexSettings)) { - // soft-delete generates DV files. - assertThat(status.getStats().getProcessedFileCount(), greaterThan(2)); - } else { - assertThat(status.getStats().getProcessedFileCount(), equalTo(2)); - } + assertThat(status.getStats().getProcessedFileCount(), equalTo(2)); // we flush before the snapshot such that we have to process the segments_N files plus the .del file } } } diff --git a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java index 588118db4aef..caf4f725fa45 100644 --- a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java +++ b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java @@ -26,7 +26,6 @@ import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.VersionType; @@ -786,26 +785,4 @@ public void testGCDeletesZero() throws Exception { .getVersion(), equalTo(-1L)); } - - public void testSpecialVersioning() { - internalCluster().ensureAtLeastNumDataNodes(2); - createIndex("test", Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0).build()); - IndexResponse doc1 = client().prepareIndex("test", "type", "1").setSource("field", "value1") - .setVersion(0).setVersionType(VersionType.EXTERNAL).execute().actionGet(); - assertThat(doc1.getVersion(), equalTo(0L)); - IndexResponse doc2 = client().prepareIndex("test", "type", "1").setSource("field", "value2") - .setVersion(Versions.MATCH_ANY).setVersionType(VersionType.INTERNAL).execute().actionGet(); - assertThat(doc2.getVersion(), equalTo(1L)); - client().prepareDelete("test", "type", "1").get(); //v2 - IndexResponse doc3 = client().prepareIndex("test", "type", "1").setSource("field", "value3") - .setVersion(Versions.MATCH_DELETED).setVersionType(VersionType.INTERNAL).execute().actionGet(); - assertThat(doc3.getVersion(), equalTo(3L)); - IndexResponse doc4 = client().prepareIndex("test", "type", "1").setSource("field", "value4") - .setVersion(4L).setVersionType(VersionType.EXTERNAL_GTE).execute().actionGet(); - assertThat(doc4.getVersion(), equalTo(4L)); - // Make sure that these versions are replicated correctly - client().admin().indices().prepareUpdateSettings("test") - .setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)).get(); - ensureGreen("test"); - } } 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 b558cd1ba900..b5ba5f18b395 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 @@ -19,18 +19,14 @@ package org.elasticsearch.index.engine; -import org.apache.logging.log4j.Logger; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.codecs.Codec; -import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.StoredField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; -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.Term; @@ -38,41 +34,32 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.ReferenceManager; import org.apache.lucene.search.Sort; -import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TotalHitCountCollector; import org.apache.lucene.store.Directory; -import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.cluster.ClusterModule; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.routing.AllocationId; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.Lucene; -import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; -import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.MapperTestUtils; -import org.elasticsearch.index.VersionType; import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.index.mapper.IdFieldMapper; -import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.Uid; -import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.index.seqno.LocalCheckpointTracker; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.SequenceNumbers; @@ -93,30 +80,17 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; -import java.util.function.Function; import java.util.function.LongSupplier; import java.util.function.ToLongBiFunction; -import java.util.stream.Collectors; import static java.util.Collections.emptyList; -import static java.util.Collections.shuffle; -import static org.elasticsearch.index.engine.Engine.Operation.Origin.PRIMARY; -import static org.elasticsearch.index.engine.Engine.Operation.Origin.REPLICA; import static org.elasticsearch.index.translog.TranslogDeletionPolicies.createTranslogDeletionPolicy; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.notNullValue; public abstract class EngineTestCase extends ESTestCase { @@ -154,20 +128,6 @@ protected static void assertVisibleCount(Engine engine, int numDocs, boolean ref } } - protected Settings indexSettings() { - // TODO randomize more settings - return Settings.builder() - .put(IndexSettings.INDEX_GC_DELETES_SETTING.getKey(), "1h") // make sure this doesn't kick in on us - .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), codecName) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.getKey(), - between(10, 10 * IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.get(Settings.EMPTY))) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), - randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) - .build(); - } - @Override @Before public void setUp() throws Exception { @@ -182,7 +142,13 @@ public void setUp() throws Exception { } else { codecName = "default"; } - defaultSettings = IndexSettingsModule.newIndexSettings("test", indexSettings()); + defaultSettings = IndexSettingsModule.newIndexSettings("test", Settings.builder() + .put(IndexSettings.INDEX_GC_DELETES_SETTING.getKey(), "1h") // make sure this doesn't kick in on us + .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), codecName) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.getKey(), + between(10, 10 * IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.get(Settings.EMPTY))) + .build()); // TODO randomize more settings threadPool = new TestThreadPool(getClass().getName()); store = createStore(); storeReplica = createStore(); @@ -214,7 +180,7 @@ public EngineConfig copy(EngineConfig config, LongSupplier globalCheckpointSuppl new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), config.getTranslogConfig(), config.getFlushMergesAfter(), config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), - config.getCircuitBreakerService(), globalCheckpointSupplier, config.getPrimaryTermSupplier(), tombstoneDocSupplier()); + config.getCircuitBreakerService(), globalCheckpointSupplier, config.getPrimaryTermSupplier()); } public EngineConfig copy(EngineConfig config, Analyzer analyzer) { @@ -223,18 +189,7 @@ public EngineConfig copy(EngineConfig config, Analyzer analyzer) { new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), config.getTranslogConfig(), config.getFlushMergesAfter(), config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), - config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier(), - config.getTombstoneDocSupplier()); - } - - public EngineConfig copy(EngineConfig config, MergePolicy mergePolicy) { - return new EngineConfig(config.getShardId(), config.getAllocationId(), config.getThreadPool(), config.getIndexSettings(), - config.getWarmer(), config.getStore(), mergePolicy, config.getAnalyzer(), config.getSimilarity(), - new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), - config.getTranslogConfig(), config.getFlushMergesAfter(), - config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), - config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier(), - config.getTombstoneDocSupplier()); + config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier()); } @Override @@ -243,11 +198,9 @@ public void tearDown() throws Exception { super.tearDown(); if (engine != null && engine.isClosed.get() == false) { engine.getTranslog().getDeletionPolicy().assertNoOpenTranslogRefs(); - assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, createMapperService("test")); } if (replicaEngine != null && replicaEngine.isClosed.get() == false) { replicaEngine.getTranslog().getDeletionPolicy().assertNoOpenTranslogRefs(); - assertConsistentHistoryBetweenTranslogAndLuceneIndex(replicaEngine, createMapperService("test")); } IOUtils.close( replicaEngine, storeReplica, @@ -275,18 +228,8 @@ public static ParsedDocument createParsedDoc(String id, String routing) { return testParsedDocument(id, routing, testDocumentWithTextField(), new BytesArray("{ \"value\" : \"test\" }"), null); } - public static ParsedDocument createParsedDoc(String id, String routing, boolean recoverySource) { - return testParsedDocument(id, routing, testDocumentWithTextField(), new BytesArray("{ \"value\" : \"test\" }"), null, - recoverySource); - } - protected static ParsedDocument testParsedDocument( String id, String routing, ParseContext.Document document, BytesReference source, Mapping mappingUpdate) { - return testParsedDocument(id, routing, document, source, mappingUpdate, false); - } - protected static ParsedDocument testParsedDocument( - String id, String routing, ParseContext.Document document, BytesReference source, Mapping mappingUpdate, - boolean recoverySource) { Field uidField = new Field("_id", Uid.encodeId(id), IdFieldMapper.Defaults.FIELD_TYPE); Field versionField = new NumericDocValuesField("_version", 0); SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); @@ -296,57 +239,11 @@ protected static ParsedDocument testParsedDocument( document.add(seqID.seqNoDocValue); document.add(seqID.primaryTerm); BytesRef ref = source.toBytesRef(); - if (recoverySource) { - document.add(new StoredField(SourceFieldMapper.RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); - document.add(new NumericDocValuesField(SourceFieldMapper.RECOVERY_SOURCE_NAME, 1)); - } else { - document.add(new StoredField(SourceFieldMapper.NAME, ref.bytes, ref.offset, ref.length)); - } + document.add(new StoredField(SourceFieldMapper.NAME, ref.bytes, ref.offset, ref.length)); return new ParsedDocument(versionField, seqID, id, "test", routing, Arrays.asList(document), source, XContentType.JSON, mappingUpdate); } - /** - * Creates a tombstone document that only includes uid, seq#, term and version fields. - */ - public static EngineConfig.TombstoneDocSupplier tombstoneDocSupplier(){ - return new EngineConfig.TombstoneDocSupplier() { - @Override - public ParsedDocument newDeleteTombstoneDoc(String type, String id) { - final ParseContext.Document doc = new ParseContext.Document(); - Field uidField = new Field(IdFieldMapper.NAME, Uid.encodeId(id), IdFieldMapper.Defaults.FIELD_TYPE); - doc.add(uidField); - Field versionField = new NumericDocValuesField(VersionFieldMapper.NAME, 0); - doc.add(versionField); - SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); - doc.add(seqID.seqNo); - doc.add(seqID.seqNoDocValue); - doc.add(seqID.primaryTerm); - seqID.tombstoneField.setLongValue(1); - doc.add(seqID.tombstoneField); - return new ParsedDocument(versionField, seqID, id, type, null, - Collections.singletonList(doc), new BytesArray("{}"), XContentType.JSON, null); - } - - @Override - public ParsedDocument newNoopTombstoneDoc(String reason) { - final ParseContext.Document doc = new ParseContext.Document(); - SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); - doc.add(seqID.seqNo); - doc.add(seqID.seqNoDocValue); - doc.add(seqID.primaryTerm); - seqID.tombstoneField.setLongValue(1); - doc.add(seqID.tombstoneField); - Field versionField = new NumericDocValuesField(VersionFieldMapper.NAME, 0); - doc.add(versionField); - BytesRef byteRef = new BytesRef(reason); - doc.add(new StoredField(SourceFieldMapper.NAME, byteRef.bytes, byteRef.offset, byteRef.length)); - return new ParsedDocument(versionField, seqID, null, null, null, - Collections.singletonList(doc), null, XContentType.JSON, null); - } - }; - } - protected Store createStore() throws IOException { return createStore(newDirectory()); } @@ -564,7 +461,7 @@ public void onFailedEngine(String reason, @Nullable Exception e) { new NoneCircuitBreakerService(), globalCheckpointSupplier == null ? new ReplicationTracker(shardId, allocationId.getId(), indexSettings, SequenceNumbers.NO_OPS_PERFORMED, update -> {}) : - globalCheckpointSupplier, primaryTerm::get, tombstoneDocSupplier()); + globalCheckpointSupplier, primaryTerm::get); return config; } @@ -577,7 +474,7 @@ protected static BytesArray bytesArray(String string) { return new BytesArray(string.getBytes(Charset.defaultCharset())); } - protected static Term newUid(String id) { + protected Term newUid(String id) { return new Term("_id", Uid.encodeId(id)); } @@ -602,279 +499,6 @@ protected Engine.Index replicaIndexForDoc(ParsedDocument doc, long version, long protected Engine.Delete replicaDeleteForDoc(String id, long version, long seqNo, long startTime) { return new Engine.Delete("test", id, newUid(id), seqNo, 1, version, null, Engine.Operation.Origin.REPLICA, startTime); } - protected static void assertVisibleCount(InternalEngine engine, int numDocs) throws IOException { - assertVisibleCount(engine, numDocs, true); - } - - protected static void assertVisibleCount(InternalEngine engine, int numDocs, boolean refresh) throws IOException { - if (refresh) { - engine.refresh("test"); - } - try (Engine.Searcher searcher = engine.acquireSearcher("test")) { - final TotalHitCountCollector collector = new TotalHitCountCollector(); - searcher.searcher().search(new MatchAllDocsQuery(), collector); - assertThat(collector.getTotalHits(), equalTo(numDocs)); - } - } - - public static List generateSingleDocHistory(boolean forReplica, VersionType versionType, - long primaryTerm, int minOpCount, int maxOpCount, String docId) { - final int numOfOps = randomIntBetween(minOpCount, maxOpCount); - final List ops = new ArrayList<>(); - final Term id = newUid(docId); - final int startWithSeqNo = 0; - final String valuePrefix = (forReplica ? "r_" : "p_" ) + docId + "_"; - final boolean incrementTermWhenIntroducingSeqNo = randomBoolean(); - for (int i = 0; i < numOfOps; i++) { - final Engine.Operation op; - final long version; - switch (versionType) { - case INTERNAL: - version = forReplica ? i : Versions.MATCH_ANY; - break; - case EXTERNAL: - version = i; - break; - case EXTERNAL_GTE: - version = randomBoolean() ? Math.max(i - 1, 0) : i; - break; - case FORCE: - version = randomNonNegativeLong(); - break; - default: - throw new UnsupportedOperationException("unknown version type: " + versionType); - } - if (randomBoolean()) { - op = new Engine.Index(id, testParsedDocument(docId, null, testDocumentWithTextField(valuePrefix + i), B_1, null), - forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, - forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, - version, - forReplica ? null : versionType, - forReplica ? REPLICA : PRIMARY, - System.currentTimeMillis(), -1, false - ); - } else { - op = new Engine.Delete("test", docId, id, - forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, - forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, - version, - forReplica ? null : versionType, - forReplica ? REPLICA : PRIMARY, - System.currentTimeMillis()); - } - ops.add(op); - } - return ops; - } - - public static void assertOpsOnReplica( - final List ops, - final InternalEngine replicaEngine, - boolean shuffleOps, - final Logger logger) throws IOException { - final Engine.Operation lastOp = ops.get(ops.size() - 1); - final String lastFieldValue; - if (lastOp instanceof Engine.Index) { - Engine.Index index = (Engine.Index) lastOp; - lastFieldValue = index.docs().get(0).get("value"); - } else { - // delete - lastFieldValue = null; - } - if (shuffleOps) { - int firstOpWithSeqNo = 0; - while (firstOpWithSeqNo < ops.size() && ops.get(firstOpWithSeqNo).seqNo() < 0) { - firstOpWithSeqNo++; - } - // shuffle ops but make sure legacy ops are first - shuffle(ops.subList(0, firstOpWithSeqNo), random()); - shuffle(ops.subList(firstOpWithSeqNo, ops.size()), random()); - } - boolean firstOp = true; - for (Engine.Operation op : ops) { - logger.info("performing [{}], v [{}], seq# [{}], term [{}]", - op.operationType().name().charAt(0), op.version(), op.seqNo(), op.primaryTerm()); - if (op instanceof Engine.Index) { - Engine.IndexResult result = replicaEngine.index((Engine.Index) op); - // replicas don't really care to about creation status of documents - // this allows to ignore the case where a document was found in the live version maps in - // a delete state and return false for the created flag in favor of code simplicity - // as deleted or not. This check is just signal regression so a decision can be made if it's - // intentional - assertThat(result.isCreated(), equalTo(firstOp)); - assertThat(result.getVersion(), equalTo(op.version())); - assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); - - } else { - Engine.DeleteResult result = replicaEngine.delete((Engine.Delete) op); - // Replicas don't really care to about found status of documents - // this allows to ignore the case where a document was found in the live version maps in - // a delete state and return true for the found flag in favor of code simplicity - // his check is just signal regression so a decision can be made if it's - // intentional - assertThat(result.isFound(), equalTo(firstOp == false)); - assertThat(result.getVersion(), equalTo(op.version())); - assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); - } - if (randomBoolean()) { - replicaEngine.refresh("test"); - } - if (randomBoolean()) { - replicaEngine.flush(); - replicaEngine.refresh("test"); - } - firstOp = false; - } - - assertVisibleCount(replicaEngine, lastFieldValue == null ? 0 : 1); - if (lastFieldValue != null) { - try (Engine.Searcher searcher = replicaEngine.acquireSearcher("test")) { - final TotalHitCountCollector collector = new TotalHitCountCollector(); - searcher.searcher().search(new TermQuery(new Term("value", lastFieldValue)), collector); - assertThat(collector.getTotalHits(), equalTo(1)); - } - } - } - - protected void concurrentlyApplyOps(List ops, InternalEngine engine) throws InterruptedException { - Thread[] thread = new Thread[randomIntBetween(3, 5)]; - CountDownLatch startGun = new CountDownLatch(thread.length); - AtomicInteger offset = new AtomicInteger(-1); - for (int i = 0; i < thread.length; i++) { - thread[i] = new Thread(() -> { - startGun.countDown(); - try { - startGun.await(); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - int docOffset; - while ((docOffset = offset.incrementAndGet()) < ops.size()) { - try { - final Engine.Operation op = ops.get(docOffset); - if (op instanceof Engine.Index) { - engine.index((Engine.Index) op); - } else if (op instanceof Engine.Delete){ - engine.delete((Engine.Delete) op); - } else { - engine.noOp((Engine.NoOp) op); - } - if ((docOffset + 1) % 4 == 0) { - engine.refresh("test"); - } - if (rarely()) { - engine.flush(); - } - } catch (IOException e) { - throw new AssertionError(e); - } - } - }); - thread[i].start(); - } - for (int i = 0; i < thread.length; i++) { - thread[i].join(); - } - } - - /** - * Gets all docId from the given engine. - */ - public static Set 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<>(); - for (LeafReaderContext leafContext : searcher.reader().leaves()) { - LeafReader reader = leafContext.reader(); - 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))); - } - } - } - return ids; - } - } - - /** - * Reads all engine operations that have been processed by the engine from Lucene index. - * The returned operations are sorted and de-duplicated, thus each sequence number will be have at most one operation. - */ - public static List readAllOperationsInLucene(Engine engine, MapperService mapper) throws IOException { - final List operations = new ArrayList<>(); - long maxSeqNo = Math.max(0, ((InternalEngine)engine).getLocalCheckpointTracker().getMaxSeqNo()); - try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapper, 0, maxSeqNo, false)) { - Translog.Operation op; - while ((op = snapshot.next()) != null){ - operations.add(op); - } - } - return operations; - } - - /** - * 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) { - return; - } - final long maxSeqNo = ((InternalEngine) engine).getLocalCheckpointTracker().getMaxSeqNo(); - if (maxSeqNo < 0) { - return; // nothing to check - } - final Map translogOps = new HashMap<>(); - try (Translog.Snapshot snapshot = EngineTestCase.getTranslog(engine).newSnapshot()) { - Translog.Operation op; - while ((op = snapshot.next()) != null) { - translogOps.put(op.seqNo(), op); - } - } - final Map luceneOps = readAllOperationsInLucene(engine, mapper).stream() - .collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); - final long globalCheckpoint = EngineTestCase.getTranslog(engine).getLastSyncedGlobalCheckpoint(); - final long retainedOps = engine.config().getIndexSettings().getSoftDeleteRetentionOperations(); - final long seqNoForRecovery; - try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { - seqNoForRecovery = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; - } - final long minSeqNoToRetain = Math.min(seqNoForRecovery, globalCheckpoint + 1 - retainedOps); - for (Translog.Operation translogOp : translogOps.values()) { - final Translog.Operation luceneOp = luceneOps.get(translogOp.seqNo()); - if (luceneOp == null) { - if (minSeqNoToRetain <= translogOp.seqNo() && translogOp.seqNo() <= maxSeqNo) { - fail("Operation not found seq# [" + translogOp.seqNo() + "], global checkpoint [" + globalCheckpoint + "], " + - "retention policy [" + retainedOps + "], maxSeqNo [" + maxSeqNo + "], translog op [" + translogOp + "]"); - } else { - continue; - } - } - assertThat(luceneOp, notNullValue()); - assertThat(luceneOp.toString(), luceneOp.primaryTerm(), equalTo(translogOp.primaryTerm())); - assertThat(luceneOp.opType(), equalTo(translogOp.opType())); - if (luceneOp.opType() == Translog.Operation.Type.INDEX) { - assertThat(luceneOp.getSource().source, equalTo(translogOp.getSource().source)); - } - } - } - - protected MapperService createMapperService(String type) throws IOException { - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .settings(Settings.builder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1).put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)) - .putMapping(type, "{\"properties\": {}}") - .build(); - MapperService mapperService = MapperTestUtils.newMapperService(new NamedXContentRegistry(ClusterModule.getNamedXWriteables()), - createTempDir(), Settings.EMPTY, "test"); - mapperService.merge(indexMetaData, MapperService.MergeReason.MAPPING_UPDATE); - return mapperService; - } /** * Exposes a translog associated with the given engine for testing purpose. 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 f2afdff9c3a3..3f1f5daf5148 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 @@ -60,7 +60,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.InternalEngineFactory; import org.elasticsearch.index.seqno.GlobalCheckpointSyncAction; @@ -100,14 +99,10 @@ public abstract class ESIndexLevelReplicationTestCase extends IndexShardTestCase protected final Index index = new Index("test", "uuid"); private final ShardId shardId = new ShardId(index, 0); - protected final Map indexMapping = Collections.singletonMap("type", "{ \"type\": {} }"); + private final Map indexMapping = Collections.singletonMap("type", "{ \"type\": {} }"); protected ReplicationGroup createGroup(int replicas) throws IOException { - return createGroup(replicas, Settings.EMPTY); - } - - protected ReplicationGroup createGroup(int replicas, Settings settings) throws IOException { - IndexMetaData metaData = buildIndexMetaData(replicas, settings, indexMapping); + IndexMetaData metaData = buildIndexMetaData(replicas); return new ReplicationGroup(metaData); } @@ -116,17 +111,9 @@ protected IndexMetaData buildIndexMetaData(int replicas) throws IOException { } protected IndexMetaData buildIndexMetaData(int replicas, Map mappings) throws IOException { - return buildIndexMetaData(replicas, Settings.EMPTY, mappings); - } - - protected IndexMetaData buildIndexMetaData(int replicas, Settings indexSettings, Map mappings) throws IOException { Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, replicas) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), - randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) - .put(indexSettings) .build(); IndexMetaData.Builder metaData = IndexMetaData.builder(index.getName()) .settings(settings) @@ -159,7 +146,7 @@ protected class ReplicationGroup implements AutoCloseable, Iterable } }); - protected ReplicationGroup(final IndexMetaData indexMetaData) throws IOException { + ReplicationGroup(final IndexMetaData indexMetaData) throws IOException { final ShardRouting primaryRouting = this.createShardRouting("s0", true); primary = newShard(primaryRouting, indexMetaData, null, getEngineFactory(primaryRouting), () -> {}); replicas = new CopyOnWriteArrayList<>(); @@ -461,7 +448,7 @@ private void updateAllocationIDsOnPrimary() throws IOException { } } - protected abstract class ReplicationAction, + abstract class ReplicationAction, ReplicaRequest extends ReplicationRequest, Response extends ReplicationResponse> { private final Request request; @@ -469,7 +456,7 @@ protected abstract class ReplicationAction listener, ReplicationGroup group, String opType) { + ReplicationAction(Request request, ActionListener listener, ReplicationGroup group, String opType) { this.request = request; this.listener = listener; this.replicationGroup = group; @@ -595,11 +582,11 @@ public void markShardCopyAsStaleIfNeeded(ShardId shardId, String allocationId, R } } - protected class PrimaryResult implements ReplicationOperation.PrimaryResult { + class PrimaryResult implements ReplicationOperation.PrimaryResult { final ReplicaRequest replicaRequest; final Response finalResponse; - public PrimaryResult(ReplicaRequest replicaRequest, Response finalResponse) { + PrimaryResult(ReplicaRequest replicaRequest, Response finalResponse) { this.replicaRequest = replicaRequest; this.finalResponse = finalResponse; } 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 2f4a3dfd6c12..d2a84589669a 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 @@ -18,8 +18,13 @@ */ package org.elasticsearch.index.shard; +import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexNotFoundException; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.store.Directory; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.flush.FlushRequest; import org.elasticsearch.action.index.IndexRequest; @@ -52,8 +57,10 @@ import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.EngineTestCase; import org.elasticsearch.index.engine.InternalEngineFactory; +import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.SourceToParse; +import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.similarity.SimilarityService; @@ -173,63 +180,37 @@ public Directory newDirectory() throws IOException { } /** - * Creates a new initializing shard. The shard will have its own unique data path. + * creates a new initializing shard. The shard will have its own unique data path. * - * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica (ready to recover from - * another shard) + * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica + * (ready to recover from another shard) */ protected IndexShard newShard(boolean primary) throws IOException { - return newShard(primary, Settings.EMPTY, new InternalEngineFactory()); - } - - /** - * Creates a new initializing shard. The shard will have its own unique data path. - * - * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica (ready to recover from - * another shard) - * @param settings the settings to use for this shard - * @param engineFactory the engine factory to use for this shard - */ - protected IndexShard newShard(boolean primary, Settings settings, EngineFactory engineFactory) throws IOException { - final RecoverySource recoverySource = - primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE; - final ShardRouting shardRouting = - TestShardRouting.newShardRouting( - new ShardId("index", "_na_", 0), randomAlphaOfLength(10), primary, ShardRoutingState.INITIALIZING, recoverySource); - return newShard(shardRouting, settings, engineFactory); - } - - protected IndexShard newShard(ShardRouting shardRouting, final IndexingOperationListener... listeners) throws IOException { - return newShard(shardRouting, Settings.EMPTY, new InternalEngineFactory(), listeners); + ShardRouting shardRouting = TestShardRouting.newShardRouting(new ShardId("index", "_na_", 0), randomAlphaOfLength(10), primary, + ShardRoutingState.INITIALIZING, + primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE); + return newShard(shardRouting); } /** - * Creates a new initializing shard. The shard will have its own unique data path. + * creates a new initializing shard. The shard will have its own unique data path. * - * @param shardRouting the {@link ShardRouting} to use for this shard - * @param settings the settings to use for this shard - * @param engineFactory the engine factory to use for this shard - * @param listeners an optional set of listeners to add to the shard + * @param shardRouting the {@link ShardRouting} to use for this shard + * @param listeners an optional set of listeners to add to the shard */ protected IndexShard newShard( final ShardRouting shardRouting, - final Settings settings, - final EngineFactory engineFactory, final IndexingOperationListener... listeners) throws IOException { assert shardRouting.initializing() : shardRouting; - Settings indexSettings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) - .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), - randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) - .put(settings) - .build(); + 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(shardRouting.getIndexName()) - .settings(indexSettings) + .settings(settings) .primaryTerm(0, primaryTerm) .putMapping("_doc", "{ \"properties\": {} }"); - return newShard(shardRouting, metaData.build(), engineFactory, listeners); + return newShard(shardRouting, metaData.build(), listeners); } /** @@ -244,7 +225,7 @@ protected IndexShard newShard(ShardId shardId, boolean primary, IndexingOperatio ShardRouting shardRouting = TestShardRouting.newShardRouting(shardId, randomAlphaOfLength(5), primary, ShardRoutingState.INITIALIZING, primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE); - return newShard(shardRouting, Settings.EMPTY, new InternalEngineFactory(), listeners); + return newShard(shardRouting, listeners); } /** @@ -284,10 +265,9 @@ protected IndexShard newShard(ShardId shardId, boolean primary, String nodeId, I * @param indexMetaData indexMetaData for the shard, including any mapping * @param listeners an optional set of listeners to add to the shard */ - protected IndexShard newShard( - ShardRouting routing, IndexMetaData indexMetaData, EngineFactory engineFactory, IndexingOperationListener... listeners) + protected IndexShard newShard(ShardRouting routing, IndexMetaData indexMetaData, IndexingOperationListener... listeners) throws IOException { - return newShard(routing, indexMetaData, null, engineFactory, () -> {}, listeners); + return newShard(routing, indexMetaData, null, new InternalEngineFactory(), () -> {}, listeners); } /** @@ -392,39 +372,19 @@ protected IndexShard reinitShard(IndexShard current, ShardRouting routing, Index } /** - * Creates a new empty shard and starts it. The shard will randomly be a replica or a primary. + * creates a new empyu shard and starts it. The shard will be either a replica or a primary. */ protected IndexShard newStartedShard() throws IOException { return newStartedShard(randomBoolean()); } /** - * Creates a new empty shard and starts it - * @param settings the settings to use for this shard - */ - protected IndexShard newStartedShard(Settings settings) throws IOException { - return newStartedShard(randomBoolean(), settings, new InternalEngineFactory()); - } - - /** - * Creates a new empty shard and starts it. + * creates a new empty shard and starts it. * * @param primary controls whether the shard will be a primary or a replica. */ - protected IndexShard newStartedShard(final boolean primary) throws IOException { - return newStartedShard(primary, Settings.EMPTY, new InternalEngineFactory()); - } - - /** - * Creates a new empty shard with the specified settings and engine factory and starts it. - * - * @param primary controls whether the shard will be a primary or a replica. - * @param settings the settings to use for this shard - * @param engineFactory the engine factory to use for this shard - */ - protected IndexShard newStartedShard( - final boolean primary, final Settings settings, final EngineFactory engineFactory) throws IOException { - IndexShard shard = newShard(primary, settings, engineFactory); + protected IndexShard newStartedShard(boolean primary) throws IOException { + IndexShard shard = newShard(primary); if (primary) { recoverShardFromStore(shard); } else { @@ -441,7 +401,6 @@ 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()); @@ -623,7 +582,22 @@ private Store.MetadataSnapshot getMetadataSnapshotOrEmpty(IndexShard replica) th } protected Set getShardDocUIDs(final IndexShard shard) throws IOException { - return EngineTestCase.getDocIds(shard.getEngine(), true); + shard.refresh("get_uids"); + try (Engine.Searcher searcher = shard.acquireSearcher("test")) { + Set ids = new HashSet<>(); + for (LeafReaderContext leafContext : searcher.reader().leaves()) { + LeafReader reader = leafContext.reader(); + 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))); + } + } + } + return ids; + } } protected void assertDocCount(IndexShard shard, int docDount) throws IOException { @@ -636,12 +610,6 @@ protected void assertDocs(IndexShard shard, String... ids) throws IOException { assertThat(shardDocUIDs, hasSize(ids.length)); } - public static void assertConsistentHistoryBetweenTranslogAndLucene(IndexShard shard) throws IOException { - final Engine engine = shard.getEngineOrNull(); - if (engine != null) { - EngineTestCase.assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, shard.mapperService()); - } - } protected Engine.IndexResult indexDoc(IndexShard shard, String type, String id) throws IOException { return indexDoc(shard, type, id, "{}"); @@ -685,14 +653,11 @@ protected void updateMappings(IndexShard shard, IndexMetaData indexMetadata) { } protected Engine.DeleteResult deleteDoc(IndexShard shard, String type, String id) throws IOException { - final Engine.DeleteResult result; if (shard.routingEntry().primary()) { - result = shard.applyDeleteOperationOnPrimary(Versions.MATCH_ANY, type, id, VersionType.INTERNAL); - shard.updateLocalCheckpointForShard(shard.routingEntry().allocationId().getId(), shard.getEngine().getLocalCheckpoint()); + return shard.applyDeleteOperationOnPrimary(Versions.MATCH_ANY, type, id, VersionType.INTERNAL); } else { - result = shard.applyDeleteOperationOnReplica(shard.seqNoStats().getMaxSeqNo() + 1, 0L, type, id); + return shard.applyDeleteOperationOnReplica(shard.seqNoStats().getMaxSeqNo() + 1, 0L, type, id); } - return result; } protected void flushShard(IndexShard shard) { 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 be9e40ab4209..322e2a128c97 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -723,10 +723,6 @@ public Settings indexSettings() { } // always default delayed allocation to 0 to make sure we have tests are not delayed builder.put(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), 0); - builder.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()); - if (randomBoolean()) { - builder.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), between(0, 1000)); - } return builder.build(); } 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 19290f8cf118..9633f56dea94 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java @@ -41,7 +41,6 @@ import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexService; -import org.elasticsearch.index.IndexSettings; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.breaker.HierarchyCircuitBreakerService; import org.elasticsearch.node.MockNode; @@ -88,14 +87,6 @@ protected void startNode(long seed) throws Exception { .setOrder(0) .setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)).get(); - client().admin().indices() - .preparePutTemplate("random-soft-deletes-template") - .setPatterns(Collections.singletonList("*")) - .setOrder(0) - .setSettings(Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) - .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), - randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) - ).get(); } private static void stopNode() throws IOException { 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 4c813372fae3..306f79e5e16e 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -1163,26 +1163,6 @@ private void assertOpenTranslogReferences() throws Exception { }); } - /** - * Asserts that the document history in Lucene index is consistent with Translog's on every index shard of the cluster. - * This assertion might be expensive, thus we prefer not to execute on every test but only interesting tests. - */ - public void assertConsistentHistoryBetweenTranslogAndLuceneIndex() throws IOException { - final Collection nodesAndClients = nodes.values(); - for (NodeAndClient nodeAndClient : nodesAndClients) { - IndicesService indexServices = getInstance(IndicesService.class, nodeAndClient.name); - for (IndexService indexService : indexServices) { - for (IndexShard indexShard : indexService) { - try { - IndexShardTestCase.assertConsistentHistoryBetweenTranslogAndLucene(indexShard); - } catch (AlreadyClosedException ignored) { - // shard is closed - } - } - } - } - } - private void randomlyResetClients() throws IOException { // only reset the clients on nightly tests, it causes heavy load... if (RandomizedTest.isNightly() && rarely(random)) { From ad4dd086d2cef4df3008fd1a3acffe5735231ce8 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 30 Aug 2018 22:11:23 -0400 Subject: [PATCH 251/283] Integrates soft-deletes into Elasticsearch (#33222) This PR integrates Lucene soft-deletes(LUCENE-8200) into Elasticsearch. Highlight works in this PR include: - Replace hard-deletes by soft-deletes in InternalEngine - Use _recovery_source if _source is disabled or modified (#31106) - Soft-deletes retention policy based on the global checkpoint (#30335) - Read operation history from Lucene instead of translog (#30120) - Use Lucene history in peer-recovery (#30522) Relates #30086 Closes #29530 --- These works have been done by the whole team; however, these individuals (lexical order) have significant contribution in coding and reviewing: Co-authored-by: Adrien Grand Co-authored-by: Boaz Leskes Co-authored-by: Jason Tedor Co-authored-by: Martijn van Groningen Co-authored-by: Nhat Nguyen Co-authored-by: Simon Willnauer --- .../percolator/CandidateQueryTests.java | 8 +- .../PercolatorFieldMapperTests.java | 30 +- .../elasticsearch/common/lucene/Lucene.java | 86 ++- .../uid/PerThreadIDVersionAndSeqNoLookup.java | 21 +- .../common/settings/IndexScopedSettings.java | 2 + .../elasticsearch/index/IndexSettings.java | 38 ++ .../index/engine/CombinedDeletionPolicy.java | 12 +- .../elasticsearch/index/engine/Engine.java | 28 +- .../index/engine/EngineConfig.java | 27 +- .../index/engine/InternalEngine.java | 388 +++++++++-- .../index/engine/LuceneChangesSnapshot.java | 368 +++++++++++ .../RecoverySourcePruneMergePolicy.java | 292 +++++++++ .../index/engine/SoftDeletesPolicy.java | 120 ++++ .../index/fieldvisitor/FieldsVisitor.java | 10 +- .../index/mapper/DocumentMapper.java | 34 +- .../index/mapper/DocumentParser.java | 33 +- .../index/mapper/FieldNamesFieldMapper.java | 5 +- .../index/mapper/ParseContext.java | 20 +- .../index/mapper/ParsedDocument.java | 11 + .../index/mapper/SeqNoFieldMapper.java | 7 +- .../index/mapper/SourceFieldMapper.java | 16 +- .../elasticsearch/index/shard/IndexShard.java | 47 +- .../index/shard/PrimaryReplicaSyncer.java | 2 +- .../index/shard/StoreRecovery.java | 1 + .../org/elasticsearch/index/store/Store.java | 2 +- .../index/translog/Translog.java | 3 + .../index/translog/TranslogWriter.java | 20 +- .../translog/TruncateTranslogCommand.java | 2 + .../recovery/RecoverySourceHandler.java | 59 +- .../blobstore/BlobStoreRepository.java | 1 + .../snapshots/RestoreService.java | 4 +- .../cluster/routing/PrimaryAllocationIT.java | 1 + .../common/lucene/LuceneTests.java | 91 +++ .../discovery/AbstractDisruptionTestCase.java | 1 + .../gateway/RecoveryFromGatewayIT.java | 13 +- .../index/IndexServiceTests.java | 3 +- .../index/IndexSettingsTests.java | 8 + .../engine/CombinedDeletionPolicyTests.java | 69 +- .../index/engine/InternalEngineTests.java | 620 ++++++++++++------ .../engine/LuceneChangesSnapshotTests.java | 289 ++++++++ .../RecoverySourcePruneMergePolicyTests.java | 161 +++++ .../index/engine/SoftDeletesPolicyTests.java | 75 +++ .../index/mapper/DocumentParserTests.java | 10 +- .../index/mapper/DynamicMappingTests.java | 6 +- .../IndexLevelReplicationTests.java | 29 +- .../RecoveryDuringReplicationTests.java | 11 +- .../index/shard/IndexShardTests.java | 58 +- .../shard/PrimaryReplicaSyncerTests.java | 21 +- .../index/shard/RefreshListenersTests.java | 4 +- .../indices/recovery/IndexRecoveryIT.java | 6 + .../PeerRecoveryTargetServiceTests.java | 2 + .../recovery/RecoverySourceHandlerTests.java | 6 - .../indices/recovery/RecoveryTests.java | 80 ++- .../indices/stats/IndexStatsIT.java | 37 +- .../AbstractSnapshotIntegTestCase.java | 6 + .../SharedClusterSnapshotRestoreIT.java | 13 +- .../versioning/SimpleVersioningIT.java | 23 + .../index/engine/EngineTestCase.java | 400 ++++++++++- .../ESIndexLevelReplicationTestCase.java | 27 +- .../index/shard/IndexShardTestCase.java | 131 ++-- .../elasticsearch/test/ESIntegTestCase.java | 4 + .../test/ESSingleNodeTestCase.java | 9 + .../test/InternalTestCluster.java | 20 + 63 files changed, 3432 insertions(+), 499 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java create mode 100644 server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java create mode 100644 server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java create mode 100644 server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java create mode 100644 server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java index e6d637aabb14..9c8979601e8d 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/CandidateQueryTests.java @@ -77,6 +77,7 @@ import org.apache.lucene.store.RAMDirectory; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -87,6 +88,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; @@ -1109,7 +1111,11 @@ private void duelRun(PercolateQuery.QueryStore queryStore, MemoryIndex memoryInd } private void addQuery(Query query, List docs) { - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(query, parseContext); ParseContext.Document queryDocument = parseContext.doc(); diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java index ecff48b344c5..80524a2f862f 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java @@ -42,6 +42,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -58,6 +59,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.DocumentMapperParser; import org.elasticsearch.index.mapper.MapperParsingException; @@ -182,7 +184,11 @@ public void testExtractTerms() throws Exception { DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); ParseContext.Document document = parseContext.doc(); @@ -204,7 +210,7 @@ public void testExtractTerms() throws Exception { bq.add(termQuery1, Occur.MUST); bq.add(termQuery2, Occur.MUST); - parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, mapperService.documentMapperParser(), + parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); document = parseContext.doc(); @@ -232,8 +238,12 @@ public void testExtractRanges() throws Exception { bq.add(rangeQuery2, Occur.MUST); DocumentMapper documentMapper = mapperService.documentMapper("doc"); + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); ParseContext.Document document = parseContext.doc(); @@ -259,7 +269,7 @@ public void testExtractRanges() throws Exception { .rangeQuery(15, 20, true, true, null, null, null, null); bq.add(rangeQuery2, Occur.MUST); - parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(bq.build(), parseContext); document = parseContext.doc(); @@ -283,7 +293,11 @@ public void testExtractTermsAndRanges_failed() throws Exception { TermRangeQuery query = new TermRangeQuery("field1", new BytesRef("a"), new BytesRef("z"), true, true); DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(query, parseContext); ParseContext.Document document = parseContext.doc(); @@ -298,7 +312,11 @@ public void testExtractTermsAndRanges_partial() throws Exception { PhraseQuery phraseQuery = new PhraseQuery("field", "term"); DocumentMapper documentMapper = mapperService.documentMapper("doc"); PercolatorFieldMapper fieldMapper = (PercolatorFieldMapper) documentMapper.mappers().getMapper(fieldName); - ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(Settings.EMPTY, + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext.InternalParseContext parseContext = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), documentMapper, null, null); fieldMapper.processQuery(phraseQuery, parseContext); ParseContext.Document document = parseContext.doc(); diff --git a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java index a24a6aea07fc..1c1e56878932 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/Lucene.java @@ -27,8 +27,10 @@ import org.apache.lucene.codecs.DocValuesFormat; import org.apache.lucene.codecs.PostingsFormat; import org.apache.lucene.document.LatLonDocValuesField; +import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.FilterDirectoryReader; import org.apache.lucene.index.FilterLeafReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexFileNames; @@ -96,6 +98,8 @@ public class Lucene { assert annotation == null : "DocValuesFormat " + LATEST_DOC_VALUES_FORMAT + " is deprecated" ; } + public static final String SOFT_DELETES_FIELD = "__soft_deletes"; + public static final NamedAnalyzer STANDARD_ANALYZER = new NamedAnalyzer("_standard", AnalyzerScope.GLOBAL, new StandardAnalyzer()); public static final NamedAnalyzer KEYWORD_ANALYZER = new NamedAnalyzer("_keyword", AnalyzerScope.GLOBAL, new KeywordAnalyzer()); @@ -140,7 +144,7 @@ public static Iterable files(SegmentInfos infos) throws IOException { public static int getNumDocs(SegmentInfos info) { int numDocs = 0; for (SegmentCommitInfo si : info) { - numDocs += si.info.maxDoc() - si.getDelCount(); + numDocs += si.info.maxDoc() - si.getDelCount() - si.getSoftDelCount(); } return numDocs; } @@ -197,6 +201,7 @@ public static SegmentInfos pruneUnreferencedFiles(String segmentsFileName, Direc } final CommitPoint cp = new CommitPoint(si, directory); try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(Lucene.STANDARD_ANALYZER) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setIndexCommit(cp) .setCommitOnClose(false) .setMergePolicy(NoMergePolicy.INSTANCE) @@ -220,6 +225,7 @@ public static void cleanLuceneIndex(Directory directory) throws IOException { } } try (IndexWriter writer = new IndexWriter(directory, new IndexWriterConfig(Lucene.STANDARD_ANALYZER) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setMergePolicy(NoMergePolicy.INSTANCE) // no merges .setCommitOnClose(false) // no commits .setOpenMode(IndexWriterConfig.OpenMode.CREATE))) // force creation - don't append... @@ -829,4 +835,82 @@ public int length() { } }; } + + /** + * Wraps a directory reader to make all documents live except those were rolled back + * or hard-deleted due to non-aborting exceptions during indexing. + * The wrapped reader can be used to query all documents. + * + * @param in the input directory reader + * @return the wrapped reader + */ + public static DirectoryReader wrapAllDocsLive(DirectoryReader in) throws IOException { + return new DirectoryReaderWithAllLiveDocs(in); + } + + private static final class DirectoryReaderWithAllLiveDocs extends FilterDirectoryReader { + static final class LeafReaderWithLiveDocs extends FilterLeafReader { + final Bits liveDocs; + final int numDocs; + LeafReaderWithLiveDocs(LeafReader in, Bits liveDocs, int numDocs) { + super(in); + this.liveDocs = liveDocs; + this.numDocs = numDocs; + } + @Override + public Bits getLiveDocs() { + return liveDocs; + } + @Override + public int numDocs() { + return numDocs; + } + @Override + public CacheHelper getCoreCacheHelper() { + return in.getCoreCacheHelper(); + } + @Override + public CacheHelper getReaderCacheHelper() { + return null; // Modifying liveDocs + } + } + + DirectoryReaderWithAllLiveDocs(DirectoryReader in) throws IOException { + super(in, new SubReaderWrapper() { + @Override + public LeafReader wrap(LeafReader leaf) { + SegmentReader segmentReader = segmentReader(leaf); + Bits hardLiveDocs = segmentReader.getHardLiveDocs(); + if (hardLiveDocs == null) { + return new LeafReaderWithLiveDocs(leaf, null, leaf.maxDoc()); + } + // TODO: Can we avoid calculate numDocs by using SegmentReader#getSegmentInfo with LUCENE-8458? + int numDocs = 0; + for (int i = 0; i < hardLiveDocs.length(); i++) { + if (hardLiveDocs.get(i)) { + numDocs++; + } + } + return new LeafReaderWithLiveDocs(segmentReader, hardLiveDocs, numDocs); + } + }); + } + + @Override + protected DirectoryReader doWrapDirectoryReader(DirectoryReader in) throws IOException { + return wrapAllDocsLive(in); + } + + @Override + public CacheHelper getReaderCacheHelper() { + return null; // Modifying liveDocs + } + } + + /** + * Returns a numeric docvalues which can be used to soft-delete documents. + */ + public static NumericDocValuesField newSoftDeletesField() { + return new NumericDocValuesField(SOFT_DELETES_FIELD, 1); + } } diff --git a/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java b/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java index 38fcdfe5f1b6..3a037bed62b7 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/uid/PerThreadIDVersionAndSeqNoLookup.java @@ -28,6 +28,7 @@ import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndSeqNo; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndVersion; import org.elasticsearch.index.mapper.SeqNoFieldMapper; @@ -66,15 +67,22 @@ final class PerThreadIDVersionAndSeqNoLookup { */ PerThreadIDVersionAndSeqNoLookup(LeafReader reader, String uidField) throws IOException { this.uidField = uidField; - Terms terms = reader.terms(uidField); + final Terms terms = reader.terms(uidField); if (terms == null) { - throw new IllegalArgumentException("reader misses the [" + uidField + "] field"); + // If a segment contains only no-ops, it does not have _uid but has both _soft_deletes and _tombstone fields. + final NumericDocValues softDeletesDV = reader.getNumericDocValues(Lucene.SOFT_DELETES_FIELD); + final NumericDocValues tombstoneDV = reader.getNumericDocValues(SeqNoFieldMapper.TOMBSTONE_NAME); + if (softDeletesDV == null || tombstoneDV == null) { + throw new IllegalArgumentException("reader does not have _uid terms but not a no-op segment; " + + "_soft_deletes [" + softDeletesDV + "], _tombstone [" + tombstoneDV + "]"); + } + termsEnum = null; + } else { + termsEnum = terms.iterator(); } - termsEnum = terms.iterator(); if (reader.getNumericDocValues(VersionFieldMapper.NAME) == null) { - throw new IllegalArgumentException("reader misses the [" + VersionFieldMapper.NAME + "] field"); + throw new IllegalArgumentException("reader misses the [" + VersionFieldMapper.NAME + "] field; _uid terms [" + terms + "]"); } - Object readerKey = null; assert (readerKey = reader.getCoreCacheHelper().getKey()) != null; this.readerKey = readerKey; @@ -111,7 +119,8 @@ public DocIdAndVersion lookupVersion(BytesRef id, LeafReaderContext context) * {@link DocIdSetIterator#NO_MORE_DOCS} is returned if not found * */ private int getDocID(BytesRef id, Bits liveDocs) throws IOException { - if (termsEnum.seekExact(id)) { + // termsEnum can possibly be null here if this leaf contains only no-ops. + if (termsEnum != null && termsEnum.seekExact(id)) { int docID = DocIdSetIterator.NO_MORE_DOCS; // there may be more than one matching docID, in the case of nested docs, so we want the last one: docsEnum = termsEnum.postings(docsEnum, 0); diff --git a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java index 46e3867f7aea..f3de294046c4 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java @@ -129,6 +129,8 @@ public final class IndexScopedSettings extends AbstractScopedSettings { IndexSettings.MAX_REGEX_LENGTH_SETTING, ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING, IndexSettings.INDEX_GC_DELETES_SETTING, + IndexSettings.INDEX_SOFT_DELETES_SETTING, + IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING, IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED_SETTING, UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING, EnableAllocationDecider.INDEX_ROUTING_REBALANCE_ENABLE_SETTING, diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 44cd743bbd42..3ea022bbebd4 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -237,6 +237,21 @@ public final class IndexSettings { public static final Setting INDEX_GC_DELETES_SETTING = Setting.timeSetting("index.gc_deletes", DEFAULT_GC_DELETES, new TimeValue(-1, TimeUnit.MILLISECONDS), Property.Dynamic, Property.IndexScope); + + /** + * Specifies if the index should use soft-delete instead of hard-delete for update/delete operations. + */ + public static final Setting INDEX_SOFT_DELETES_SETTING = + Setting.boolSetting("index.soft_deletes.enabled", false, Property.IndexScope, Property.Final); + + /** + * Controls how many soft-deleted documents will be kept around before being merged away. Keeping more deleted + * documents increases the chance of operation-based recoveries and allows querying a longer history of documents. + * If soft-deletes is enabled, an engine by default will retain all operations up to the global checkpoint. + **/ + public static final Setting INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING = + Setting.longSetting("index.soft_deletes.retention.operations", 0, 0, Property.IndexScope, Property.Dynamic); + /** * The maximum number of refresh listeners allows on this shard. */ @@ -289,6 +304,8 @@ public final class IndexSettings { private final IndexSortConfig indexSortConfig; private final IndexScopedSettings scopedSettings; private long gcDeletesInMillis = DEFAULT_GC_DELETES.millis(); + private final boolean softDeleteEnabled; + private volatile long softDeleteRetentionOperations; private volatile boolean warmerEnabled; private volatile int maxResultWindow; private volatile int maxInnerResultWindow; @@ -400,6 +417,8 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti generationThresholdSize = scopedSettings.get(INDEX_TRANSLOG_GENERATION_THRESHOLD_SIZE_SETTING); mergeSchedulerConfig = new MergeSchedulerConfig(this); gcDeletesInMillis = scopedSettings.get(INDEX_GC_DELETES_SETTING).getMillis(); + softDeleteEnabled = version.onOrAfter(Version.V_7_0_0_alpha1) && scopedSettings.get(INDEX_SOFT_DELETES_SETTING); + softDeleteRetentionOperations = scopedSettings.get(INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING); warmerEnabled = scopedSettings.get(INDEX_WARMER_ENABLED_SETTING); maxResultWindow = scopedSettings.get(MAX_RESULT_WINDOW_SETTING); maxInnerResultWindow = scopedSettings.get(MAX_INNER_RESULT_WINDOW_SETTING); @@ -458,6 +477,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti scopedSettings.addSettingsUpdateConsumer(INDEX_SEARCH_IDLE_AFTER, this::setSearchIdleAfter); scopedSettings.addSettingsUpdateConsumer(MAX_REGEX_LENGTH_SETTING, this::setMaxRegexLength); scopedSettings.addSettingsUpdateConsumer(DEFAULT_PIPELINE, this::setDefaultPipeline); + scopedSettings.addSettingsUpdateConsumer(INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING, this::setSoftDeleteRetentionOperations); } private void setSearchIdleAfter(TimeValue searchIdleAfter) { this.searchIdleAfter = searchIdleAfter; } @@ -841,4 +861,22 @@ public String getDefaultPipeline() { public void setDefaultPipeline(String defaultPipeline) { this.defaultPipeline = defaultPipeline; } + + /** + * Returns true if soft-delete is enabled. + */ + public boolean isSoftDeleteEnabled() { + return softDeleteEnabled; + } + + private void setSoftDeleteRetentionOperations(long ops) { + this.softDeleteRetentionOperations = ops; + } + + /** + * Returns the number of extra operations (i.e. soft-deleted documents) to be kept for recoveries and history purpose. + */ + public long getSoftDeleteRetentionOperations() { + return this.softDeleteRetentionOperations; + } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java index d0575c8a8c97..d10690379edd 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java +++ b/server/src/main/java/org/elasticsearch/index/engine/CombinedDeletionPolicy.java @@ -46,14 +46,17 @@ public final class CombinedDeletionPolicy extends IndexDeletionPolicy { private final Logger logger; private final TranslogDeletionPolicy translogDeletionPolicy; + private final SoftDeletesPolicy softDeletesPolicy; private final LongSupplier globalCheckpointSupplier; private final ObjectIntHashMap snapshottedCommits; // Number of snapshots held against each commit point. private volatile IndexCommit safeCommit; // the most recent safe commit point - its max_seqno at most the persisted global checkpoint. private volatile IndexCommit lastCommit; // the most recent commit point - CombinedDeletionPolicy(Logger logger, TranslogDeletionPolicy translogDeletionPolicy, LongSupplier globalCheckpointSupplier) { + CombinedDeletionPolicy(Logger logger, TranslogDeletionPolicy translogDeletionPolicy, + SoftDeletesPolicy softDeletesPolicy, LongSupplier globalCheckpointSupplier) { this.logger = logger; this.translogDeletionPolicy = translogDeletionPolicy; + this.softDeletesPolicy = softDeletesPolicy; this.globalCheckpointSupplier = globalCheckpointSupplier; this.snapshottedCommits = new ObjectIntHashMap<>(); } @@ -80,7 +83,7 @@ public synchronized void onCommit(List commits) throws IO deleteCommit(commits.get(i)); } } - updateTranslogDeletionPolicy(); + updateRetentionPolicy(); } private void deleteCommit(IndexCommit commit) throws IOException { @@ -90,7 +93,7 @@ private void deleteCommit(IndexCommit commit) throws IOException { assert commit.isDeleted() : "Deletion commit [" + commitDescription(commit) + "] was suppressed"; } - private void updateTranslogDeletionPolicy() throws IOException { + private void updateRetentionPolicy() throws IOException { assert Thread.holdsLock(this); logger.debug("Safe commit [{}], last commit [{}]", commitDescription(safeCommit), commitDescription(lastCommit)); assert safeCommit.isDeleted() == false : "The safe commit must not be deleted"; @@ -101,6 +104,9 @@ private void updateTranslogDeletionPolicy() throws IOException { assert minRequiredGen <= lastGen : "minRequiredGen must not be greater than lastGen"; translogDeletionPolicy.setTranslogGenerationOfLastCommit(lastGen); translogDeletionPolicy.setMinTranslogGenerationForRecovery(minRequiredGen); + + softDeletesPolicy.setLocalCheckpointOfSafeCommit( + Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY))); } /** 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 4d95cf89ef00..08724d6e7942 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/Engine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/Engine.java @@ -58,6 +58,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.index.mapper.ParsedDocument; @@ -97,6 +98,7 @@ public abstract class Engine implements Closeable { public static final String SYNC_COMMIT_ID = "sync_id"; public static final String HISTORY_UUID_KEY = "history_uuid"; + public static final String MIN_RETAINED_SEQNO = "min_retained_seq_no"; protected final ShardId shardId; protected final String allocationId; @@ -585,18 +587,32 @@ public enum SearcherScope { public abstract void syncTranslog() throws IOException; - public abstract Closeable acquireTranslogRetentionLock(); + /** + * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed + */ + public abstract Closeable acquireRetentionLockForPeerRecovery(); + + /** + * Creates a new history snapshot from Lucene for reading operations whose seqno in the requesting seqno range (both inclusive) + */ + public abstract Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, + long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException; + + /** + * Creates a new history snapshot for reading operations since {@code startingSeqNo} (inclusive). + * The returned snapshot can be retrieved from either Lucene index or translog files. + */ + public abstract Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException; /** - * Creates a new translog snapshot from this engine for reading translog operations whose seq# at least the provided seq#. - * The caller has to close the returned snapshot after finishing the reading. + * Returns the estimated number of history operations whose seq# at least {@code startingSeqNo}(inclusive) in this engine. */ - public abstract Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException; + public abstract int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException; /** - * Returns the estimated number of translog operations in this engine whose seq# at least the provided seq#. + * Checks if this engine has every operations since {@code startingSeqNo}(inclusive) in its history (either Lucene or translog) */ - public abstract int estimateTranslogOperationsFromMinSeq(long minSeqNo); + public abstract boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException; public abstract TranslogStats getTranslogStats(); diff --git a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java index 2deae61bd52e..23a90553f60a 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java +++ b/server/src/main/java/org/elasticsearch/index/engine/EngineConfig.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.codec.CodecService; +import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.store.Store; import org.elasticsearch.index.translog.Translog; @@ -80,6 +81,7 @@ public final class EngineConfig { private final CircuitBreakerService circuitBreakerService; private final LongSupplier globalCheckpointSupplier; private final LongSupplier primaryTermSupplier; + private final TombstoneDocSupplier tombstoneDocSupplier; /** * Index setting to change the low level lucene codec used for writing new segments. @@ -126,7 +128,8 @@ public EngineConfig(ShardId shardId, String allocationId, ThreadPool threadPool, List externalRefreshListener, List internalRefreshListener, Sort indexSort, TranslogRecoveryRunner translogRecoveryRunner, CircuitBreakerService circuitBreakerService, - LongSupplier globalCheckpointSupplier, LongSupplier primaryTermSupplier) { + LongSupplier globalCheckpointSupplier, LongSupplier primaryTermSupplier, + TombstoneDocSupplier tombstoneDocSupplier) { this.shardId = shardId; this.allocationId = allocationId; this.indexSettings = indexSettings; @@ -164,6 +167,7 @@ public EngineConfig(ShardId shardId, String allocationId, ThreadPool threadPool, this.circuitBreakerService = circuitBreakerService; this.globalCheckpointSupplier = globalCheckpointSupplier; this.primaryTermSupplier = primaryTermSupplier; + this.tombstoneDocSupplier = tombstoneDocSupplier; } /** @@ -373,4 +377,25 @@ public CircuitBreakerService getCircuitBreakerService() { public LongSupplier getPrimaryTermSupplier() { return primaryTermSupplier; } + + /** + * A supplier supplies tombstone documents which will be used in soft-update methods. + * The returned document consists only _uid, _seqno, _term and _version fields; other metadata fields are excluded. + */ + public interface TombstoneDocSupplier { + /** + * Creates a tombstone document for a delete operation. + */ + ParsedDocument newDeleteTombstoneDoc(String type, String id); + + /** + * Creates a tombstone document for a noop operation. + * @param reason the reason of an a noop + */ + ParsedDocument newNoopTombstoneDoc(String reason); + } + + public TombstoneDocSupplier getTombstoneDocSupplier() { + return tombstoneDocSupplier; + } } 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 023e659ffabe..da4decc93b1c 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -21,16 +21,20 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LiveIndexWriterConfig; import org.apache.lucene.index.MergePolicy; import org.apache.lucene.index.SegmentCommitInfo; import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.ReferenceManager; @@ -42,6 +46,7 @@ import org.apache.lucene.store.LockObtainFailedException; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.InfoStream; +import org.elasticsearch.Assertions; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.common.Nullable; @@ -61,7 +66,11 @@ import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.mapper.IdFieldMapper; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.ParseContext; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.merge.MergeStats; import org.elasticsearch.index.merge.OnGoingMerge; import org.elasticsearch.index.seqno.LocalCheckpointTracker; @@ -140,6 +149,10 @@ public class InternalEngine extends Engine { private final CounterMetric numDocDeletes = new CounterMetric(); private final CounterMetric numDocAppends = new CounterMetric(); private final CounterMetric numDocUpdates = new CounterMetric(); + private final NumericDocValuesField softDeletesField = Lucene.newSoftDeletesField(); + private final boolean softDeleteEnabled; + private final SoftDeletesPolicy softDeletesPolicy; + private final LastRefreshedCheckpointListener lastRefreshedCheckpointListener; /** * How many bytes we are currently moving to disk, via either IndexWriter.flush or refresh. IndexingMemoryController polls this @@ -184,8 +197,10 @@ public InternalEngine(EngineConfig engineConfig) { assert translog.getGeneration() != null; this.translog = translog; this.localCheckpointTracker = createLocalCheckpointTracker(localCheckpointTrackerSupplier); + this.softDeleteEnabled = engineConfig.getIndexSettings().isSoftDeleteEnabled(); + this.softDeletesPolicy = newSoftDeletesPolicy(); this.combinedDeletionPolicy = - new CombinedDeletionPolicy(logger, translogDeletionPolicy, translog::getLastSyncedGlobalCheckpoint); + new CombinedDeletionPolicy(logger, translogDeletionPolicy, softDeletesPolicy, translog::getLastSyncedGlobalCheckpoint); writer = createWriter(); bootstrapAppendOnlyInfoFromWriter(writer); historyUUID = loadHistoryUUID(writer); @@ -215,6 +230,8 @@ public InternalEngine(EngineConfig engineConfig) { for (ReferenceManager.RefreshListener listener: engineConfig.getInternalRefreshListener()) { this.internalSearcherManager.addListener(listener); } + this.lastRefreshedCheckpointListener = new LastRefreshedCheckpointListener(localCheckpointTracker.getCheckpoint()); + this.internalSearcherManager.addListener(lastRefreshedCheckpointListener); success = true; } finally { if (success == false) { @@ -240,6 +257,18 @@ private LocalCheckpointTracker createLocalCheckpointTracker( return localCheckpointTrackerSupplier.apply(maxSeqNo, localCheckpoint); } + private SoftDeletesPolicy newSoftDeletesPolicy() throws IOException { + final Map commitUserData = store.readLastCommittedSegmentsInfo().userData; + final long lastMinRetainedSeqNo; + if (commitUserData.containsKey(Engine.MIN_RETAINED_SEQNO)) { + lastMinRetainedSeqNo = Long.parseLong(commitUserData.get(Engine.MIN_RETAINED_SEQNO)); + } else { + lastMinRetainedSeqNo = Long.parseLong(commitUserData.get(SequenceNumbers.MAX_SEQ_NO)) + 1; + } + return new SoftDeletesPolicy(translog::getLastSyncedGlobalCheckpoint, lastMinRetainedSeqNo, + engineConfig.getIndexSettings().getSoftDeleteRetentionOperations()); + } + /** * This reference manager delegates all it's refresh calls to another (internal) SearcherManager * The main purpose for this is that if we have external refreshes happening we don't issue extra @@ -451,19 +480,31 @@ public void syncTranslog() throws IOException { revisitIndexDeletionPolicyOnTranslogSynced(); } + /** + * Creates a new history snapshot for reading operations since the provided seqno. + * The returned snapshot can be retrieved from either Lucene index or translog files. + */ @Override - public Closeable acquireTranslogRetentionLock() { - return getTranslog().acquireRetentionLock(); - } - - @Override - public Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException { - return getTranslog().newSnapshotFromMinSeqNo(minSeqNo); + public Translog.Snapshot readHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + return newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false); + } else { + return getTranslog().newSnapshotFromMinSeqNo(startingSeqNo); + } } + /** + * Returns the estimated number of history operations whose seq# at least the provided seq# in this engine. + */ @Override - public int estimateTranslogOperationsFromMinSeq(long minSeqNo) { - return getTranslog().estimateTotalOperationsFromMinSeq(minSeqNo); + public int estimateNumberOfHistoryOperations(String source, MapperService mapperService, long startingSeqNo) throws IOException { + if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + try (Translog.Snapshot snapshot = newChangesSnapshot(source, mapperService, Math.max(0, startingSeqNo), Long.MAX_VALUE, false)) { + return snapshot.totalOperations(); + } + } else { + return getTranslog().estimateTotalOperationsFromMinSeq(startingSeqNo); + } } @Override @@ -790,7 +831,7 @@ public IndexResult index(Index index) throws IOException { if (plan.earlyResultOnPreFlightError.isPresent()) { indexResult = plan.earlyResultOnPreFlightError.get(); assert indexResult.getResultType() == Result.Type.FAILURE : indexResult.getResultType(); - } else if (plan.indexIntoLucene) { + } else if (plan.indexIntoLucene || plan.addStaleOpToLucene) { indexResult = indexIntoLucene(index, plan); } else { indexResult = new IndexResult( @@ -801,8 +842,10 @@ public IndexResult index(Index index) throws IOException { if (indexResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Index(index, indexResult)); } else if (indexResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - // if we have document failure, record it as a no-op in the translog with the generated seq_no - location = translog.add(new Translog.NoOp(indexResult.getSeqNo(), index.primaryTerm(), indexResult.getFailure().toString())); + // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no + final NoOp noOp = new NoOp(indexResult.getSeqNo(), index.primaryTerm(), index.origin(), + index.startTime(), indexResult.getFailure().toString()); + location = innerNoOp(noOp).getTranslogLocation(); } else { location = null; } @@ -854,7 +897,6 @@ private IndexingStrategy planIndexingAsNonPrimary(Index index) throws IOExceptio // unlike the primary, replicas don't really care to about creation status of documents // this allows to ignore the case where a document was found in the live version maps in // a delete state and return false for the created flag in favor of code simplicity - final OpVsLuceneDocStatus opVsLucene; if (index.seqNo() <= localCheckpointTracker.getCheckpoint()){ // the operation seq# is lower then the current local checkpoint and thus was already put into lucene // this can happen during recovery where older operations are sent from the translog that are already @@ -863,16 +905,15 @@ private IndexingStrategy planIndexingAsNonPrimary(Index index) throws IOExceptio // question may have been deleted in an out of order op that is not replayed. // See testRecoverFromStoreWithOutOfOrderDelete for an example of local recovery // See testRecoveryWithOutOfOrderDelete for an example of peer recovery - opVsLucene = OpVsLuceneDocStatus.OP_STALE_OR_EQUAL; - } else { - opVsLucene = compareOpToLuceneDocBasedOnSeqNo(index); - } - if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { plan = IndexingStrategy.processButSkipLucene(false, index.seqNo(), index.version()); } else { - plan = IndexingStrategy.processNormally( - opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, index.seqNo(), index.version() - ); + final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(index); + if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { + plan = IndexingStrategy.processAsStaleOp(softDeleteEnabled, index.seqNo(), index.version()); + } else { + plan = IndexingStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, + index.seqNo(), index.version()); + } } } return plan; @@ -921,7 +962,7 @@ private IndexResult indexIntoLucene(Index index, IndexingStrategy plan) throws IOException { assert plan.seqNoForIndexing >= 0 : "ops should have an assigned seq no.; origin: " + index.origin(); assert plan.versionForIndexing >= 0 : "version must be set. got " + plan.versionForIndexing; - assert plan.indexIntoLucene; + assert plan.indexIntoLucene || plan.addStaleOpToLucene; /* Update the document's sequence number and primary term; the sequence number here is derived here from either the sequence * number service if this is on the primary, or the existing document's sequence number if this is on the replica. The * primary term here has already been set, see IndexShard#prepareIndex where the Engine$Index operation is created. @@ -929,7 +970,9 @@ private IndexResult indexIntoLucene(Index index, IndexingStrategy plan) index.parsedDoc().updateSeqID(plan.seqNoForIndexing, index.primaryTerm()); index.parsedDoc().version().setLongValue(plan.versionForIndexing); try { - if (plan.useLuceneUpdateDocument) { + if (plan.addStaleOpToLucene) { + addStaleDocs(index.docs(), indexWriter); + } else if (plan.useLuceneUpdateDocument) { updateDocs(index.uid(), index.docs(), indexWriter); } else { // document does not exists, we can optimize for create, but double check if assertions are running @@ -993,16 +1036,29 @@ private void addDocs(final List docs, final IndexWriter i numDocAppends.inc(docs.size()); } - private static final class IndexingStrategy { + private void addStaleDocs(final List docs, final IndexWriter indexWriter) throws IOException { + assert softDeleteEnabled : "Add history documents but soft-deletes is disabled"; + for (ParseContext.Document doc : docs) { + doc.add(softDeletesField); // soft-deleted every document before adding to Lucene + } + if (docs.size() > 1) { + indexWriter.addDocuments(docs); + } else { + indexWriter.addDocument(docs.get(0)); + } + } + + protected static final class IndexingStrategy { final boolean currentNotFoundOrDeleted; final boolean useLuceneUpdateDocument; final long seqNoForIndexing; final long versionForIndexing; final boolean indexIntoLucene; + final boolean addStaleOpToLucene; final Optional earlyResultOnPreFlightError; private IndexingStrategy(boolean currentNotFoundOrDeleted, boolean useLuceneUpdateDocument, - boolean indexIntoLucene, long seqNoForIndexing, + boolean indexIntoLucene, boolean addStaleOpToLucene, long seqNoForIndexing, long versionForIndexing, IndexResult earlyResultOnPreFlightError) { assert useLuceneUpdateDocument == false || indexIntoLucene : "use lucene update is set to true, but we're not indexing into lucene"; @@ -1015,37 +1071,40 @@ private IndexingStrategy(boolean currentNotFoundOrDeleted, boolean useLuceneUpda this.seqNoForIndexing = seqNoForIndexing; this.versionForIndexing = versionForIndexing; this.indexIntoLucene = indexIntoLucene; + this.addStaleOpToLucene = addStaleOpToLucene; this.earlyResultOnPreFlightError = earlyResultOnPreFlightError == null ? Optional.empty() : Optional.of(earlyResultOnPreFlightError); } static IndexingStrategy optimizedAppendOnly(long seqNoForIndexing) { - return new IndexingStrategy(true, false, true, seqNoForIndexing, 1, null); + return new IndexingStrategy(true, false, true, false, seqNoForIndexing, 1, null); } static IndexingStrategy skipDueToVersionConflict( VersionConflictEngineException e, boolean currentNotFoundOrDeleted, long currentVersion, long term) { final IndexResult result = new IndexResult(e, currentVersion, term); return new IndexingStrategy( - currentNotFoundOrDeleted, false, false, SequenceNumbers.UNASSIGNED_SEQ_NO, Versions.NOT_FOUND, result); + currentNotFoundOrDeleted, false, false, false, SequenceNumbers.UNASSIGNED_SEQ_NO, Versions.NOT_FOUND, result); } static IndexingStrategy processNormally(boolean currentNotFoundOrDeleted, long seqNoForIndexing, long versionForIndexing) { return new IndexingStrategy(currentNotFoundOrDeleted, currentNotFoundOrDeleted == false, - true, seqNoForIndexing, versionForIndexing, null); + true, false, seqNoForIndexing, versionForIndexing, null); } static IndexingStrategy overrideExistingAsIfNotThere( long seqNoForIndexing, long versionForIndexing) { - return new IndexingStrategy(true, true, true, seqNoForIndexing, versionForIndexing, null); + return new IndexingStrategy(true, true, true, false, seqNoForIndexing, versionForIndexing, null); + } + + static IndexingStrategy processButSkipLucene(boolean currentNotFoundOrDeleted, long seqNoForIndexing, long versionForIndexing) { + return new IndexingStrategy(currentNotFoundOrDeleted, false, false, false, seqNoForIndexing, versionForIndexing, null); } - static IndexingStrategy processButSkipLucene(boolean currentNotFoundOrDeleted, - long seqNoForIndexing, long versionForIndexing) { - return new IndexingStrategy(currentNotFoundOrDeleted, false, - false, seqNoForIndexing, versionForIndexing, null); + static IndexingStrategy processAsStaleOp(boolean addStaleOpToLucene, long seqNoForIndexing, long versionForIndexing) { + return new IndexingStrategy(false, false, false, addStaleOpToLucene, seqNoForIndexing, versionForIndexing, null); } } @@ -1072,10 +1131,18 @@ private boolean assertDocDoesNotExist(final Index index, final boolean allowDele } private void updateDocs(final Term uid, final List docs, final IndexWriter indexWriter) throws IOException { - if (docs.size() > 1) { - indexWriter.updateDocuments(uid, docs); + if (softDeleteEnabled) { + if (docs.size() > 1) { + indexWriter.softUpdateDocuments(uid, docs, softDeletesField); + } else { + indexWriter.softUpdateDocument(uid, docs.get(0), softDeletesField); + } } else { - indexWriter.updateDocument(uid, docs.get(0)); + if (docs.size() > 1) { + indexWriter.updateDocuments(uid, docs); + } else { + indexWriter.updateDocument(uid, docs.get(0)); + } } numDocUpdates.inc(docs.size()); } @@ -1099,7 +1166,7 @@ public DeleteResult delete(Delete delete) throws IOException { if (plan.earlyResultOnPreflightError.isPresent()) { deleteResult = plan.earlyResultOnPreflightError.get(); - } else if (plan.deleteFromLucene) { + } else if (plan.deleteFromLucene || plan.addStaleOpToLucene) { deleteResult = deleteInLucene(delete, plan); } else { deleteResult = new DeleteResult( @@ -1110,8 +1177,10 @@ public DeleteResult delete(Delete delete) throws IOException { if (deleteResult.getResultType() == Result.Type.SUCCESS) { location = translog.add(new Translog.Delete(delete, deleteResult)); } else if (deleteResult.getSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - location = translog.add(new Translog.NoOp(deleteResult.getSeqNo(), - delete.primaryTerm(), deleteResult.getFailure().toString())); + // if we have document failure, record it as a no-op in the translog and Lucene with the generated seq_no + final NoOp noOp = new NoOp(deleteResult.getSeqNo(), delete.primaryTerm(), delete.origin(), + delete.startTime(), deleteResult.getFailure().toString()); + location = innerNoOp(noOp).getTranslogLocation(); } else { location = null; } @@ -1142,7 +1211,7 @@ private DeletionStrategy planDeletionAsNonPrimary(Delete delete) throws IOExcept // unlike the primary, replicas don't really care to about found status of documents // this allows to ignore the case where a document was found in the live version maps in // a delete state and return true for the found flag in favor of code simplicity - final OpVsLuceneDocStatus opVsLucene; + final DeletionStrategy plan; if (delete.seqNo() <= localCheckpointTracker.getCheckpoint()) { // the operation seq# is lower then the current local checkpoint and thus was already put into lucene // this can happen during recovery where older operations are sent from the translog that are already @@ -1151,18 +1220,15 @@ private DeletionStrategy planDeletionAsNonPrimary(Delete delete) throws IOExcept // question may have been deleted in an out of order op that is not replayed. // See testRecoverFromStoreWithOutOfOrderDelete for an example of local recovery // See testRecoveryWithOutOfOrderDelete for an example of peer recovery - opVsLucene = OpVsLuceneDocStatus.OP_STALE_OR_EQUAL; - } else { - opVsLucene = compareOpToLuceneDocBasedOnSeqNo(delete); - } - - final DeletionStrategy plan; - if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { plan = DeletionStrategy.processButSkipLucene(false, delete.seqNo(), delete.version()); } else { - plan = DeletionStrategy.processNormally( - opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, - delete.seqNo(), delete.version()); + final OpVsLuceneDocStatus opVsLucene = compareOpToLuceneDocBasedOnSeqNo(delete); + if (opVsLucene == OpVsLuceneDocStatus.OP_STALE_OR_EQUAL) { + plan = DeletionStrategy.processAsStaleOp(softDeleteEnabled, false, delete.seqNo(), delete.version()); + } else { + plan = DeletionStrategy.processNormally(opVsLucene == OpVsLuceneDocStatus.LUCENE_DOC_NOT_FOUND, + delete.seqNo(), delete.version()); + } } return plan; } @@ -1197,15 +1263,31 @@ private DeletionStrategy planDeletionAsPrimary(Delete delete) throws IOException private DeleteResult deleteInLucene(Delete delete, DeletionStrategy plan) throws IOException { try { - if (plan.currentlyDeleted == false) { + if (softDeleteEnabled) { + final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newDeleteTombstoneDoc(delete.type(), delete.id()); + assert tombstone.docs().size() == 1 : "Tombstone doc should have single doc [" + tombstone + "]"; + tombstone.updateSeqID(plan.seqNoOfDeletion, delete.primaryTerm()); + tombstone.version().setLongValue(plan.versionOfDeletion); + final ParseContext.Document doc = tombstone.docs().get(0); + assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null : + "Delete tombstone document but _tombstone field is not set [" + doc + " ]"; + doc.add(softDeletesField); + if (plan.addStaleOpToLucene || plan.currentlyDeleted) { + indexWriter.addDocument(doc); + } else { + indexWriter.softUpdateDocument(delete.uid(), doc, softDeletesField); + } + } else if (plan.currentlyDeleted == false) { // any exception that comes from this is a either an ACE or a fatal exception there // can't be any document failures coming from this indexWriter.deleteDocuments(delete.uid()); + } + if (plan.deleteFromLucene) { numDocDeletes.inc(); + versionMap.putDeleteUnderLock(delete.uid().bytes(), + new DeleteVersionValue(plan.versionOfDeletion, plan.seqNoOfDeletion, delete.primaryTerm(), + engineConfig.getThreadPool().relativeTimeInMillis())); } - versionMap.putDeleteUnderLock(delete.uid().bytes(), - new DeleteVersionValue(plan.versionOfDeletion, plan.seqNoOfDeletion, delete.primaryTerm(), - engineConfig.getThreadPool().relativeTimeInMillis())); return new DeleteResult( plan.versionOfDeletion, getPrimaryTerm(), plan.seqNoOfDeletion, plan.currentlyDeleted == false); } catch (Exception ex) { @@ -1219,15 +1301,16 @@ private DeleteResult deleteInLucene(Delete delete, DeletionStrategy plan) } } - private static final class DeletionStrategy { + protected static final class DeletionStrategy { // of a rare double delete final boolean deleteFromLucene; + final boolean addStaleOpToLucene; final boolean currentlyDeleted; final long seqNoOfDeletion; final long versionOfDeletion; final Optional earlyResultOnPreflightError; - private DeletionStrategy(boolean deleteFromLucene, boolean currentlyDeleted, + private DeletionStrategy(boolean deleteFromLucene, boolean addStaleOpToLucene, boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion, DeleteResult earlyResultOnPreflightError) { assert (deleteFromLucene && earlyResultOnPreflightError != null) == false : @@ -1235,6 +1318,7 @@ private DeletionStrategy(boolean deleteFromLucene, boolean currentlyDeleted, "deleteFromLucene: " + deleteFromLucene + " earlyResultOnPreFlightError:" + earlyResultOnPreflightError; this.deleteFromLucene = deleteFromLucene; + this.addStaleOpToLucene = addStaleOpToLucene; this.currentlyDeleted = currentlyDeleted; this.seqNoOfDeletion = seqNoOfDeletion; this.versionOfDeletion = versionOfDeletion; @@ -1246,16 +1330,22 @@ static DeletionStrategy skipDueToVersionConflict( VersionConflictEngineException e, long currentVersion, long term, boolean currentlyDeleted) { final long unassignedSeqNo = SequenceNumbers.UNASSIGNED_SEQ_NO; final DeleteResult deleteResult = new DeleteResult(e, currentVersion, term, unassignedSeqNo, currentlyDeleted == false); - return new DeletionStrategy(false, currentlyDeleted, unassignedSeqNo, Versions.NOT_FOUND, deleteResult); + return new DeletionStrategy(false, false, currentlyDeleted, unassignedSeqNo, Versions.NOT_FOUND, deleteResult); } static DeletionStrategy processNormally(boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion) { - return new DeletionStrategy(true, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); + return new DeletionStrategy(true, false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); + + } + public static DeletionStrategy processButSkipLucene(boolean currentlyDeleted, + long seqNoOfDeletion, long versionOfDeletion) { + return new DeletionStrategy(false, false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); } - public static DeletionStrategy processButSkipLucene(boolean currentlyDeleted, long seqNoOfDeletion, long versionOfDeletion) { - return new DeletionStrategy(false, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); + static DeletionStrategy processAsStaleOp(boolean addStaleOpToLucene, boolean currentlyDeleted, + long seqNoOfDeletion, long versionOfDeletion) { + return new DeletionStrategy(false, addStaleOpToLucene, currentlyDeleted, seqNoOfDeletion, versionOfDeletion, null); } } @@ -1284,7 +1374,28 @@ private NoOpResult innerNoOp(final NoOp noOp) throws IOException { assert noOp.seqNo() > SequenceNumbers.NO_OPS_PERFORMED; final long seqNo = noOp.seqNo(); try { - final NoOpResult noOpResult = new NoOpResult(getPrimaryTerm(), noOp.seqNo()); + Exception failure = null; + if (softDeleteEnabled) { + try { + final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newNoopTombstoneDoc(noOp.reason()); + tombstone.updateSeqID(noOp.seqNo(), noOp.primaryTerm()); + // A noop tombstone does not require a _version but it's added to have a fully dense docvalues for the version field. + // 1L is selected to optimize the compression because it might probably be the most common value in version field. + tombstone.version().setLongValue(1L); + assert tombstone.docs().size() == 1 : "Tombstone should have a single doc [" + tombstone + "]"; + final ParseContext.Document doc = tombstone.docs().get(0); + assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null + : "Noop tombstone document but _tombstone field is not set [" + doc + " ]"; + doc.add(softDeletesField); + indexWriter.addDocument(doc); + } catch (Exception ex) { + if (maybeFailEngine("noop", ex)) { + throw ex; + } + failure = ex; + } + } + final NoOpResult noOpResult = failure != null ? new NoOpResult(getPrimaryTerm(), noOp.seqNo(), failure) : new NoOpResult(getPrimaryTerm(), noOp.seqNo()); if (noOp.origin() != Operation.Origin.LOCAL_TRANSLOG_RECOVERY) { final Translog.Location location = translog.add(new Translog.NoOp(noOp.seqNo(), noOp.primaryTerm(), noOp.reason())); noOpResult.setTranslogLocation(location); @@ -1309,6 +1420,7 @@ final void refresh(String source, SearcherScope scope) throws EngineException { // since it flushes the index as well (though, in terms of concurrency, we are allowed to do it) // both refresh types will result in an internal refresh but only the external will also // 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(); @@ -1334,6 +1446,7 @@ final void refresh(String source, SearcherScope scope) throws EngineException { } finally { store.decRef(); } + lastRefreshedCheckpointListener.updateRefreshedCheckpoint(localCheckpointBeforeRefresh); } } catch (AlreadyClosedException e) { failOnTragicEvent(e); @@ -1348,7 +1461,8 @@ final void refresh(String source, SearcherScope scope) throws EngineException { } finally { writingBytes.addAndGet(-bytes); } - + assert lastRefreshedCheckpoint() >= localCheckpointBeforeRefresh : "refresh checkpoint was not advanced; " + + "local_checkpoint=" + localCheckpointBeforeRefresh + " refresh_checkpoint=" + lastRefreshedCheckpoint(); // TODO: maybe we should just put a scheduled job in threadPool? // We check for pruning in each delete request, but we also prune here e.g. in case a delete burst comes in and then no more deletes // for a long time: @@ -1930,7 +2044,11 @@ private IndexWriter createWriter() throws IOException { // pkg-private for testing IndexWriter createWriter(Directory directory, IndexWriterConfig iwc) throws IOException { - return new IndexWriter(directory, iwc); + if (Assertions.ENABLED) { + return new AssertingIndexWriter(directory, iwc); + } else { + return new IndexWriter(directory, iwc); + } } private IndexWriterConfig getIndexWriterConfig() { @@ -1946,11 +2064,15 @@ private IndexWriterConfig getIndexWriterConfig() { } iwc.setInfoStream(verbose ? InfoStream.getDefault() : new LoggerInfoStream(logger)); iwc.setMergeScheduler(mergeScheduler); - MergePolicy mergePolicy = config().getMergePolicy(); // Give us the opportunity to upgrade old segments while performing // background merges - mergePolicy = new ElasticsearchMergePolicy(mergePolicy); - iwc.setMergePolicy(mergePolicy); + MergePolicy mergePolicy = config().getMergePolicy(); + if (softDeleteEnabled) { + iwc.setSoftDeletesField(Lucene.SOFT_DELETES_FIELD); + mergePolicy = new RecoverySourcePruneMergePolicy(SourceFieldMapper.RECOVERY_SOURCE_NAME, softDeletesPolicy::getRetentionQuery, + new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, softDeletesPolicy::getRetentionQuery, mergePolicy)); + } + iwc.setMergePolicy(new ElasticsearchMergePolicy(mergePolicy)); iwc.setSimilarity(engineConfig.getSimilarity()); iwc.setRAMBufferSizeMB(engineConfig.getIndexingBufferSize().getMbFrac()); iwc.setCodec(engineConfig.getCodec()); @@ -2147,6 +2269,9 @@ protected void commitIndexWriter(final IndexWriter writer, final Translog transl commitData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(localCheckpointTracker.getMaxSeqNo())); commitData.put(MAX_UNSAFE_AUTO_ID_TIMESTAMP_COMMIT_ID, Long.toString(maxUnsafeAutoIdTimestamp.get())); commitData.put(HISTORY_UUID_KEY, historyUUID); + if (softDeleteEnabled) { + commitData.put(Engine.MIN_RETAINED_SEQNO, Long.toString(softDeletesPolicy.getMinRetainedSeqNo())); + } logger.trace("committing writer with commit data [{}]", commitData); return commitData.entrySet().iterator(); }); @@ -2202,6 +2327,7 @@ public void onSettingsChanged() { final IndexSettings indexSettings = engineConfig.getIndexSettings(); translogDeletionPolicy.setRetentionAgeInMillis(indexSettings.getTranslogRetentionAge().getMillis()); translogDeletionPolicy.setRetentionSizeInBytes(indexSettings.getTranslogRetentionSize().getBytes()); + softDeletesPolicy.setRetentionOperations(indexSettings.getSoftDeleteRetentionOperations()); } public MergeStats getMergeStats() { @@ -2296,6 +2422,69 @@ long getNumDocUpdates() { return numDocUpdates.count(); } + @Override + public Translog.Snapshot newChangesSnapshot(String source, MapperService mapperService, + long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { + // TODO: Should we defer the refresh until we really need it? + ensureOpen(); + if (lastRefreshedCheckpoint() < toSeqNo) { + refresh(source, SearcherScope.INTERNAL); + } + Searcher searcher = acquireSearcher(source, SearcherScope.INTERNAL); + try { + LuceneChangesSnapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE, fromSeqNo, toSeqNo, requiredFullRange); + searcher = null; + return snapshot; + } catch (Exception e) { + try { + maybeFailEngine("acquire changes snapshot", e); + } catch (Exception inner) { + e.addSuppressed(inner); + } + throw e; + } finally { + IOUtils.close(searcher); + } + } + + @Override + public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException { + if (engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + return getMinRetainedSeqNo() <= startingSeqNo; + } else { + final long currentLocalCheckpoint = getLocalCheckpointTracker().getCheckpoint(); + final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); + try (Translog.Snapshot snapshot = getTranslog().newSnapshotFromMinSeqNo(startingSeqNo)) { + Translog.Operation operation; + while ((operation = snapshot.next()) != null) { + if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { + tracker.markSeqNoAsCompleted(operation.seqNo()); + } + } + } + return tracker.getCheckpoint() >= currentLocalCheckpoint; + } + } + + /** + * Returns the minimum seqno that is retained in the Lucene index. + * Operations whose seq# are at least this value should exist in the Lucene index. + */ + final long getMinRetainedSeqNo() { + assert softDeleteEnabled : Thread.currentThread().getName(); + return softDeletesPolicy.getMinRetainedSeqNo(); + } + + @Override + public Closeable acquireRetentionLockForPeerRecovery() { + if (softDeleteEnabled) { + return softDeletesPolicy.acquireRetentionLock(); + } else { + return translog.acquireRetentionLock(); + } + } + @Override public boolean isRecovering() { return pendingTranslogRecovery.get(); @@ -2311,4 +2500,69 @@ private static Map commitDataAsMap(final IndexWriter indexWriter } return commitData; } + + private final class AssertingIndexWriter extends IndexWriter { + AssertingIndexWriter(Directory d, IndexWriterConfig conf) throws IOException { + super(d, conf); + } + @Override + public long updateDocument(Term term, Iterable doc) throws IOException { + assert softDeleteEnabled == false : "Call #updateDocument but soft-deletes is enabled"; + return super.updateDocument(term, doc); + } + @Override + public long updateDocuments(Term delTerm, Iterable> docs) throws IOException { + assert softDeleteEnabled == false : "Call #updateDocuments but soft-deletes is enabled"; + return super.updateDocuments(delTerm, docs); + } + @Override + public long deleteDocuments(Term... terms) throws IOException { + assert softDeleteEnabled == false : "Call #deleteDocuments but soft-deletes is enabled"; + return super.deleteDocuments(terms); + } + @Override + public long softUpdateDocument(Term term, Iterable doc, Field... softDeletes) throws IOException { + assert softDeleteEnabled : "Call #softUpdateDocument but soft-deletes is disabled"; + return super.softUpdateDocument(term, doc, softDeletes); + } + @Override + public long softUpdateDocuments(Term term, Iterable> docs, Field... softDeletes) throws IOException { + assert softDeleteEnabled : "Call #softUpdateDocuments but soft-deletes is disabled"; + return super.softUpdateDocuments(term, docs, softDeletes); + } + } + + /** + * Returned the last local checkpoint value has been refreshed internally. + */ + final long lastRefreshedCheckpoint() { + return lastRefreshedCheckpointListener.refreshedCheckpoint.get(); + } + + private final class LastRefreshedCheckpointListener implements ReferenceManager.RefreshListener { + final AtomicLong refreshedCheckpoint; + private long pendingCheckpoint; + + LastRefreshedCheckpointListener(long initialLocalCheckpoint) { + this.refreshedCheckpoint = new AtomicLong(initialLocalCheckpoint); + } + + @Override + public void beforeRefresh() { + // all changes until this point should be visible after refresh + pendingCheckpoint = localCheckpointTracker.getCheckpoint(); + } + + @Override + public void afterRefresh(boolean didRefresh) { + if (didRefresh) { + updateRefreshedCheckpoint(pendingCheckpoint); + } + } + + void updateRefreshedCheckpoint(long checkpoint) { + refreshedCheckpoint.updateAndGet(curr -> Math.max(curr, checkpoint)); + assert refreshedCheckpoint.get() >= checkpoint : refreshedCheckpoint.get() + " < " + checkpoint; + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java new file mode 100644 index 000000000000..deebfba9ed42 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java @@ -0,0 +1,368 @@ +/* + * 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.document.LongPoint; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.util.ArrayUtil; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.fieldvisitor.FieldsVisitor; +import org.elasticsearch.index.mapper.IdFieldMapper; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.SourceFieldMapper; +import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.mapper.VersionFieldMapper; +import org.elasticsearch.index.translog.Translog; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A {@link Translog.Snapshot} from changes in a Lucene index + */ +final class LuceneChangesSnapshot implements Translog.Snapshot { + static final int DEFAULT_BATCH_SIZE = 1024; + + private final int searchBatchSize; + private final long fromSeqNo, toSeqNo; + private long lastSeenSeqNo; + private int skippedOperations; + private final boolean requiredFullRange; + + private final IndexSearcher indexSearcher; + private final MapperService mapperService; + private int docIndex = 0; + private final int totalHits; + private ScoreDoc[] scoreDocs; + private final ParallelArray parallelArray; + private final Closeable onClose; + + /** + * Creates a new "translog" snapshot from Lucene for reading operations whose seq# in the specified range. + * + * @param engineSearcher the internal engine searcher which will be taken over if the snapshot is opened successfully + * @param mapperService the mapper service which will be mainly used to resolve the document's type and uid + * @param searchBatchSize the number of documents should be returned by each search + * @param fromSeqNo the min requesting seq# - inclusive + * @param toSeqNo the maximum requesting seq# - inclusive + * @param requiredFullRange if true, the snapshot will strictly check for the existence of operations between fromSeqNo and toSeqNo + */ + LuceneChangesSnapshot(Engine.Searcher engineSearcher, MapperService mapperService, int searchBatchSize, + long fromSeqNo, long toSeqNo, boolean requiredFullRange) throws IOException { + if (fromSeqNo < 0 || toSeqNo < 0 || fromSeqNo > toSeqNo) { + throw new IllegalArgumentException("Invalid range; from_seqno [" + fromSeqNo + "], to_seqno [" + toSeqNo + "]"); + } + if (searchBatchSize <= 0) { + throw new IllegalArgumentException("Search_batch_size must be positive [" + searchBatchSize + "]"); + } + final AtomicBoolean closed = new AtomicBoolean(); + this.onClose = () -> { + if (closed.compareAndSet(false, true)) { + IOUtils.close(engineSearcher); + } + }; + this.mapperService = mapperService; + this.searchBatchSize = searchBatchSize; + this.fromSeqNo = fromSeqNo; + this.toSeqNo = toSeqNo; + this.lastSeenSeqNo = fromSeqNo - 1; + this.requiredFullRange = requiredFullRange; + this.indexSearcher = new IndexSearcher(Lucene.wrapAllDocsLive(engineSearcher.getDirectoryReader())); + this.indexSearcher.setQueryCache(null); + this.parallelArray = new ParallelArray(searchBatchSize); + final TopDocs topDocs = searchOperations(null); + this.totalHits = Math.toIntExact(topDocs.totalHits); + this.scoreDocs = topDocs.scoreDocs; + fillParallelArray(scoreDocs, parallelArray); + } + + @Override + public void close() throws IOException { + onClose.close(); + } + + @Override + public int totalOperations() { + return totalHits; + } + + @Override + public int skippedOperations() { + return skippedOperations; + } + + @Override + public Translog.Operation next() throws IOException { + Translog.Operation op = null; + for (int idx = nextDocIndex(); idx != -1; idx = nextDocIndex()) { + op = readDocAsOp(idx); + if (op != null) { + break; + } + } + if (requiredFullRange) { + rangeCheck(op); + } + if (op != null) { + lastSeenSeqNo = op.seqNo(); + } + return op; + } + + private void rangeCheck(Translog.Operation op) { + if (op == null) { + if (lastSeenSeqNo < toSeqNo) { + throw new IllegalStateException("Not all operations between from_seqno [" + fromSeqNo + "] " + + "and to_seqno [" + toSeqNo + "] found; prematurely terminated last_seen_seqno [" + lastSeenSeqNo + "]"); + } + } else { + final long expectedSeqNo = lastSeenSeqNo + 1; + if (op.seqNo() != expectedSeqNo) { + throw new IllegalStateException("Not all operations between from_seqno [" + fromSeqNo + "] " + + "and to_seqno [" + toSeqNo + "] found; expected seqno [" + expectedSeqNo + "]; found [" + op + "]"); + } + } + } + + private int nextDocIndex() throws IOException { + // we have processed all docs in the current search - fetch the next batch + if (docIndex == scoreDocs.length && docIndex > 0) { + final ScoreDoc prev = scoreDocs[scoreDocs.length - 1]; + scoreDocs = searchOperations(prev).scoreDocs; + fillParallelArray(scoreDocs, parallelArray); + docIndex = 0; + } + if (docIndex < scoreDocs.length) { + int idx = docIndex; + docIndex++; + return idx; + } + return -1; + } + + private void fillParallelArray(ScoreDoc[] scoreDocs, ParallelArray parallelArray) throws IOException { + if (scoreDocs.length > 0) { + for (int i = 0; i < scoreDocs.length; i++) { + scoreDocs[i].shardIndex = i; + } + // for better loading performance we sort the array by docID and + // then visit all leaves in order. + ArrayUtil.introSort(scoreDocs, Comparator.comparingInt(i -> i.doc)); + int docBase = -1; + int maxDoc = 0; + List leaves = indexSearcher.getIndexReader().leaves(); + int readerIndex = 0; + CombinedDocValues combinedDocValues = null; + LeafReaderContext leaf = null; + for (int i = 0; i < scoreDocs.length; i++) { + ScoreDoc scoreDoc = scoreDocs[i]; + if (scoreDoc.doc >= docBase + maxDoc) { + do { + leaf = leaves.get(readerIndex++); + docBase = leaf.docBase; + maxDoc = leaf.reader().maxDoc(); + } while (scoreDoc.doc >= docBase + maxDoc); + combinedDocValues = new CombinedDocValues(leaf.reader()); + } + final int segmentDocID = scoreDoc.doc - docBase; + final int index = scoreDoc.shardIndex; + parallelArray.leafReaderContexts[index] = leaf; + parallelArray.seqNo[index] = combinedDocValues.docSeqNo(segmentDocID); + parallelArray.primaryTerm[index] = combinedDocValues.docPrimaryTerm(segmentDocID); + parallelArray.version[index] = combinedDocValues.docVersion(segmentDocID); + parallelArray.isTombStone[index] = combinedDocValues.isTombstone(segmentDocID); + parallelArray.hasRecoverySource[index] = combinedDocValues.hasRecoverySource(segmentDocID); + } + // now sort back based on the shardIndex. we use this to store the previous index + ArrayUtil.introSort(scoreDocs, Comparator.comparingInt(i -> i.shardIndex)); + } + } + + private TopDocs searchOperations(ScoreDoc after) throws IOException { + final Query rangeQuery = LongPoint.newRangeQuery(SeqNoFieldMapper.NAME, lastSeenSeqNo + 1, toSeqNo); + final Sort sortedBySeqNoThenByTerm = new Sort( + new SortField(SeqNoFieldMapper.NAME, SortField.Type.LONG), + new SortField(SeqNoFieldMapper.PRIMARY_TERM_NAME, SortField.Type.LONG, true) + ); + return indexSearcher.searchAfter(after, rangeQuery, searchBatchSize, sortedBySeqNoThenByTerm); + } + + private Translog.Operation readDocAsOp(int docIndex) throws IOException { + final LeafReaderContext leaf = parallelArray.leafReaderContexts[docIndex]; + final int segmentDocID = scoreDocs[docIndex].doc - leaf.docBase; + final long primaryTerm = parallelArray.primaryTerm[docIndex]; + // We don't have to read the nested child documents - those docs don't have primary terms. + if (primaryTerm == -1) { + skippedOperations++; + return null; + } + final long seqNo = parallelArray.seqNo[docIndex]; + // Only pick the first seen seq# + if (seqNo == lastSeenSeqNo) { + skippedOperations++; + return null; + } + final long version = parallelArray.version[docIndex]; + final String sourceField = parallelArray.hasRecoverySource[docIndex] ? SourceFieldMapper.RECOVERY_SOURCE_NAME : + SourceFieldMapper.NAME; + final FieldsVisitor fields = new FieldsVisitor(true, sourceField); + leaf.reader().document(segmentDocID, fields); + fields.postProcess(mapperService); + + final Translog.Operation op; + final boolean isTombstone = parallelArray.isTombStone[docIndex]; + if (isTombstone && fields.uid() == null) { + op = new Translog.NoOp(seqNo, primaryTerm, fields.source().utf8ToString()); + assert version == 1L : "Noop tombstone should have version 1L; actual version [" + version + "]"; + assert assertDocSoftDeleted(leaf.reader(), segmentDocID) : "Noop but soft_deletes field is not set [" + op + "]"; + } else { + final String id = fields.uid().id(); + final String type = fields.uid().type(); + final Term uid = new Term(IdFieldMapper.NAME, Uid.encodeId(id)); + if (isTombstone) { + op = new Translog.Delete(type, id, uid, seqNo, primaryTerm, version); + assert assertDocSoftDeleted(leaf.reader(), segmentDocID) : "Delete op but soft_deletes field is not set [" + op + "]"; + } else { + final BytesReference source = fields.source(); + if (source == null) { + // TODO: Callers should ask for the range that source should be retained. Thus we should always + // check for the existence source once we make peer-recovery to send ops after the local checkpoint. + if (requiredFullRange) { + throw new IllegalStateException("source not found for seqno=" + seqNo + + " from_seqno=" + fromSeqNo + " to_seqno=" + toSeqNo); + } else { + skippedOperations++; + return null; + } + } + // TODO: pass the latest timestamp from engine. + final long autoGeneratedIdTimestamp = -1; + op = new Translog.Index(type, id, seqNo, primaryTerm, version, + source.toBytesRef().bytes, fields.routing(), autoGeneratedIdTimestamp); + } + } + assert fromSeqNo <= op.seqNo() && op.seqNo() <= toSeqNo && lastSeenSeqNo < op.seqNo() : "Unexpected operation; " + + "last_seen_seqno [" + lastSeenSeqNo + "], from_seqno [" + fromSeqNo + "], to_seqno [" + toSeqNo + "], op [" + op + "]"; + return op; + } + + private boolean assertDocSoftDeleted(LeafReader leafReader, int segmentDocId) throws IOException { + final NumericDocValues ndv = leafReader.getNumericDocValues(Lucene.SOFT_DELETES_FIELD); + if (ndv == null || ndv.advanceExact(segmentDocId) == false) { + throw new IllegalStateException("DocValues for field [" + Lucene.SOFT_DELETES_FIELD + "] is not found"); + } + return ndv.longValue() == 1; + } + + private static final class ParallelArray { + final LeafReaderContext[] leafReaderContexts; + final long[] version; + final long[] seqNo; + final long[] primaryTerm; + final boolean[] isTombStone; + final boolean[] hasRecoverySource; + + ParallelArray(int size) { + version = new long[size]; + seqNo = new long[size]; + primaryTerm = new long[size]; + isTombStone = new boolean[size]; + hasRecoverySource = new boolean[size]; + leafReaderContexts = new LeafReaderContext[size]; + } + } + + private static final class CombinedDocValues { + private final NumericDocValues versionDV; + private final NumericDocValues seqNoDV; + private final NumericDocValues primaryTermDV; + private final NumericDocValues tombstoneDV; + private final NumericDocValues recoverySource; + + CombinedDocValues(LeafReader leafReader) throws IOException { + this.versionDV = Objects.requireNonNull(leafReader.getNumericDocValues(VersionFieldMapper.NAME), "VersionDV is missing"); + this.seqNoDV = Objects.requireNonNull(leafReader.getNumericDocValues(SeqNoFieldMapper.NAME), "SeqNoDV is missing"); + this.primaryTermDV = Objects.requireNonNull( + leafReader.getNumericDocValues(SeqNoFieldMapper.PRIMARY_TERM_NAME), "PrimaryTermDV is missing"); + this.tombstoneDV = leafReader.getNumericDocValues(SeqNoFieldMapper.TOMBSTONE_NAME); + this.recoverySource = leafReader.getNumericDocValues(SourceFieldMapper.RECOVERY_SOURCE_NAME); + } + + long docVersion(int segmentDocId) throws IOException { + assert versionDV.docID() < segmentDocId; + if (versionDV.advanceExact(segmentDocId) == false) { + throw new IllegalStateException("DocValues for field [" + VersionFieldMapper.NAME + "] is not found"); + } + return versionDV.longValue(); + } + + long docSeqNo(int segmentDocId) throws IOException { + assert seqNoDV.docID() < segmentDocId; + if (seqNoDV.advanceExact(segmentDocId) == false) { + throw new IllegalStateException("DocValues for field [" + SeqNoFieldMapper.NAME + "] is not found"); + } + return seqNoDV.longValue(); + } + + long docPrimaryTerm(int segmentDocId) throws IOException { + if (primaryTermDV == null) { + return -1L; + } + assert primaryTermDV.docID() < segmentDocId; + // Use -1 for docs which don't have primary term. The caller considers those docs as nested docs. + if (primaryTermDV.advanceExact(segmentDocId) == false) { + return -1; + } + return primaryTermDV.longValue(); + } + + boolean isTombstone(int segmentDocId) throws IOException { + if (tombstoneDV == null) { + return false; + } + assert tombstoneDV.docID() < segmentDocId; + return tombstoneDV.advanceExact(segmentDocId) && tombstoneDV.longValue() > 0; + } + + boolean hasRecoverySource(int segmentDocId) throws IOException { + if (recoverySource == null) { + return false; + } + assert recoverySource.docID() < segmentDocId; + return recoverySource.advanceExact(segmentDocId); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java b/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java new file mode 100644 index 000000000000..fde97562de8f --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicy.java @@ -0,0 +1,292 @@ +/* + * 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.codecs.DocValuesProducer; +import org.apache.lucene.codecs.StoredFieldsReader; +import org.apache.lucene.index.BinaryDocValues; +import org.apache.lucene.index.CodecReader; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FilterCodecReader; +import org.apache.lucene.index.FilterNumericDocValues; +import org.apache.lucene.index.MergePolicy; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.OneMergeWrappingMergePolicy; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.index.SortedNumericDocValues; +import org.apache.lucene.index.SortedSetDocValues; +import org.apache.lucene.index.StoredFieldVisitor; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.ConjunctionDISI; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.DocValuesFieldExistsQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.BitSet; +import org.apache.lucene.util.BitSetIterator; + +import java.io.IOException; +import java.util.Arrays; +import java.util.function.Supplier; + +final class RecoverySourcePruneMergePolicy extends OneMergeWrappingMergePolicy { + RecoverySourcePruneMergePolicy(String recoverySourceField, Supplier retainSourceQuerySupplier, MergePolicy in) { + super(in, toWrap -> new OneMerge(toWrap.segments) { + @Override + public CodecReader wrapForMerge(CodecReader reader) throws IOException { + CodecReader wrapped = toWrap.wrapForMerge(reader); + return wrapReader(recoverySourceField, wrapped, retainSourceQuerySupplier); + } + }); + } + + // pkg private for testing + static CodecReader wrapReader(String recoverySourceField, CodecReader reader, Supplier retainSourceQuerySupplier) + throws IOException { + NumericDocValues recoverySource = reader.getNumericDocValues(recoverySourceField); + if (recoverySource == null || recoverySource.nextDoc() == DocIdSetIterator.NO_MORE_DOCS) { + return reader; // early terminate - nothing to do here since non of the docs has a recovery source anymore. + } + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new DocValuesFieldExistsQuery(recoverySourceField), BooleanClause.Occur.FILTER); + builder.add(retainSourceQuerySupplier.get(), BooleanClause.Occur.FILTER); + IndexSearcher s = new IndexSearcher(reader); + s.setQueryCache(null); + Weight weight = s.createWeight(s.rewrite(builder.build()), false, 1.0f); + Scorer scorer = weight.scorer(reader.getContext()); + if (scorer != null) { + return new SourcePruningFilterCodecReader(recoverySourceField, reader, BitSet.of(scorer.iterator(), reader.maxDoc())); + } else { + return new SourcePruningFilterCodecReader(recoverySourceField, reader, null); + } + } + + private static class SourcePruningFilterCodecReader extends FilterCodecReader { + private final BitSet recoverySourceToKeep; + private final String recoverySourceField; + + SourcePruningFilterCodecReader(String recoverySourceField, CodecReader reader, BitSet recoverySourceToKeep) { + super(reader); + this.recoverySourceField = recoverySourceField; + this.recoverySourceToKeep = recoverySourceToKeep; + } + + @Override + public DocValuesProducer getDocValuesReader() { + DocValuesProducer docValuesReader = super.getDocValuesReader(); + return new FilterDocValuesProducer(docValuesReader) { + @Override + public NumericDocValues getNumeric(FieldInfo field) throws IOException { + NumericDocValues numeric = super.getNumeric(field); + if (recoverySourceField.equals(field.name)) { + assert numeric != null : recoverySourceField + " must have numeric DV but was null"; + final DocIdSetIterator intersection; + if (recoverySourceToKeep == null) { + // we can't return null here lucenes DocIdMerger expects an instance + intersection = DocIdSetIterator.empty(); + } else { + intersection = ConjunctionDISI.intersectIterators(Arrays.asList(numeric, + new BitSetIterator(recoverySourceToKeep, recoverySourceToKeep.length()))); + } + return new FilterNumericDocValues(numeric) { + @Override + public int nextDoc() throws IOException { + return intersection.nextDoc(); + } + + @Override + public int advance(int target) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean advanceExact(int target) { + throw new UnsupportedOperationException(); + } + }; + + } + return numeric; + } + }; + } + + @Override + public StoredFieldsReader getFieldsReader() { + StoredFieldsReader fieldsReader = super.getFieldsReader(); + return new FilterStoredFieldsReader(fieldsReader) { + @Override + public void visitDocument(int docID, StoredFieldVisitor visitor) throws IOException { + if (recoverySourceToKeep != null && recoverySourceToKeep.get(docID)) { + super.visitDocument(docID, visitor); + } else { + super.visitDocument(docID, new FilterStoredFieldVisitor(visitor) { + @Override + public Status needsField(FieldInfo fieldInfo) throws IOException { + if (recoverySourceField.equals(fieldInfo.name)) { + return Status.NO; + } + return super.needsField(fieldInfo); + } + }); + } + } + }; + } + + @Override + public CacheHelper getCoreCacheHelper() { + return null; + } + + @Override + public CacheHelper getReaderCacheHelper() { + return null; + } + + private static class FilterDocValuesProducer extends DocValuesProducer { + private final DocValuesProducer in; + + FilterDocValuesProducer(DocValuesProducer in) { + this.in = in; + } + + @Override + public NumericDocValues getNumeric(FieldInfo field) throws IOException { + return in.getNumeric(field); + } + + @Override + public BinaryDocValues getBinary(FieldInfo field) throws IOException { + return in.getBinary(field); + } + + @Override + public SortedDocValues getSorted(FieldInfo field) throws IOException { + return in.getSorted(field); + } + + @Override + public SortedNumericDocValues getSortedNumeric(FieldInfo field) throws IOException { + return in.getSortedNumeric(field); + } + + @Override + public SortedSetDocValues getSortedSet(FieldInfo field) throws IOException { + return in.getSortedSet(field); + } + + @Override + public void checkIntegrity() throws IOException { + in.checkIntegrity(); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public long ramBytesUsed() { + return in.ramBytesUsed(); + } + } + + private static class FilterStoredFieldsReader extends StoredFieldsReader { + + private final StoredFieldsReader fieldsReader; + + FilterStoredFieldsReader(StoredFieldsReader fieldsReader) { + this.fieldsReader = fieldsReader; + } + + @Override + public long ramBytesUsed() { + return fieldsReader.ramBytesUsed(); + } + + @Override + public void close() throws IOException { + fieldsReader.close(); + } + + @Override + public void visitDocument(int docID, StoredFieldVisitor visitor) throws IOException { + fieldsReader.visitDocument(docID, visitor); + } + + @Override + public StoredFieldsReader clone() { + return fieldsReader.clone(); + } + + @Override + public void checkIntegrity() throws IOException { + fieldsReader.checkIntegrity(); + } + } + + private static class FilterStoredFieldVisitor extends StoredFieldVisitor { + private final StoredFieldVisitor visitor; + + FilterStoredFieldVisitor(StoredFieldVisitor visitor) { + this.visitor = visitor; + } + + @Override + public void binaryField(FieldInfo fieldInfo, byte[] value) throws IOException { + visitor.binaryField(fieldInfo, value); + } + + @Override + public void stringField(FieldInfo fieldInfo, byte[] value) throws IOException { + visitor.stringField(fieldInfo, value); + } + + @Override + public void intField(FieldInfo fieldInfo, int value) throws IOException { + visitor.intField(fieldInfo, value); + } + + @Override + public void longField(FieldInfo fieldInfo, long value) throws IOException { + visitor.longField(fieldInfo, value); + } + + @Override + public void floatField(FieldInfo fieldInfo, float value) throws IOException { + visitor.floatField(fieldInfo, value); + } + + @Override + public void doubleField(FieldInfo fieldInfo, double value) throws IOException { + visitor.doubleField(fieldInfo, value); + } + + @Override + public Status needsField(FieldInfo fieldInfo) throws IOException { + return visitor.needsField(fieldInfo); + } + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java b/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java new file mode 100644 index 000000000000..af2ded8c4662 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/engine/SoftDeletesPolicy.java @@ -0,0 +1,120 @@ +/* + * 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.document.LongPoint; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.index.translog.Translog; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.LongSupplier; + +/** + * A policy that controls how many soft-deleted documents should be retained for peer-recovery and querying history changes purpose. + */ +final class SoftDeletesPolicy { + private final LongSupplier globalCheckpointSupplier; + private long localCheckpointOfSafeCommit; + // This lock count is used to prevent `minRetainedSeqNo` from advancing. + private int retentionLockCount; + // The extra number of operations before the global checkpoint are retained + private long retentionOperations; + // The min seq_no value that is retained - ops after this seq# should exist in the Lucene index. + private long minRetainedSeqNo; + + SoftDeletesPolicy(LongSupplier globalCheckpointSupplier, long minRetainedSeqNo, long retentionOperations) { + this.globalCheckpointSupplier = globalCheckpointSupplier; + this.retentionOperations = retentionOperations; + this.minRetainedSeqNo = minRetainedSeqNo; + this.localCheckpointOfSafeCommit = SequenceNumbers.NO_OPS_PERFORMED; + this.retentionLockCount = 0; + } + + /** + * Updates the number of soft-deleted documents prior to the global checkpoint to be retained + * See {@link org.elasticsearch.index.IndexSettings#INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING} + */ + synchronized void setRetentionOperations(long retentionOperations) { + this.retentionOperations = retentionOperations; + } + + /** + * Sets the local checkpoint of the current safe commit + */ + synchronized void setLocalCheckpointOfSafeCommit(long newCheckpoint) { + if (newCheckpoint < this.localCheckpointOfSafeCommit) { + throw new IllegalArgumentException("Local checkpoint can't go backwards; " + + "new checkpoint [" + newCheckpoint + "]," + "current checkpoint [" + localCheckpointOfSafeCommit + "]"); + } + this.localCheckpointOfSafeCommit = newCheckpoint; + } + + /** + * Acquires a lock on soft-deleted documents to prevent them from cleaning up in merge processes. This is necessary to + * make sure that all operations that are being retained will be retained until the lock is released. + * This is a analogy to the translog's retention lock; see {@link Translog#acquireRetentionLock()} + */ + synchronized Releasable acquireRetentionLock() { + assert retentionLockCount >= 0 : "Invalid number of retention locks [" + retentionLockCount + "]"; + retentionLockCount++; + final AtomicBoolean released = new AtomicBoolean(); + return () -> { + if (released.compareAndSet(false, true)) { + releaseRetentionLock(); + } + }; + } + + private synchronized void releaseRetentionLock() { + assert retentionLockCount > 0 : "Invalid number of retention locks [" + retentionLockCount + "]"; + retentionLockCount--; + } + + /** + * Returns the min seqno that is retained in the Lucene index. + * Operations whose seq# is least this value should exist in the Lucene index. + */ + synchronized long getMinRetainedSeqNo() { + // Do not advance if the retention lock is held + if (retentionLockCount == 0) { + // This policy retains operations for two purposes: peer-recovery and querying changes history. + // - Peer-recovery is driven by the local checkpoint of the safe commit. In peer-recovery, the primary transfers a safe commit, + // then sends ops after the local checkpoint of that commit. This requires keeping all ops after localCheckpointOfSafeCommit; + // - Changes APIs are driven the combination of the global checkpoint and retention ops. Here we prefer using the global + // checkpoint instead of max_seqno because only operations up to the global checkpoint are exposed in the the changes APIs. + final long minSeqNoForQueryingChanges = globalCheckpointSupplier.getAsLong() - retentionOperations; + final long minSeqNoToRetain = Math.min(minSeqNoForQueryingChanges, localCheckpointOfSafeCommit) + 1; + // This can go backward as the retentionOperations value can be changed in settings. + minRetainedSeqNo = Math.max(minRetainedSeqNo, minSeqNoToRetain); + } + return minRetainedSeqNo; + } + + /** + * Returns a soft-deletes retention query that will be used in {@link org.apache.lucene.index.SoftDeletesRetentionMergePolicy} + * Documents including tombstones are soft-deleted and matched this query will be retained and won't cleaned up by merges. + */ + Query getRetentionQuery() { + return LongPoint.newRangeQuery(SeqNoFieldMapper.NAME, getMinRetainedSeqNo(), Long.MAX_VALUE); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java b/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java index 4c65635c61b3..462f8ce8e68b 100644 --- a/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java +++ b/server/src/main/java/org/elasticsearch/index/fieldvisitor/FieldsVisitor.java @@ -54,13 +54,19 @@ public class FieldsVisitor extends StoredFieldVisitor { RoutingFieldMapper.NAME)); private final boolean loadSource; + private final String sourceFieldName; private final Set requiredFields; protected BytesReference source; protected String type, id; protected Map> fieldsValues; public FieldsVisitor(boolean loadSource) { + this(loadSource, SourceFieldMapper.NAME); + } + + public FieldsVisitor(boolean loadSource, String sourceFieldName) { this.loadSource = loadSource; + this.sourceFieldName = sourceFieldName; requiredFields = new HashSet<>(); reset(); } @@ -103,7 +109,7 @@ public void postProcess(MapperService mapperService) { @Override public void binaryField(FieldInfo fieldInfo, byte[] value) throws IOException { - if (SourceFieldMapper.NAME.equals(fieldInfo.name)) { + if (sourceFieldName.equals(fieldInfo.name)) { source = new BytesArray(value); } else if (IdFieldMapper.NAME.equals(fieldInfo.name)) { id = Uid.decodeId(value); @@ -175,7 +181,7 @@ public void reset() { requiredFields.addAll(BASE_REQUIRED_FIELDS); if (loadSource) { - requiredFields.add(SourceFieldMapper.NAME); + requiredFields.add(sourceFieldName); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java index a0640ac68a99..663aa7e6f9e1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java @@ -19,11 +19,14 @@ package org.elasticsearch.index.mapper; +import org.apache.lucene.document.StoredField; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.Query; import org.apache.lucene.search.Scorer; import org.apache.lucene.search.Weight; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.ElasticsearchGenerationException; +import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.text.Text; @@ -39,12 +42,15 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Stream; public class DocumentMapper implements ToXContentFragment { @@ -121,6 +127,8 @@ public DocumentMapper build(MapperService mapperService) { private final Map objectMappers; private final boolean hasNestedObjects; + private final MetadataFieldMapper[] deleteTombstoneMetadataFieldMappers; + private final MetadataFieldMapper[] noopTombstoneMetadataFieldMappers; public DocumentMapper(MapperService mapperService, Mapping mapping) { this.mapperService = mapperService; @@ -171,6 +179,15 @@ public DocumentMapper(MapperService mapperService, Mapping mapping) { } catch (Exception e) { throw new ElasticsearchGenerationException("failed to serialize source for type [" + type + "]", e); } + + final Collection deleteTombstoneMetadataFields = Arrays.asList(VersionFieldMapper.NAME, IdFieldMapper.NAME, + TypeFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME); + this.deleteTombstoneMetadataFieldMappers = Stream.of(mapping.metadataMappers) + .filter(field -> deleteTombstoneMetadataFields.contains(field.name())).toArray(MetadataFieldMapper[]::new); + final Collection noopTombstoneMetadataFields = Arrays.asList( + VersionFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME); + this.noopTombstoneMetadataFieldMappers = Stream.of(mapping.metadataMappers) + .filter(field -> noopTombstoneMetadataFields.contains(field.name())).toArray(MetadataFieldMapper[]::new); } public Mapping mapping() { @@ -242,7 +259,22 @@ public Map objectMappers() { } public ParsedDocument parse(SourceToParse source) throws MapperParsingException { - return documentParser.parseDocument(source); + return documentParser.parseDocument(source, mapping.metadataMappers); + } + + public ParsedDocument createDeleteTombstoneDoc(String index, String type, String id) throws MapperParsingException { + final SourceToParse emptySource = SourceToParse.source(index, type, id, new BytesArray("{}"), XContentType.JSON); + return documentParser.parseDocument(emptySource, deleteTombstoneMetadataFieldMappers).toTombstone(); + } + + public ParsedDocument createNoopTombstoneDoc(String index, String reason) throws MapperParsingException { + final String id = ""; // _id won't be used. + final SourceToParse sourceToParse = SourceToParse.source(index, type, id, new BytesArray("{}"), XContentType.JSON); + final ParsedDocument parsedDoc = documentParser.parseDocument(sourceToParse, noopTombstoneMetadataFieldMappers).toTombstone(); + // Store the reason of a noop as a raw string in the _source field + final BytesRef byteRef = new BytesRef(reason); + parsedDoc.rootDoc().add(new StoredField(SourceFieldMapper.NAME, byteRef.bytes, byteRef.offset, byteRef.length)); + return parsedDoc; } /** diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java index 0fd156c09053..85123f602edf 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java @@ -55,7 +55,7 @@ final class DocumentParser { this.docMapper = docMapper; } - ParsedDocument parseDocument(SourceToParse source) throws MapperParsingException { + ParsedDocument parseDocument(SourceToParse source, MetadataFieldMapper[] metadataFieldsMappers) throws MapperParsingException { validateType(source); final Mapping mapping = docMapper.mapping(); @@ -64,9 +64,9 @@ ParsedDocument parseDocument(SourceToParse source) throws MapperParsingException try (XContentParser parser = XContentHelper.createParser(docMapperParser.getXContentRegistry(), LoggingDeprecationHandler.INSTANCE, source.source(), xContentType)) { - context = new ParseContext.InternalParseContext(indexSettings.getSettings(), docMapperParser, docMapper, source, parser); + context = new ParseContext.InternalParseContext(indexSettings, docMapperParser, docMapper, source, parser); validateStart(parser); - internalParseDocument(mapping, context, parser); + internalParseDocument(mapping, metadataFieldsMappers, context, parser); validateEnd(parser); } catch (Exception e) { throw wrapInMapperParsingException(source, e); @@ -81,10 +81,11 @@ ParsedDocument parseDocument(SourceToParse source) throws MapperParsingException return parsedDocument(source, context, createDynamicUpdate(mapping, docMapper, context.getDynamicMappers())); } - private static void internalParseDocument(Mapping mapping, ParseContext.InternalParseContext context, XContentParser parser) throws IOException { + private static void internalParseDocument(Mapping mapping, MetadataFieldMapper[] metadataFieldsMappers, + ParseContext.InternalParseContext context, XContentParser parser) throws IOException { final boolean emptyDoc = isEmptyDoc(mapping, parser); - for (MetadataFieldMapper metadataMapper : mapping.metadataMappers) { + for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) { metadataMapper.preParse(context); } @@ -95,7 +96,7 @@ private static void internalParseDocument(Mapping mapping, ParseContext.Internal parseObjectOrNested(context, mapping.root); } - for (MetadataFieldMapper metadataMapper : mapping.metadataMappers) { + for (MetadataFieldMapper metadataMapper : metadataFieldsMappers) { metadataMapper.postParse(context); } } @@ -495,7 +496,7 @@ private static void parseObject(final ParseContext context, ObjectMapper mapper, if (builder == null) { builder = new ObjectMapper.Builder(currentFieldName).enabled(true); } - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); objectMapper = builder.build(builderContext); context.addDynamicMapper(objectMapper); context.path().add(currentFieldName); @@ -538,7 +539,7 @@ private static void parseArray(ParseContext context, ObjectMapper parentMapper, if (builder == null) { parseNonDynamicArray(context, parentMapper, lastFieldName, arrayFieldName); } else { - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); mapper = builder.build(builderContext); assert mapper != null; if (mapper instanceof ArrayValueMapperParser) { @@ -696,13 +697,13 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont if (parseableAsLong && context.root().numericDetection()) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.LONG); if (builder == null) { - builder = newLongBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newLongBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } else if (parseableAsDouble && context.root().numericDetection()) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.DOUBLE); if (builder == null) { - builder = newFloatBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newFloatBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } else if (parseableAsLong == false && parseableAsDouble == false && context.root().dateDetection()) { @@ -718,7 +719,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont } Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.DATE); if (builder == null) { - builder = newDateBuilder(currentFieldName, dateTimeFormatter, Version.indexCreated(context.indexSettings())); + builder = newDateBuilder(currentFieldName, dateTimeFormatter, context.indexSettings().getIndexVersionCreated()); } if (builder instanceof DateFieldMapper.Builder) { DateFieldMapper.Builder dateBuilder = (DateFieldMapper.Builder) builder; @@ -741,7 +742,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont if (numberType == XContentParser.NumberType.INT || numberType == XContentParser.NumberType.LONG) { Mapper.Builder builder = context.root().findTemplateBuilder(context, currentFieldName, XContentFieldType.LONG); if (builder == null) { - builder = newLongBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newLongBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } else if (numberType == XContentParser.NumberType.FLOAT || numberType == XContentParser.NumberType.DOUBLE) { @@ -750,7 +751,7 @@ private static Mapper.Builder createBuilderFromDynamicValue(final ParseCont // no templates are defined, we use float by default instead of double // since this is much more space-efficient and should be enough most of // the time - builder = newFloatBuilder(currentFieldName, Version.indexCreated(context.indexSettings())); + builder = newFloatBuilder(currentFieldName, context.indexSettings().getIndexVersionCreated()); } return builder; } @@ -785,7 +786,7 @@ private static void parseDynamicValue(final ParseContext context, ObjectMapper p return; } final String path = context.path().pathAsText(currentFieldName); - final Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + final Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); final MappedFieldType existingFieldType = context.mapperService().fullName(path); final Mapper.Builder builder; if (existingFieldType != null) { @@ -883,8 +884,8 @@ private static Tuple getDynamicParentMapper(ParseContext if (builder == null) { builder = new ObjectMapper.Builder(paths[i]).enabled(true); } - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); - mapper = (ObjectMapper) builder.build(builderContext); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), + context.path()); mapper = (ObjectMapper) builder.build(builderContext); if (mapper.nested() != ObjectMapper.Nested.NO) { throw new MapperParsingException("It is forbidden to create dynamic nested objects ([" + context.path().pathAsText(paths[i]) + "]) through `copy_to` or dots in field names"); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java index 606777392dec..8389a3062701 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldNamesFieldMapper.java @@ -24,7 +24,6 @@ import org.apache.lucene.index.IndexableField; import org.apache.lucene.search.Query; import org.elasticsearch.Version; -import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.lucene.Lucene; @@ -205,12 +204,12 @@ public FieldNamesFieldType fieldType() { } @Override - public void preParse(ParseContext context) throws IOException { + public void preParse(ParseContext context) { } @Override public void postParse(ParseContext context) throws IOException { - if (context.indexSettings().getAsVersion(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).before(Version.V_6_1_0)) { + if (context.indexSettings().getIndexVersionCreated().before(Version.V_6_1_0)) { super.parse(context); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java b/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java index b77ffee05caf..cf8cc4022fd8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ParseContext.java @@ -24,9 +24,8 @@ import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexableField; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.IndexSettings; import java.util.ArrayList; import java.util.Collection; @@ -196,7 +195,7 @@ public boolean isWithinMultiFields() { } @Override - public Settings indexSettings() { + public IndexSettings indexSettings() { return in.indexSettings(); } @@ -315,8 +314,7 @@ public static class InternalParseContext extends ParseContext { private final List documents; - @Nullable - private final Settings indexSettings; + private final IndexSettings indexSettings; private final SourceToParse sourceToParse; @@ -334,8 +332,8 @@ public static class InternalParseContext extends ParseContext { private final Set ignoredFields = new HashSet<>(); - public InternalParseContext(@Nullable Settings indexSettings, DocumentMapperParser docMapperParser, DocumentMapper docMapper, - SourceToParse source, XContentParser parser) { + public InternalParseContext(IndexSettings indexSettings, DocumentMapperParser docMapperParser, DocumentMapper docMapper, + SourceToParse source, XContentParser parser) { this.indexSettings = indexSettings; this.docMapper = docMapper; this.docMapperParser = docMapperParser; @@ -347,7 +345,7 @@ public InternalParseContext(@Nullable Settings indexSettings, DocumentMapperPars this.version = null; this.sourceToParse = source; this.dynamicMappers = new ArrayList<>(); - this.maxAllowedNumNestedDocs = MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING.get(indexSettings); + this.maxAllowedNumNestedDocs = indexSettings.getValue(MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING); this.numNestedDocs = 0L; } @@ -357,8 +355,7 @@ public DocumentMapperParser docMapperParser() { } @Override - @Nullable - public Settings indexSettings() { + public IndexSettings indexSettings() { return this.indexSettings; } @@ -565,8 +562,7 @@ public boolean isWithinMultiFields() { return false; } - @Nullable - public abstract Settings indexSettings(); + public abstract IndexSettings indexSettings(); public abstract SourceToParse sourceToParse(); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java index 414cb3a98eca..d2cf17ddd350 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ParsedDocument.java @@ -83,6 +83,17 @@ public void updateSeqID(long sequenceNumber, long primaryTerm) { this.seqID.primaryTerm.setLongValue(primaryTerm); } + /** + * Makes the processing document as a tombstone document rather than a regular document. + * Tombstone documents are stored in Lucene index to represent delete operations or Noops. + */ + ParsedDocument toTombstone() { + assert docs().size() == 1 : "Tombstone should have a single doc [" + docs() + "]"; + this.seqID.tombstoneField.setLongValue(1); + rootDoc().add(this.seqID.tombstoneField); + return this; + } + public String routing() { return this.routing; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java index ac3ffe462723..5a0db4163bf2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java @@ -69,26 +69,29 @@ public static class SequenceIDFields { public final Field seqNo; public final Field seqNoDocValue; public final Field primaryTerm; + public final Field tombstoneField; - public SequenceIDFields(Field seqNo, Field seqNoDocValue, Field primaryTerm) { + public SequenceIDFields(Field seqNo, Field seqNoDocValue, Field primaryTerm, Field tombstoneField) { Objects.requireNonNull(seqNo, "sequence number field cannot be null"); Objects.requireNonNull(seqNoDocValue, "sequence number dv field cannot be null"); Objects.requireNonNull(primaryTerm, "primary term field cannot be null"); this.seqNo = seqNo; this.seqNoDocValue = seqNoDocValue; this.primaryTerm = primaryTerm; + this.tombstoneField = tombstoneField; } public static SequenceIDFields emptySeqID() { return new SequenceIDFields(new LongPoint(NAME, SequenceNumbers.UNASSIGNED_SEQ_NO), new NumericDocValuesField(NAME, SequenceNumbers.UNASSIGNED_SEQ_NO), - new NumericDocValuesField(PRIMARY_TERM_NAME, 0)); + new NumericDocValuesField(PRIMARY_TERM_NAME, 0), new NumericDocValuesField(TOMBSTONE_NAME, 0)); } } public static final String NAME = "_seq_no"; public static final String CONTENT_TYPE = "_seq_no"; public static final String PRIMARY_TERM_NAME = "_primary_term"; + public static final String TOMBSTONE_NAME = "_tombstone"; public static class SeqNoDefaults { public static final String NAME = SeqNoFieldMapper.NAME; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java index f2090613c096..7bfe793bba4a 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.mapper; +import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.StoredField; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexableField; @@ -49,6 +50,7 @@ public class SourceFieldMapper extends MetadataFieldMapper { public static final String NAME = "_source"; + public static final String RECOVERY_SOURCE_NAME = "_recovery_source"; public static final String CONTENT_TYPE = "_source"; private final Function, Map> filter; @@ -224,7 +226,8 @@ public Mapper parse(ParseContext context) throws IOException { @Override protected void parseCreateField(ParseContext context, List fields) throws IOException { - BytesReference source = context.sourceToParse().source(); + BytesReference originalSource = context.sourceToParse().source(); + BytesReference source = originalSource; if (enabled && fieldType().stored() && source != null) { // Percolate and tv APIs may not set the source and that is ok, because these APIs will not index any data if (filter != null) { @@ -240,8 +243,17 @@ protected void parseCreateField(ParseContext context, List field } BytesRef ref = source.toBytesRef(); fields.add(new StoredField(fieldType().name(), ref.bytes, ref.offset, ref.length)); + } else { + source = null; } - } + + if (originalSource != null && source != originalSource && context.indexSettings().isSoftDeleteEnabled()) { + // if we omitted source or modified it we add the _recovery_source to ensure we have it for ops based recovery + BytesRef ref = originalSource.toBytesRef(); + fields.add(new StoredField(RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); + fields.add(new NumericDocValuesField(RECOVERY_SOURCE_NAME, 1)); + } + } @Override protected String contentType() { 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 e030c95b56e3..ef5f9ab0ef3e 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -92,12 +92,14 @@ import org.elasticsearch.index.flush.FlushStats; import org.elasticsearch.index.get.GetStats; import org.elasticsearch.index.get.ShardGetService; +import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.DocumentMapperForType; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.RootObjectMapper; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.merge.MergeStats; @@ -1620,25 +1622,33 @@ public void onSettingsChanged() { } /** - * Acquires a lock on the translog files, preventing them from being trimmed. + * Acquires a lock on the translog files and Lucene soft-deleted documents to prevent them from being trimmed */ - public Closeable acquireTranslogRetentionLock() { - return getEngine().acquireTranslogRetentionLock(); + public Closeable acquireRetentionLockForPeerRecovery() { + return getEngine().acquireRetentionLockForPeerRecovery(); } /** - * Creates a new translog snapshot for reading translog operations whose seq# at least the provided seq#. - * The caller has to close the returned snapshot after finishing the reading. + * Returns the estimated number of history operations whose seq# at least the provided seq# in this shard. */ - public Translog.Snapshot newTranslogSnapshotFromMinSeqNo(long minSeqNo) throws IOException { - return getEngine().newTranslogSnapshotFromMinSeqNo(minSeqNo); + public int estimateNumberOfHistoryOperations(String source, long startingSeqNo) throws IOException { + return getEngine().estimateNumberOfHistoryOperations(source, mapperService, startingSeqNo); } /** - * Returns the estimated number of operations in translog whose seq# at least the provided seq#. + * Creates a new history snapshot for reading operations since the provided starting seqno (inclusive). + * The returned snapshot can be retrieved from either Lucene index or translog files. */ - public int estimateTranslogOperationsFromMinSeq(long minSeqNo) { - return getEngine().estimateTranslogOperationsFromMinSeq(minSeqNo); + public Translog.Snapshot getHistoryOperations(String source, long startingSeqNo) throws IOException { + return getEngine().readHistoryOperations(source, mapperService, startingSeqNo); + } + + /** + * Checks if we have a completed history of operations since the given starting seqno (inclusive). + * This method should be called after acquiring the retention lock; See {@link #acquireRetentionLockForPeerRecovery()} + */ + public boolean hasCompleteHistoryOperations(String source, long startingSeqNo) throws IOException { + return getEngine().hasCompleteOperationHistory(source, mapperService, startingSeqNo); } public List segments(boolean verbose) { @@ -2209,7 +2219,7 @@ private EngineConfig newEngineConfig() { IndexingMemoryController.SHARD_INACTIVE_TIME_SETTING.get(indexSettings.getSettings()), Collections.singletonList(refreshListeners), Collections.singletonList(new RefreshMetricUpdater(refreshMetric)), - indexSort, this::runTranslogRecovery, circuitBreakerService, replicationTracker, () -> operationPrimaryTerm); + indexSort, this::runTranslogRecovery, circuitBreakerService, replicationTracker, () -> operationPrimaryTerm, tombstoneDocSupplier()); } /** @@ -2648,4 +2658,19 @@ public void afterRefresh(boolean didRefresh) throws IOException { refreshMetric.inc(System.nanoTime() - currentRefreshStartTime); } } + + private EngineConfig.TombstoneDocSupplier tombstoneDocSupplier() { + final RootObjectMapper.Builder noopRootMapper = new RootObjectMapper.Builder("__noop"); + final DocumentMapper noopDocumentMapper = new DocumentMapper.Builder(noopRootMapper, mapperService).build(mapperService); + return new EngineConfig.TombstoneDocSupplier() { + @Override + public ParsedDocument newDeleteTombstoneDoc(String type, String id) { + return docMapper(type).getDocumentMapper().createDeleteTombstoneDoc(shardId.getIndexName(), type, id); + } + @Override + public ParsedDocument newNoopTombstoneDoc(String reason) { + return noopDocumentMapper.createNoopTombstoneDoc(shardId.getIndexName(), reason); + } + }; + } } diff --git a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java index 1edc0eb5dcaf..016a8afff696 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java +++ b/server/src/main/java/org/elasticsearch/index/shard/PrimaryReplicaSyncer.java @@ -89,7 +89,7 @@ public void resync(final IndexShard indexShard, final ActionListener // Wrap translog snapshot to make it synchronized as it is accessed by different threads through SnapshotSender. // Even though those calls are not concurrent, snapshot.next() uses non-synchronized state and is not multi-thread-compatible // Also fail the resync early if the shard is shutting down - snapshot = indexShard.newTranslogSnapshotFromMinSeqNo(startingSeqNo); + snapshot = indexShard.getHistoryOperations("resync", startingSeqNo); final Translog.Snapshot originalSnapshot = snapshot; final Translog.Snapshot wrappedSnapshot = new Translog.Snapshot() { @Override diff --git a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java index e9acfe3d8b06..ae3f90e63e7d 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java +++ b/server/src/main/java/org/elasticsearch/index/shard/StoreRecovery.java @@ -156,6 +156,7 @@ void addIndices(final RecoveryState.Index indexRecoveryStats, final Directory ta final Directory hardLinkOrCopyTarget = new org.apache.lucene.store.HardlinkCopyDirectoryWrapper(target); IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) // we don't want merges to happen here - we call maybe merge on the engine // later once we stared it up otherwise we would need to wait for it here 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 001e263ea8ff..85975bc68c85 100644 --- a/server/src/main/java/org/elasticsearch/index/store/Store.java +++ b/server/src/main/java/org/elasticsearch/index/store/Store.java @@ -1009,7 +1009,6 @@ public RecoveryDiff recoveryDiff(MetadataSnapshot recoveryTargetSnapshot) { } final String segmentId = IndexFileNames.parseSegmentName(meta.name()); final String extension = IndexFileNames.getExtension(meta.name()); - assert FIELD_INFOS_FILE_EXTENSION.equals(extension) == false || IndexFileNames.stripExtension(IndexFileNames.stripSegmentName(meta.name())).isEmpty() : "FieldInfos are generational but updateable DV are not supported in elasticsearch"; if (IndexFileNames.SEGMENTS.equals(segmentId) || DEL_FILE_EXTENSION.equals(extension) || LIV_FILE_EXTENSION.equals(extension)) { // only treat del files as per-commit files fnm files are generational but only for upgradable DV perCommitStoreFiles.add(meta); @@ -1595,6 +1594,7 @@ private static IndexWriter newIndexWriter(final IndexWriterConfig.OpenMode openM throws IOException { assert openMode == IndexWriterConfig.OpenMode.APPEND || commit == null : "can't specify create flag with a commit"; IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) .setIndexCommit(commit) // we don't want merges to happen here - we call maybe merge on the engine diff --git a/server/src/main/java/org/elasticsearch/index/translog/Translog.java b/server/src/main/java/org/elasticsearch/index/translog/Translog.java index 618aa546e425..f17acac37896 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/Translog.java +++ b/server/src/main/java/org/elasticsearch/index/translog/Translog.java @@ -1261,6 +1261,8 @@ public String toString() { ", type='" + type + '\'' + ", seqNo=" + seqNo + ", primaryTerm=" + primaryTerm + + ", version=" + version + + ", autoGeneratedIdTimestamp=" + autoGeneratedIdTimestamp + '}'; } @@ -1403,6 +1405,7 @@ public String toString() { "uid=" + uid + ", seqNo=" + seqNo + ", primaryTerm=" + primaryTerm + + ", version=" + version + '}'; } } diff --git a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java index f48f2ceb7927..e0cfe9eaaff0 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TranslogWriter.java @@ -40,6 +40,7 @@ import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.LongSupplier; @@ -192,7 +193,24 @@ private synchronized boolean assertNoSeqNumberConflict(long seqNo, BytesReferenc new BufferedChecksumStreamInput(data.streamInput(), "assertion")); Translog.Operation prvOp = Translog.readOperation( new BufferedChecksumStreamInput(previous.v1().streamInput(), "assertion")); - if (newOp.equals(prvOp) == false) { + // TODO: We haven't had timestamp for Index operations in Lucene yet, we need to loosen this check without timestamp. + final boolean sameOp; + if (newOp instanceof Translog.Index && prvOp instanceof Translog.Index) { + final Translog.Index o1 = (Translog.Index) prvOp; + final Translog.Index o2 = (Translog.Index) newOp; + sameOp = Objects.equals(o1.id(), o2.id()) && Objects.equals(o1.type(), o2.type()) + && Objects.equals(o1.source(), o2.source()) && Objects.equals(o1.routing(), o2.routing()) + && o1.primaryTerm() == o2.primaryTerm() && o1.seqNo() == o2.seqNo() + && o1.version() == o2.version(); + } else if (newOp instanceof Translog.Delete && prvOp instanceof Translog.Delete) { + final Translog.Delete o1 = (Translog.Delete) newOp; + final Translog.Delete o2 = (Translog.Delete) prvOp; + sameOp = Objects.equals(o1.id(), o2.id()) && Objects.equals(o1.type(), o2.type()) + && o1.primaryTerm() == o2.primaryTerm() && o1.seqNo() == o2.seqNo() && o1.version() == o2.version(); + } else { + sameOp = false; + } + if (sameOp == false) { throw new AssertionError( "seqNo [" + seqNo + "] was processed twice in generation [" + generation + "], with different data. " + "prvOp [" + prvOp + "], newOp [" + newOp + "]", previous.v2()); diff --git a/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java b/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java index 86995ae7c5a9..a90f8af0af42 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java +++ b/server/src/main/java/org/elasticsearch/index/translog/TruncateTranslogCommand.java @@ -32,6 +32,7 @@ import org.apache.lucene.store.Lock; import org.apache.lucene.store.LockObtainFailedException; import org.apache.lucene.store.NativeFSLockFactory; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.cli.EnvironmentAwareCommand; @@ -177,6 +178,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th terminal.println("Marking index with the new history uuid"); // commit the new histroy id IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) // we don't want merges to happen here - we call maybe merge on the engine // later once we stared it up otherwise we would need to wait for it here diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java index 352f07d57649..10f796e5e155 100644 --- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java +++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoverySourceHandler.java @@ -146,11 +146,11 @@ public RecoveryResponse recoverToTarget() throws IOException { assert targetShardRouting.initializing() : "expected recovery target to be initializing but was " + targetShardRouting; }, shardId + " validating recovery target ["+ request.targetAllocationId() + "] registered ", shard, cancellableThreads, logger); - try (Closeable ignored = shard.acquireTranslogRetentionLock()) { + try (Closeable ignored = shard.acquireRetentionLockForPeerRecovery()) { final long startingSeqNo; final long requiredSeqNoRangeStart; final boolean isSequenceNumberBasedRecovery = request.startingSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO && - isTargetSameHistory() && isTranslogReadyForSequenceNumberBasedRecovery(); + isTargetSameHistory() && shard.hasCompleteHistoryOperations("peer-recovery", request.startingSeqNo()); if (isSequenceNumberBasedRecovery) { logger.trace("performing sequence numbers based recovery. starting at [{}]", request.startingSeqNo()); startingSeqNo = request.startingSeqNo(); @@ -162,14 +162,16 @@ public RecoveryResponse recoverToTarget() throws IOException { } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "snapshot failed", e); } - // we set this to 0 to create a translog roughly according to the retention policy - // on the target. Note that it will still filter out legacy operations with no sequence numbers - startingSeqNo = 0; - // but we must have everything above the local checkpoint in the commit + // We must have everything above the local checkpoint in the commit requiredSeqNoRangeStart = Long.parseLong(phase1Snapshot.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + // If soft-deletes enabled, we need to transfer only operations after the local_checkpoint of the commit to have + // the same history on the target. However, with translog, we need to set this to 0 to create a translog roughly + // according to the retention policy on the target. Note that it will still filter out legacy operations without seqNo. + startingSeqNo = shard.indexSettings().isSoftDeleteEnabled() ? requiredSeqNoRangeStart : 0; try { - phase1(phase1Snapshot.getIndexCommit(), () -> shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); + final int estimateNumOps = shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo); + phase1(phase1Snapshot.getIndexCommit(), () -> estimateNumOps); } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "phase1 failed", e); } finally { @@ -186,7 +188,8 @@ public RecoveryResponse recoverToTarget() throws IOException { try { // For a sequence based recovery, the target can keep its local translog - prepareTargetForTranslog(isSequenceNumberBasedRecovery == false, shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); + prepareTargetForTranslog(isSequenceNumberBasedRecovery == false, + shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo)); } catch (final Exception e) { throw new RecoveryEngineException(shard.shardId(), 1, "prepare target for translog failed", e); } @@ -207,11 +210,13 @@ public RecoveryResponse recoverToTarget() throws IOException { */ cancellableThreads.execute(() -> shard.waitForOpsToComplete(endingSeqNo)); - logger.trace("all operations up to [{}] completed, which will be used as an ending sequence number", endingSeqNo); - - logger.trace("snapshot translog for recovery; current size is [{}]", shard.estimateTranslogOperationsFromMinSeq(startingSeqNo)); + if (logger.isTraceEnabled()) { + logger.trace("all operations up to [{}] completed, which will be used as an ending sequence number", endingSeqNo); + logger.trace("snapshot translog for recovery; current size is [{}]", + shard.estimateNumberOfHistoryOperations("peer-recovery", startingSeqNo)); + } final long targetLocalCheckpoint; - try(Translog.Snapshot snapshot = shard.newTranslogSnapshotFromMinSeqNo(startingSeqNo)) { + try (Translog.Snapshot snapshot = shard.getHistoryOperations("peer-recovery", startingSeqNo)) { targetLocalCheckpoint = phase2(startingSeqNo, requiredSeqNoRangeStart, endingSeqNo, snapshot); } catch (Exception e) { throw new RecoveryEngineException(shard.shardId(), 2, "phase2 failed", e); @@ -268,36 +273,6 @@ public void onFailure(Exception e) { }); } - /** - * Determines if the source translog is ready for a sequence-number-based peer recovery. The main condition here is that the source - * translog contains all operations above the local checkpoint on the target. We already know the that translog contains or will contain - * all ops above the source local checkpoint, so we can stop check there. - * - * @return {@code true} if the source is ready for a sequence-number-based recovery - * @throws IOException if an I/O exception occurred reading the translog snapshot - */ - boolean isTranslogReadyForSequenceNumberBasedRecovery() throws IOException { - final long startingSeqNo = request.startingSeqNo(); - assert startingSeqNo >= 0; - final long localCheckpoint = shard.getLocalCheckpoint(); - logger.trace("testing sequence numbers in range: [{}, {}]", startingSeqNo, localCheckpoint); - // the start recovery request is initialized with the starting sequence number set to the target shard's local checkpoint plus one - if (startingSeqNo - 1 <= localCheckpoint) { - final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1); - try (Translog.Snapshot snapshot = shard.newTranslogSnapshotFromMinSeqNo(startingSeqNo)) { - Translog.Operation operation; - while ((operation = snapshot.next()) != null) { - if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) { - tracker.markSeqNoAsCompleted(operation.seqNo()); - } - } - } - return tracker.getCheckpoint() >= localCheckpoint; - } else { - return false; - } - } - /** * Perform phase1 of the recovery operations. Once this {@link IndexCommit} * snapshot has been performed no commit operations (files being fsync'd) 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 a4d6518e9af9..9469f657c96b 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -1492,6 +1492,7 @@ public void restore() throws IOException { // empty shard would cause exceptions to be thrown. Since there is no data to restore from an empty // shard anyway, we just create the empty shard here and then exit. IndexWriter writer = new IndexWriter(store.directory(), new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setOpenMode(IndexWriterConfig.OpenMode.CREATE) .setCommitOnClose(true)); writer.close(); diff --git a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java index 702d63d0d940..6acdbad2ccec 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RestoreService.java @@ -64,6 +64,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.repositories.IndexId; @@ -120,7 +121,8 @@ public class RestoreService extends AbstractComponent implements ClusterStateApp SETTING_NUMBER_OF_SHARDS, SETTING_VERSION_CREATED, SETTING_INDEX_UUID, - SETTING_CREATION_DATE)); + SETTING_CREATION_DATE, + IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey())); // It's OK to change some settings, but we shouldn't allow simply removing them private static final Set UNREMOVABLE_SETTINGS; diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java b/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java index 90173455c3be..9786c0eaf529 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/PrimaryAllocationIT.java @@ -392,6 +392,7 @@ public void testPrimaryReplicaResyncFailed() throws Exception { assertThat(shard.getLocalCheckpoint(), equalTo(numDocs + moreDocs)); } }, 30, TimeUnit.SECONDS); + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); } } diff --git a/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java b/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java index 753aedea01e0..890f6ef163b3 100644 --- a/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java +++ b/server/src/test/java/org/elasticsearch/common/lucene/LuceneTests.java @@ -33,18 +33,23 @@ import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.index.RandomIndexWriter; import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.Weight; import org.apache.lucene.store.Directory; import org.apache.lucene.store.MMapDirectory; import org.apache.lucene.store.MockDirectoryWrapper; import org.apache.lucene.util.Bits; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.test.ESTestCase; import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -53,6 +58,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; +import static org.hamcrest.Matchers.equalTo; + public class LuceneTests extends ESTestCase { public void testWaitForIndex() throws Exception { final MockDirectoryWrapper dir = newMockDirectory(); @@ -406,4 +413,88 @@ public void testMMapHackSupported() throws Exception { // add assume's here if needed for certain platforms, but we should know if it does not work. assertTrue("MMapDirectory does not support unmapping: " + MMapDirectory.UNMAP_NOT_SUPPORTED_REASON, MMapDirectory.UNMAP_SUPPORTED); } + + public void testWrapAllDocsLive() throws Exception { + Directory dir = newDirectory(); + IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) + .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())); + IndexWriter writer = new IndexWriter(dir, config); + int numDocs = between(1, 10); + Set liveDocs = new HashSet<>(); + for (int i = 0; i < numDocs; i++) { + String id = Integer.toString(i); + Document doc = new Document(); + doc.add(new StringField("id", id, Store.YES)); + writer.addDocument(doc); + liveDocs.add(id); + } + for (int i = 0; i < numDocs; i++) { + if (randomBoolean()) { + String id = Integer.toString(i); + Document doc = new Document(); + doc.add(new StringField("id", "v2-" + id, Store.YES)); + if (randomBoolean()) { + doc.add(Lucene.newSoftDeletesField()); + } + writer.softUpdateDocument(new Term("id", id), doc, Lucene.newSoftDeletesField()); + liveDocs.add("v2-" + id); + } + } + try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { + DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); + assertThat(reader.numDocs(), equalTo(liveDocs.size())); + IndexSearcher searcher = new IndexSearcher(reader); + Set actualDocs = new HashSet<>(); + TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); + for (ScoreDoc scoreDoc : topDocs.scoreDocs) { + actualDocs.add(reader.document(scoreDoc.doc).get("id")); + } + assertThat(actualDocs, equalTo(liveDocs)); + } + IOUtils.close(writer, dir); + } + + public void testWrapLiveDocsNotExposeAbortedDocuments() throws Exception { + Directory dir = newDirectory(); + IndexWriterConfig config = newIndexWriterConfig().setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) + .setMergePolicy(new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, MatchAllDocsQuery::new, newMergePolicy())); + IndexWriter writer = new IndexWriter(dir, config); + int numDocs = between(1, 10); + List liveDocs = new ArrayList<>(); + for (int i = 0; i < numDocs; i++) { + String id = Integer.toString(i); + Document doc = new Document(); + doc.add(new StringField("id", id, Store.YES)); + if (randomBoolean()) { + doc.add(Lucene.newSoftDeletesField()); + } + writer.addDocument(doc); + liveDocs.add(id); + } + int abortedDocs = between(1, 10); + for (int i = 0; i < abortedDocs; i++) { + try { + Document doc = new Document(); + doc.add(new StringField("id", "aborted-" + i, Store.YES)); + StringReader reader = new StringReader(""); + doc.add(new TextField("other", reader)); + reader.close(); // mark the indexing hit non-aborting error + writer.addDocument(doc); + fail("index should have failed"); + } catch (Exception ignored) { } + } + try (DirectoryReader unwrapped = DirectoryReader.open(writer)) { + DirectoryReader reader = Lucene.wrapAllDocsLive(unwrapped); + assertThat(reader.maxDoc(), equalTo(numDocs + abortedDocs)); + assertThat(reader.numDocs(), equalTo(liveDocs.size())); + IndexSearcher searcher = new IndexSearcher(reader); + List actualDocs = new ArrayList<>(); + TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), Integer.MAX_VALUE); + for (ScoreDoc scoreDoc : topDocs.scoreDocs) { + actualDocs.add(reader.document(scoreDoc.doc).get("id")); + } + assertThat(actualDocs, equalTo(liveDocs)); + } + IOUtils.close(writer, dir); + } } diff --git a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java index 6bdd8ea3f2e0..ac2f2b0d4f32 100644 --- a/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java +++ b/server/src/test/java/org/elasticsearch/discovery/AbstractDisruptionTestCase.java @@ -109,6 +109,7 @@ public void setDisruptionScheme(ServiceDisruptionScheme scheme) { protected void beforeIndexDeletion() throws Exception { if (disableBeforeIndexDeletion == false) { super.beforeIndexDeletion(); + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); assertSeqNos(); } } diff --git a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java index d098c4918a76..b0b6c35f92a1 100644 --- a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java @@ -40,6 +40,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardPath; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; @@ -397,7 +398,8 @@ public void testReuseInFileBasedPeerRecovery() throws Exception { .get(); logger.info("--> indexing docs"); - for (int i = 0; i < randomIntBetween(1, 1024); i++) { + int numDocs = randomIntBetween(1, 1024); + for (int i = 0; i < numDocs; i++) { client(primaryNode).prepareIndex("test", "type").setSource("field", "value").execute().actionGet(); } @@ -419,12 +421,15 @@ public void testReuseInFileBasedPeerRecovery() throws Exception { } logger.info("--> restart replica node"); + boolean softDeleteEnabled = internalCluster().getInstance(IndicesService.class, primaryNode) + .indexServiceSafe(resolveIndex("test")).getShard(0).indexSettings().isSoftDeleteEnabled(); + int moreDocs = randomIntBetween(1, 1024); internalCluster().restartNode(replicaNode, new RestartCallback() { @Override public Settings onNodeStopped(String nodeName) throws Exception { // index some more documents; we expect to reuse the files that already exist on the replica - for (int i = 0; i < randomIntBetween(1, 1024); i++) { + for (int i = 0; i < moreDocs; i++) { client(primaryNode).prepareIndex("test", "type").setSource("field", "value").execute().actionGet(); } @@ -432,8 +437,12 @@ public Settings onNodeStopped(String nodeName) throws Exception { client(primaryNode).admin().indices().prepareUpdateSettings("test").setSettings(Settings.builder() .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), "-1") .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1") + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) ).get(); client(primaryNode).admin().indices().prepareFlush("test").setForce(true).get(); + if (softDeleteEnabled) { // We need an extra flush to advance the min_retained_seqno of the SoftDeletesPolicy + client(primaryNode).admin().indices().prepareFlush("test").setForce(true).get(); + } return super.onNodeStopped(nodeName); } }); diff --git a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java index 28fa440d96ac..b0b4ec3930ad 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexServiceTests.java @@ -32,6 +32,7 @@ import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.shard.IndexShard; +import org.elasticsearch.index.shard.IndexShardTestCase; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -306,7 +307,7 @@ public void testAsyncTranslogTrimActuallyWorks() throws Exception { .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), -1)) .get(); IndexShard shard = indexService.getShard(0); - assertBusy(() -> assertThat(shard.estimateTranslogOperationsFromMinSeq(0L), equalTo(0))); + assertBusy(() -> assertThat(IndexShardTestCase.getTranslog(shard).totalOperations(), equalTo(0))); } public void testIllegalFsyncInterval() { diff --git a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java index b7da5add2acf..64a2fa69bcbd 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexSettingsTests.java @@ -553,4 +553,12 @@ public void testQueryDefaultField() { ); assertThat(index.getDefaultFields(), equalTo(Arrays.asList("body", "title"))); } + + public void testUpdateSoftDeletesFails() { + IndexScopedSettings settings = new IndexScopedSettings(Settings.EMPTY, IndexScopedSettings.BUILT_IN_INDEX_SETTINGS); + IllegalArgumentException error = expectThrows(IllegalArgumentException.class, () -> + settings.updateSettings(Settings.builder().put("index.soft_deletes.enabled", randomBoolean()).build(), + Settings.builder(), Settings.builder(), "index")); + assertThat(error.getMessage(), equalTo("final index setting [index.soft_deletes.enabled], not updateable")); + } } diff --git a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java index ea7de50b7b34..3f9fc9a0429b 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java @@ -51,20 +51,24 @@ public class CombinedDeletionPolicyTests extends ESTestCase { public void testKeepCommitsAfterGlobalCheckpoint() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); + final int extraRetainedOps = between(0, 100); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, extraRetainedOps); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); final LongArrayList maxSeqNoList = new LongArrayList(); final LongArrayList translogGenList = new LongArrayList(); final List commitList = new ArrayList<>(); int totalCommits = between(2, 20); long lastMaxSeqNo = 0; + long lastCheckpoint = lastMaxSeqNo; long lastTranslogGen = 0; final UUID translogUUID = UUID.randomUUID(); for (int i = 0; i < totalCommits; i++) { lastMaxSeqNo += between(1, 10000); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); lastTranslogGen += between(1, 100); - commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); maxSeqNoList.add(lastMaxSeqNo); translogGenList.add(lastTranslogGen); } @@ -85,14 +89,19 @@ public void testKeepCommitsAfterGlobalCheckpoint() throws Exception { } assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(translogGenList.get(keptIndex))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(commitList.get(keptIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } public void testAcquireIndexCommit() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); + final int extraRetainedOps = between(0, 100); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, extraRetainedOps); final UUID translogUUID = UUID.randomUUID(); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); long lastMaxSeqNo = between(1, 1000); + long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); long lastTranslogGen = between(1, 20); int safeIndex = 0; List commitList = new ArrayList<>(); @@ -102,8 +111,9 @@ public void testAcquireIndexCommit() throws Exception { int newCommits = between(1, 10); for (int n = 0; n < newCommits; n++) { lastMaxSeqNo += between(1, 1000); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); lastTranslogGen += between(1, 20); - commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); } // Advance the global checkpoint to between [safeIndex, safeIndex + 1) safeIndex = randomIntBetween(safeIndex, commitList.size() - 1); @@ -114,6 +124,9 @@ public void testAcquireIndexCommit() throws Exception { globalCheckpoint.set(randomLongBetween(lower, upper)); commitList.forEach(this::resetDeletion); indexPolicy.onCommit(commitList); + IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); // Captures and releases some commits int captures = between(0, 5); for (int n = 0; n < captures; n++) { @@ -132,7 +145,7 @@ public void testAcquireIndexCommit() throws Exception { snapshottingCommits.remove(snapshot); final long pendingSnapshots = snapshottingCommits.stream().filter(snapshot::equals).count(); final IndexCommit lastCommit = commitList.get(commitList.size() - 1); - final IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); + safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); assertThat(indexPolicy.releaseCommit(snapshot), equalTo(pendingSnapshots == 0 && snapshot.equals(lastCommit) == false && snapshot.equals(safeCommit) == false)); } @@ -143,6 +156,8 @@ public void testAcquireIndexCommit() throws Exception { equalTo(Long.parseLong(commitList.get(safeIndex).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(Long.parseLong(commitList.get(commitList.size() - 1).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(commitList.get(safeIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } snapshottingCommits.forEach(indexPolicy::releaseCommit); globalCheckpoint.set(randomLongBetween(lastMaxSeqNo, Long.MAX_VALUE)); @@ -154,25 +169,27 @@ public void testAcquireIndexCommit() throws Exception { assertThat(commitList.get(commitList.size() - 1).isDeleted(), equalTo(false)); assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(lastTranslogGen)); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); + IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); } public void testLegacyIndex() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); final UUID translogUUID = UUID.randomUUID(); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); long legacyTranslogGen = randomNonNegativeLong(); IndexCommit legacyCommit = mockLegacyIndexCommit(translogUUID, legacyTranslogGen); - indexPolicy.onCommit(singletonList(legacyCommit)); - verify(legacyCommit, never()).delete(); - assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(legacyTranslogGen)); - assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(legacyTranslogGen)); + assertThat(CombinedDeletionPolicy.findSafeCommitPoint(singletonList(legacyCommit), globalCheckpoint.get()), + equalTo(legacyCommit)); long safeTranslogGen = randomLongBetween(legacyTranslogGen, Long.MAX_VALUE); long maxSeqNo = randomLongBetween(1, Long.MAX_VALUE); - final IndexCommit freshCommit = mockIndexCommit(maxSeqNo, translogUUID, safeTranslogGen); + final IndexCommit freshCommit = mockIndexCommit(randomLongBetween(-1, maxSeqNo), maxSeqNo, translogUUID, safeTranslogGen); globalCheckpoint.set(randomLongBetween(0, maxSeqNo - 1)); indexPolicy.onCommit(Arrays.asList(legacyCommit, freshCommit)); @@ -189,25 +206,32 @@ public void testLegacyIndex() throws Exception { verify(freshCommit, times(0)).delete(); assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(safeTranslogGen)); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(safeTranslogGen)); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), equalTo(getLocalCheckpoint(freshCommit) + 1)); } public void testDeleteInvalidCommits() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(randomNonNegativeLong()); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); final int invalidCommits = between(1, 10); final List commitList = new ArrayList<>(); for (int i = 0; i < invalidCommits; i++) { - commitList.add(mockIndexCommit(randomNonNegativeLong(), UUID.randomUUID(), randomNonNegativeLong())); + long maxSeqNo = randomNonNegativeLong(); + commitList.add(mockIndexCommit(randomLongBetween(-1, maxSeqNo), maxSeqNo, UUID.randomUUID(), randomNonNegativeLong())); } final UUID expectedTranslogUUID = UUID.randomUUID(); long lastTranslogGen = 0; final int validCommits = between(1, 10); + long lastMaxSeqNo = between(1, 1000); + long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); for (int i = 0; i < validCommits; i++) { lastTranslogGen += between(1, 1000); - commitList.add(mockIndexCommit(randomNonNegativeLong(), expectedTranslogUUID, lastTranslogGen)); + lastMaxSeqNo += between(1, 1000); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, expectedTranslogUUID, lastTranslogGen)); } // We should never keep invalid commits regardless of the value of the global checkpoint. @@ -215,21 +239,26 @@ public void testDeleteInvalidCommits() throws Exception { for (int i = 0; i < invalidCommits - 1; i++) { verify(commitList.get(i), times(1)).delete(); } + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), + equalTo(getLocalCheckpoint(CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get())) + 1)); } public void testCheckUnreferencedCommits() throws Exception { final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.UNASSIGNED_SEQ_NO); + final SoftDeletesPolicy softDeletesPolicy = new SoftDeletesPolicy(globalCheckpoint::get, -1, 0); final UUID translogUUID = UUID.randomUUID(); final TranslogDeletionPolicy translogPolicy = createTranslogDeletionPolicy(); - CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, globalCheckpoint::get); + CombinedDeletionPolicy indexPolicy = new CombinedDeletionPolicy(logger, translogPolicy, softDeletesPolicy, globalCheckpoint::get); final List commitList = new ArrayList<>(); int totalCommits = between(2, 20); long lastMaxSeqNo = between(1, 1000); + long lastCheckpoint = randomLongBetween(-1, lastMaxSeqNo); long lastTranslogGen = between(1, 50); for (int i = 0; i < totalCommits; i++) { lastMaxSeqNo += between(1, 10000); lastTranslogGen += between(1, 100); - commitList.add(mockIndexCommit(lastMaxSeqNo, translogUUID, lastTranslogGen)); + lastCheckpoint = randomLongBetween(lastCheckpoint, lastMaxSeqNo); + commitList.add(mockIndexCommit(lastCheckpoint, lastMaxSeqNo, translogUUID, lastTranslogGen)); } IndexCommit safeCommit = randomFrom(commitList); globalCheckpoint.set(Long.parseLong(safeCommit.getUserData().get(SequenceNumbers.MAX_SEQ_NO))); @@ -256,8 +285,9 @@ public void testCheckUnreferencedCommits() throws Exception { } } - IndexCommit mockIndexCommit(long maxSeqNo, UUID translogUUID, long translogGen) throws IOException { + IndexCommit mockIndexCommit(long localCheckpoint, long maxSeqNo, UUID translogUUID, long translogGen) throws IOException { final Map userData = new HashMap<>(); + userData.put(SequenceNumbers.LOCAL_CHECKPOINT_KEY, Long.toString(localCheckpoint)); userData.put(SequenceNumbers.MAX_SEQ_NO, Long.toString(maxSeqNo)); userData.put(Translog.TRANSLOG_UUID_KEY, translogUUID.toString()); userData.put(Translog.TRANSLOG_GENERATION_KEY, Long.toString(translogGen)); @@ -278,6 +308,10 @@ void resetDeletion(IndexCommit commit) { }).when(commit).delete(); } + private long getLocalCheckpoint(IndexCommit commit) throws IOException { + return Long.parseLong(commit.getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + } + IndexCommit mockLegacyIndexCommit(UUID translogUUID, long translogGen) throws IOException { final Map userData = new HashMap<>(); userData.put(Translog.TRANSLOG_UUID_KEY, translogUUID.toString()); @@ -287,4 +321,5 @@ IndexCommit mockLegacyIndexCommit(UUID translogUUID, long translogGen) throws IO resetDeletion(commit); return commit; } + } 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 76e05ba1e0b5..d3aead9e44e1 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.engine; +import java.io.Closeable; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.Charset; @@ -77,10 +78,12 @@ import org.apache.lucene.index.LiveIndexWriterConfig; import org.apache.lucene.index.LogByteSizeMergePolicy; import org.apache.lucene.index.LogDocMergePolicy; +import org.apache.lucene.index.MergePolicy; import org.apache.lucene.index.NoMergePolicy; import org.apache.lucene.index.NumericDocValues; import org.apache.lucene.index.PointValues; import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.SoftDeletesRetentionMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.index.TieredMergePolicy; import org.apache.lucene.search.IndexSearcher; @@ -114,6 +117,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver; @@ -133,6 +137,7 @@ import org.elasticsearch.index.mapper.ContentPath; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.Mapper.BuilderContext; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.MetadataFieldMapper; import org.elasticsearch.index.mapper.ParseContext; @@ -172,8 +177,10 @@ import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -247,8 +254,13 @@ public void testVersionMapAfterAutoIDDocument() throws IOException { } public void testSegments() throws Exception { + Settings settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + IndexSettings indexSettings = IndexSettingsModule.newIndexSettings( + IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); try (Store store = createStore(); - InternalEngine engine = createEngine(defaultSettings, store, createTempDir(), NoMergePolicy.INSTANCE)) { + InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), NoMergePolicy.INSTANCE, null))) { List segments = engine.segments(false); assertThat(segments.isEmpty(), equalTo(true)); assertThat(engine.segmentsStats(false).getCount(), equalTo(0L)); @@ -1311,9 +1323,13 @@ public void testVersioningNewIndex() throws IOException { assertThat(indexResult.getVersion(), equalTo(1L)); } - public void testForceMerge() throws IOException { + public void testForceMergeWithoutSoftDeletes() throws IOException { + Settings settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); try (Store store = createStore(); - Engine engine = createEngine(config(defaultSettings, store, createTempDir(), + Engine engine = createEngine(config(IndexSettingsModule.newIndexSettings(indexMetaData), store, createTempDir(), new LogByteSizeMergePolicy(), null))) { // use log MP here we test some behavior in ESMP int numDocs = randomIntBetween(10, 100); for (int i = 0; i < numDocs; i++) { @@ -1354,6 +1370,165 @@ public void testForceMerge() throws IOException { } } + public void testForceMergeWithSoftDeletesRetention() throws Exception { + final long retainedExtraOps = randomLongBetween(0, 10); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + final MapperService mapperService = createMapperService("test"); + final Set liveDocs = new HashSet<>(); + try (Store store = createStore(); + InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get))) { + int numDocs = scaledRandomIntBetween(10, 100); + for (int i = 0; i < numDocs; i++) { + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null); + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + } + for (int i = 0; i < numDocs; i++) { + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null); + if (randomBoolean()) { + engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); + liveDocs.remove(doc.id()); + } + if (randomBoolean()) { + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + } + if (randomBoolean()) { + engine.flush(randomBoolean(), true); + } + } + engine.flush(); + + long localCheckpoint = engine.getLocalCheckpoint(); + globalCheckpoint.set(randomLongBetween(0, localCheckpoint)); + engine.syncTranslog(); + final long safeCommitCheckpoint; + try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { + safeCommitCheckpoint = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + } + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + Map ops = readAllOperationsInLucene(engine, mapperService) + .stream().collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); + for (long seqno = 0; seqno <= localCheckpoint; seqno++) { + long minSeqNoToRetain = Math.min(globalCheckpoint.get() + 1 - retainedExtraOps, safeCommitCheckpoint + 1); + String msg = "seq# [" + seqno + "], global checkpoint [" + globalCheckpoint + "], retained-ops [" + retainedExtraOps + "]"; + if (seqno < minSeqNoToRetain) { + Translog.Operation op = ops.get(seqno); + if (op != null) { + assertThat(op, instanceOf(Translog.Index.class)); + assertThat(msg, ((Translog.Index) op).id(), isIn(liveDocs)); + assertEquals(msg, ((Translog.Index) op).source(), B_1); + } + } else { + assertThat(msg, ops.get(seqno), notNullValue()); + } + } + settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); + indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); + engine.onSettingsChanged(); + globalCheckpoint.set(localCheckpoint); + engine.syncTranslog(); + + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + assertThat(readAllOperationsInLucene(engine, mapperService), hasSize(liveDocs.size())); + } + } + + public void testForceMergeWithSoftDeletesRetentionAndRecoverySource() throws Exception { + final long retainedExtraOps = randomLongBetween(0, 10); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), retainedExtraOps); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + final MapperService mapperService = createMapperService("test"); + final boolean omitSourceAllTheTime = randomBoolean(); + final Set liveDocs = new HashSet<>(); + final Set liveDocsWithSource = new HashSet<>(); + try (Store store = createStore(); + InternalEngine engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, + globalCheckpoint::get))) { + int numDocs = scaledRandomIntBetween(10, 100); + for (int i = 0; i < numDocs; i++) { + boolean useRecoverySource = randomBoolean() || omitSourceAllTheTime; + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null, useRecoverySource); + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + if (useRecoverySource == false) { + liveDocsWithSource.add(Integer.toString(i)); + } + } + for (int i = 0; i < numDocs; i++) { + boolean useRecoverySource = randomBoolean() || omitSourceAllTheTime; + ParsedDocument doc = testParsedDocument(Integer.toString(i), null, testDocument(), B_1, null, useRecoverySource); + if (randomBoolean()) { + engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); + liveDocs.remove(doc.id()); + liveDocsWithSource.remove(doc.id()); + } + if (randomBoolean()) { + engine.index(indexForDoc(doc)); + liveDocs.add(doc.id()); + if (useRecoverySource == false) { + liveDocsWithSource.add(doc.id()); + } else { + liveDocsWithSource.remove(doc.id()); + } + } + if (randomBoolean()) { + engine.flush(randomBoolean(), true); + } + } + engine.flush(); + globalCheckpoint.set(randomLongBetween(0, engine.getLocalCheckpoint())); + engine.syncTranslog(); + final long minSeqNoToRetain; + try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { + long safeCommitLocalCheckpoint = Long.parseLong( + safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)); + minSeqNoToRetain = Math.min(globalCheckpoint.get() + 1 - retainedExtraOps, safeCommitLocalCheckpoint + 1); + } + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + Map ops = readAllOperationsInLucene(engine, mapperService) + .stream().collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); + for (long seqno = 0; seqno <= engine.getLocalCheckpoint(); seqno++) { + String msg = "seq# [" + seqno + "], global checkpoint [" + globalCheckpoint + "], retained-ops [" + retainedExtraOps + "]"; + if (seqno < minSeqNoToRetain) { + Translog.Operation op = ops.get(seqno); + if (op != null) { + assertThat(op, instanceOf(Translog.Index.class)); + assertThat(msg, ((Translog.Index) op).id(), isIn(liveDocs)); + } + } else { + Translog.Operation op = ops.get(seqno); + assertThat(msg, op, notNullValue()); + if (op instanceof Translog.Index) { + assertEquals(msg, ((Translog.Index) op).source(), B_1); + } + } + } + settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0); + indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); + engine.onSettingsChanged(); + globalCheckpoint.set(engine.getLocalCheckpoint()); + engine.syncTranslog(); + engine.forceMerge(true, 1, false, false, false); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + assertThat(readAllOperationsInLucene(engine, mapperService), hasSize(liveDocsWithSource.size())); + } + } + public void testForceMergeAndClose() throws IOException, InterruptedException { int numIters = randomIntBetween(2, 10); for (int j = 0; j < numIters; j++) { @@ -1422,126 +1597,10 @@ public void testVersioningCreateExistsException() throws IOException { assertThat(indexResult.getFailure(), instanceOf(VersionConflictEngineException.class)); } - protected List generateSingleDocHistory(boolean forReplica, VersionType versionType, - long primaryTerm, - int minOpCount, int maxOpCount, String docId) { - final int numOfOps = randomIntBetween(minOpCount, maxOpCount); - final List ops = new ArrayList<>(); - final Term id = newUid(docId); - final int startWithSeqNo = 0; - final String valuePrefix = (forReplica ? "r_" : "p_" ) + docId + "_"; - final boolean incrementTermWhenIntroducingSeqNo = randomBoolean(); - for (int i = 0; i < numOfOps; i++) { - final Engine.Operation op; - final long version; - switch (versionType) { - case INTERNAL: - version = forReplica ? i : Versions.MATCH_ANY; - break; - case EXTERNAL: - version = i; - break; - case EXTERNAL_GTE: - version = randomBoolean() ? Math.max(i - 1, 0) : i; - break; - case FORCE: - version = randomNonNegativeLong(); - break; - default: - throw new UnsupportedOperationException("unknown version type: " + versionType); - } - if (randomBoolean()) { - op = new Engine.Index(id, testParsedDocument(docId, null, testDocumentWithTextField(valuePrefix + i), B_1, null), - forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, - forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, - version, - forReplica ? null : versionType, - forReplica ? REPLICA : PRIMARY, - System.currentTimeMillis(), -1, false - ); - } else { - op = new Engine.Delete("test", docId, id, - forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, - forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, - version, - forReplica ? null : versionType, - forReplica ? REPLICA : PRIMARY, - System.currentTimeMillis()); - } - ops.add(op); - } - return ops; - } - public void testOutOfOrderDocsOnReplica() throws IOException { final List ops = generateSingleDocHistory(true, randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL, VersionType.EXTERNAL_GTE, VersionType.FORCE), 2, 2, 20, "1"); - assertOpsOnReplica(ops, replicaEngine, true); - } - - private void assertOpsOnReplica(List ops, InternalEngine replicaEngine, boolean shuffleOps) throws IOException { - final Engine.Operation lastOp = ops.get(ops.size() - 1); - final String lastFieldValue; - if (lastOp instanceof Engine.Index) { - Engine.Index index = (Engine.Index) lastOp; - lastFieldValue = index.docs().get(0).get("value"); - } else { - // delete - lastFieldValue = null; - } - if (shuffleOps) { - int firstOpWithSeqNo = 0; - while (firstOpWithSeqNo < ops.size() && ops.get(firstOpWithSeqNo).seqNo() < 0) { - firstOpWithSeqNo++; - } - // shuffle ops but make sure legacy ops are first - shuffle(ops.subList(0, firstOpWithSeqNo), random()); - shuffle(ops.subList(firstOpWithSeqNo, ops.size()), random()); - } - boolean firstOp = true; - for (Engine.Operation op : ops) { - logger.info("performing [{}], v [{}], seq# [{}], term [{}]", - op.operationType().name().charAt(0), op.version(), op.seqNo(), op.primaryTerm()); - if (op instanceof Engine.Index) { - Engine.IndexResult result = replicaEngine.index((Engine.Index) op); - // replicas don't really care to about creation status of documents - // this allows to ignore the case where a document was found in the live version maps in - // a delete state and return false for the created flag in favor of code simplicity - // as deleted or not. This check is just signal regression so a decision can be made if it's - // intentional - assertThat(result.isCreated(), equalTo(firstOp)); - assertThat(result.getVersion(), equalTo(op.version())); - assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); - - } else { - Engine.DeleteResult result = replicaEngine.delete((Engine.Delete) op); - // Replicas don't really care to about found status of documents - // this allows to ignore the case where a document was found in the live version maps in - // a delete state and return true for the found flag in favor of code simplicity - // his check is just signal regression so a decision can be made if it's - // intentional - assertThat(result.isFound(), equalTo(firstOp == false)); - assertThat(result.getVersion(), equalTo(op.version())); - assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); - } - if (randomBoolean()) { - engine.refresh("test"); - } - if (randomBoolean()) { - engine.flush(); - engine.refresh("test"); - } - firstOp = false; - } - - assertVisibleCount(replicaEngine, lastFieldValue == null ? 0 : 1); - if (lastFieldValue != null) { - try (Searcher searcher = replicaEngine.acquireSearcher("test")) { - final TotalHitCountCollector collector = new TotalHitCountCollector(); - searcher.searcher().search(new TermQuery(new Term("value", lastFieldValue)), collector); - assertThat(collector.getTotalHits(), equalTo(1)); - } - } + assertOpsOnReplica(ops, replicaEngine, true, logger); } public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, InterruptedException { @@ -1569,11 +1628,12 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup } // randomly interleave final AtomicLong seqNoGenerator = new AtomicLong(); - Function seqNoUpdater = operation -> { - final long newSeqNo = seqNoGenerator.getAndIncrement(); + BiFunction seqNoUpdater = (operation, newSeqNo) -> { if (operation instanceof Engine.Index) { Engine.Index index = (Engine.Index) operation; - return new Engine.Index(index.uid(), index.parsedDoc(), newSeqNo, index.primaryTerm(), index.version(), + Document doc = testDocumentWithTextField(index.docs().get(0).get("value")); + ParsedDocument parsedDocument = testParsedDocument(index.id(), index.routing(), doc, index.source(), null); + return new Engine.Index(index.uid(), parsedDocument, newSeqNo, index.primaryTerm(), index.version(), index.versionType(), index.origin(), index.startTime(), index.getAutoGeneratedIdTimestamp(), index.isRetry()); } else { Engine.Delete delete = (Engine.Delete) operation; @@ -1586,12 +1646,12 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup Iterator iter2 = opsDoc2.iterator(); while (iter1.hasNext() && iter2.hasNext()) { final Engine.Operation next = randomBoolean() ? iter1.next() : iter2.next(); - allOps.add(seqNoUpdater.apply(next)); + allOps.add(seqNoUpdater.apply(next, seqNoGenerator.getAndIncrement())); } - iter1.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o))); - iter2.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o))); + iter1.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o, seqNoGenerator.getAndIncrement()))); + iter2.forEachRemaining(o -> allOps.add(seqNoUpdater.apply(o, seqNoGenerator.getAndIncrement()))); // insert some duplicates - allOps.addAll(randomSubsetOf(allOps)); + randomSubsetOf(allOps).forEach(op -> allOps.add(seqNoUpdater.apply(op, op.seqNo()))); shuffle(allOps, random()); concurrentlyApplyOps(allOps, engine); @@ -1623,42 +1683,6 @@ public void testConcurrentOutOfOrderDocsOnReplica() throws IOException, Interrup assertVisibleCount(engine, totalExpectedOps); } - private void concurrentlyApplyOps(List ops, InternalEngine engine) throws InterruptedException { - Thread[] thread = new Thread[randomIntBetween(3, 5)]; - CountDownLatch startGun = new CountDownLatch(thread.length); - AtomicInteger offset = new AtomicInteger(-1); - for (int i = 0; i < thread.length; i++) { - thread[i] = new Thread(() -> { - startGun.countDown(); - try { - startGun.await(); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - int docOffset; - while ((docOffset = offset.incrementAndGet()) < ops.size()) { - try { - final Engine.Operation op = ops.get(docOffset); - if (op instanceof Engine.Index) { - engine.index((Engine.Index) op); - } else { - engine.delete((Engine.Delete) op); - } - if ((docOffset + 1) % 4 == 0) { - engine.refresh("test"); - } - } catch (IOException e) { - throw new AssertionError(e); - } - } - }); - thread[i].start(); - } - for (int i = 0; i < thread.length; i++) { - thread[i].join(); - } - } - public void testInternalVersioningOnPrimary() throws IOException { final List ops = generateSingleDocHistory(false, VersionType.INTERNAL, 2, 2, 20, "1"); assertOpsOnPrimary(ops, Versions.NOT_FOUND, true, engine); @@ -1869,7 +1893,7 @@ public void testVersioningPromotedReplica() throws IOException { final boolean deletedOnReplica = lastReplicaOp instanceof Engine.Delete; final long finalReplicaVersion = lastReplicaOp.version(); final long finalReplicaSeqNo = lastReplicaOp.seqNo(); - assertOpsOnReplica(replicaOps, replicaEngine, true); + assertOpsOnReplica(replicaOps, replicaEngine, true, logger); final int opsOnPrimary = assertOpsOnPrimary(primaryOps, finalReplicaVersion, deletedOnReplica, replicaEngine); final long currentSeqNo = getSequenceID(replicaEngine, new Engine.Get(false, false, "type", lastReplicaOp.uid().text(), lastReplicaOp.uid())).v1(); @@ -2674,14 +2698,16 @@ public void testSkipTranslogReplay() throws IOException { Engine.IndexResult indexResult = engine.index(firstIndexRequest); assertThat(indexResult.getVersion(), equalTo(1L)); } + EngineConfig config = engine.config(); assertVisibleCount(engine, numDocs); engine.close(); - trimUnsafeCommits(engine.config()); - engine = new InternalEngine(engine.config()); - engine.skipTranslogRecovery(); - try (Engine.Searcher searcher = engine.acquireSearcher("test")) { - TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), randomIntBetween(numDocs, numDocs + 10)); - assertThat(topDocs.totalHits, equalTo(0L)); + trimUnsafeCommits(config); + try (InternalEngine engine = new InternalEngine(config)) { + engine.skipTranslogRecovery(); + try (Engine.Searcher searcher = engine.acquireSearcher("test")) { + TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), randomIntBetween(numDocs, numDocs + 10)); + assertThat(topDocs.totalHits, equalTo(0L)); + } } } @@ -2811,7 +2837,7 @@ public void testRecoverFromForeignTranslog() throws IOException { new CodecService(null, logger), config.getEventListener(), IndexSearcher.getDefaultQueryCache(), IndexSearcher.getDefaultQueryCachingPolicy(), translogConfig, TimeValue.timeValueMinutes(5), config.getExternalRefreshListener(), config.getInternalRefreshListener(), null, config.getTranslogRecoveryRunner(), - new NoneCircuitBreakerService(), () -> SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm::get); + new NoneCircuitBreakerService(), () -> SequenceNumbers.UNASSIGNED_SEQ_NO, primaryTerm::get, tombstoneDocSupplier()); try { InternalEngine internalEngine = new InternalEngine(brokenConfig); fail("translog belongs to a different engine"); @@ -2940,6 +2966,12 @@ private void maybeThrowFailure() throws IOException { } } + @Override + public long softUpdateDocument(Term term, Iterable doc, Field... softDeletes) throws IOException { + maybeThrowFailure(); + return super.softUpdateDocument(term, doc, softDeletes); + } + @Override public long deleteDocuments(Term... terms) throws IOException { maybeThrowFailure(); @@ -3140,10 +3172,10 @@ public void testDoubleDeliveryReplicaAppendingAndDeleteOnly() throws IOException } public void testDoubleDeliveryReplicaAppendingOnly() throws IOException { - final ParsedDocument doc = testParsedDocument("1", null, testDocumentWithTextField(), + final Supplier doc = () -> testParsedDocument("1", null, testDocumentWithTextField(), new BytesArray("{}".getBytes(Charset.defaultCharset())), null); - Engine.Index operation = appendOnlyReplica(doc, false, 1, randomIntBetween(0, 5)); - Engine.Index retry = appendOnlyReplica(doc, true, 1, randomIntBetween(0, 5)); + Engine.Index operation = appendOnlyReplica(doc.get(), false, 1, randomIntBetween(0, 5)); + Engine.Index retry = appendOnlyReplica(doc.get(), true, 1, randomIntBetween(0, 5)); // operations with a seq# equal or lower to the local checkpoint are not indexed to lucene // and the version lookup is skipped final boolean belowLckp = operation.seqNo() == 0 && retry.seqNo() == 0; @@ -3182,8 +3214,8 @@ public void testDoubleDeliveryReplicaAppendingOnly() throws IOException { TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), 10); assertEquals(1, topDocs.totalHits); } - operation = randomAppendOnly(doc, false, 1); - retry = randomAppendOnly(doc, true, 1); + operation = randomAppendOnly(doc.get(), false, 1); + retry = randomAppendOnly(doc.get(), true, 1); if (randomBoolean()) { Engine.IndexResult indexResult = engine.index(operation); assertNotNull(indexResult.getTranslogLocation()); @@ -3248,6 +3280,8 @@ public void testDoubleDeliveryReplica() throws IOException { TopDocs topDocs = searcher.searcher().search(new MatchAllDocsQuery(), 10); assertEquals(1, topDocs.totalHits); } + List ops = readAllOperationsInLucene(engine, createMapperService("test")); + assertThat(ops.stream().map(o -> o.seqNo()).collect(Collectors.toList()), hasItem(20L)); } public void testRetryWithAutogeneratedIdWorksAndNoDuplicateDocs() throws IOException { @@ -3716,20 +3750,22 @@ public void testOutOfOrderSequenceNumbersWithVersionConflict() throws IOExceptio final List operations = new ArrayList<>(); final int numberOfOperations = randomIntBetween(16, 32); - final Document document = testDocumentWithTextField(); final AtomicLong sequenceNumber = new AtomicLong(); final Engine.Operation.Origin origin = randomFrom(LOCAL_TRANSLOG_RECOVERY, PEER_RECOVERY, PRIMARY, REPLICA); final LongSupplier sequenceNumberSupplier = origin == PRIMARY ? () -> SequenceNumbers.UNASSIGNED_SEQ_NO : sequenceNumber::getAndIncrement; - document.add(new Field(SourceFieldMapper.NAME, BytesReference.toBytes(B_1), SourceFieldMapper.Defaults.FIELD_TYPE)); - final ParsedDocument doc = testParsedDocument("1", null, document, B_1, null); - final Term uid = newUid(doc); + final Supplier doc = () -> { + final Document document = testDocumentWithTextField(); + document.add(new Field(SourceFieldMapper.NAME, BytesReference.toBytes(B_1), SourceFieldMapper.Defaults.FIELD_TYPE)); + return testParsedDocument("1", null, document, B_1, null); + }; + final Term uid = newUid("1"); final BiFunction searcherFactory = engine::acquireSearcher; for (int i = 0; i < numberOfOperations; i++) { if (randomBoolean()) { final Engine.Index index = new Engine.Index( uid, - doc, + doc.get(), sequenceNumberSupplier.getAsLong(), 1, i, @@ -3805,7 +3841,9 @@ public void testNoOps() throws IOException { maxSeqNo, localCheckpoint); trimUnsafeCommits(engine.config()); - noOpEngine = new InternalEngine(engine.config(), supplier) { + EngineConfig noopEngineConfig = copy(engine.config(), new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, + () -> new MatchAllDocsQuery(), engine.config().getMergePolicy())); + noOpEngine = new InternalEngine(noopEngineConfig, supplier) { @Override protected long doGenerateSeqNoForOperation(Operation operation) { throw new UnsupportedOperationException(); @@ -3813,7 +3851,7 @@ protected long doGenerateSeqNoForOperation(Operation operation) { }; noOpEngine.recoverFromTranslog(Long.MAX_VALUE); final int gapsFilled = noOpEngine.fillSeqNoGaps(primaryTerm.get()); - final String reason = randomAlphaOfLength(16); + final String reason = "filling gaps"; noOpEngine.noOp(new Engine.NoOp(maxSeqNo + 1, primaryTerm.get(), LOCAL_TRANSLOG_RECOVERY, System.nanoTime(), reason)); assertThat(noOpEngine.getLocalCheckpoint(), equalTo((long) (maxSeqNo + 1))); assertThat(noOpEngine.getTranslog().stats().getUncommittedOperations(), equalTo(gapsFilled)); @@ -3835,11 +3873,77 @@ protected long doGenerateSeqNoForOperation(Operation operation) { assertThat(noOp.seqNo(), equalTo((long) (maxSeqNo + 2))); assertThat(noOp.primaryTerm(), equalTo(primaryTerm.get())); assertThat(noOp.reason(), equalTo(reason)); + if (engine.engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + MapperService mapperService = createMapperService("test"); + List operationsFromLucene = readAllOperationsInLucene(noOpEngine, mapperService); + assertThat(operationsFromLucene, hasSize(maxSeqNo + 2 - localCheckpoint)); // fills n gap and 2 manual noop. + for (int i = 0; i < operationsFromLucene.size(); i++) { + assertThat(operationsFromLucene.get(i), equalTo(new Translog.NoOp(localCheckpoint + 1 + i, primaryTerm.get(), "filling gaps"))); + } + assertConsistentHistoryBetweenTranslogAndLuceneIndex(noOpEngine, mapperService); + } } finally { IOUtils.close(noOpEngine); } } + /** + * Verifies that a segment containing only no-ops can be used to look up _version and _seqno. + */ + public void testSegmentContainsOnlyNoOps() throws Exception { + Engine.NoOpResult noOpResult = engine.noOp(new Engine.NoOp(1, primaryTerm.get(), + randomFrom(Engine.Operation.Origin.values()), randomNonNegativeLong(), "test")); + assertThat(noOpResult.getFailure(), nullValue()); + engine.refresh("test"); + Engine.DeleteResult deleteResult = engine.delete(replicaDeleteForDoc("id", 1, 2, randomNonNegativeLong())); + assertThat(deleteResult.getFailure(), nullValue()); + engine.refresh("test"); + } + + /** + * A simple test to check that random combination of operations can coexist in segments and be lookup. + * This is needed as some fields in Lucene may not exist if a segment misses operation types and this code is to check for that. + * For example, a segment containing only no-ops does not have neither _uid or _version. + */ + public void testRandomOperations() throws Exception { + int numOps = between(10, 100); + for (int i = 0; i < numOps; i++) { + String id = Integer.toString(randomIntBetween(1, 10)); + ParsedDocument doc = createParsedDoc(id, null); + Engine.Operation.TYPE type = randomFrom(Engine.Operation.TYPE.values()); + switch (type) { + case INDEX: + Engine.IndexResult index = engine.index(replicaIndexForDoc(doc, between(1, 100), i, randomBoolean())); + assertThat(index.getFailure(), nullValue()); + break; + case DELETE: + Engine.DeleteResult delete = engine.delete(replicaDeleteForDoc(doc.id(), between(1, 100), i, randomNonNegativeLong())); + assertThat(delete.getFailure(), nullValue()); + break; + case NO_OP: + Engine.NoOpResult noOp = engine.noOp(new Engine.NoOp(i, primaryTerm.get(), + randomFrom(Engine.Operation.Origin.values()), randomNonNegativeLong(), "")); + assertThat(noOp.getFailure(), nullValue()); + break; + default: + throw new IllegalStateException("Invalid op [" + type + "]"); + } + if (randomBoolean()) { + engine.refresh("test"); + } + if (randomBoolean()) { + engine.flush(); + } + if (randomBoolean()) { + engine.forceMerge(randomBoolean(), between(1, 10), randomBoolean(), false, false); + } + } + if (engine.engineConfig.getIndexSettings().isSoftDeleteEnabled()) { + List operations = readAllOperationsInLucene(engine, createMapperService("test")); + assertThat(operations, hasSize(numOps)); + } + } + public void testMinGenerationForSeqNo() throws IOException, BrokenBarrierException, InterruptedException { engine.close(); final int numberOfTriplets = randomIntBetween(1, 32); @@ -4405,7 +4509,7 @@ public void testCleanUpCommitsWhenGlobalCheckpointAdvanced() throws Exception { globalCheckpoint.set(randomLongBetween(engine.getLocalCheckpoint(), Long.MAX_VALUE)); engine.syncTranslog(); assertThat(DirectoryReader.listCommits(store.directory()), contains(commits.get(commits.size() - 1))); - assertThat(engine.estimateTranslogOperationsFromMinSeq(0L), equalTo(0)); + assertThat(engine.getTranslog().totalOperations(), equalTo(0)); } } @@ -4768,6 +4872,154 @@ public void testTrimUnsafeCommits() throws Exception { } } + public void testLuceneHistoryOnPrimary() throws Exception { + final List operations = generateSingleDocHistory(false, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "1"); + assertOperationHistoryInLucene(operations); + } + + public void testLuceneHistoryOnReplica() throws Exception { + final List operations = generateSingleDocHistory(true, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "2"); + Randomness.shuffle(operations); + assertOperationHistoryInLucene(operations); + } + + private void assertOperationHistoryInLucene(List operations) throws IOException { + final MergePolicy keepSoftDeleteDocsMP = new SoftDeletesRetentionMergePolicy( + Lucene.SOFT_DELETES_FIELD, () -> new MatchAllDocsQuery(), engine.config().getMergePolicy()); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + Set expectedSeqNos = new HashSet<>(); + try (Store store = createStore(); + Engine engine = createEngine(config(indexSettings, store, createTempDir(), keepSoftDeleteDocsMP, null))) { + for (Engine.Operation op : operations) { + if (op instanceof Engine.Index) { + Engine.IndexResult indexResult = engine.index((Engine.Index) op); + assertThat(indexResult.getFailure(), nullValue()); + expectedSeqNos.add(indexResult.getSeqNo()); + } else { + Engine.DeleteResult deleteResult = engine.delete((Engine.Delete) op); + assertThat(deleteResult.getFailure(), nullValue()); + expectedSeqNos.add(deleteResult.getSeqNo()); + } + if (rarely()) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(); + } + if (rarely()) { + engine.forceMerge(true); + } + } + MapperService mapperService = createMapperService("test"); + List actualOps = readAllOperationsInLucene(engine, mapperService); + assertThat(actualOps.stream().map(o -> o.seqNo()).collect(Collectors.toList()), containsInAnyOrder(expectedSeqNos.toArray())); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, mapperService); + } + } + + public void testKeepMinRetainedSeqNoByMergePolicy() throws IOException { + IOUtils.close(engine, store); + Settings.Builder settings = Settings.builder() + .put(defaultSettings.getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); + final IndexMetaData indexMetaData = IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build(); + final IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(indexMetaData); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + final List operations = generateSingleDocHistory(true, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL), 2, 10, 300, "2"); + Randomness.shuffle(operations); + Set existingSeqNos = new HashSet<>(); + store = createStore(); + engine = createEngine(config(indexSettings, store, createTempDir(), newMergePolicy(), null, null, globalCheckpoint::get)); + assertThat(engine.getMinRetainedSeqNo(), equalTo(0L)); + long lastMinRetainedSeqNo = engine.getMinRetainedSeqNo(); + for (Engine.Operation op : operations) { + final Engine.Result result; + if (op instanceof Engine.Index) { + result = engine.index((Engine.Index) op); + } else { + result = engine.delete((Engine.Delete) op); + } + existingSeqNos.add(result.getSeqNo()); + if (randomBoolean()) { + globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getLocalCheckpointTracker().getCheckpoint())); + } + if (rarely()) { + settings.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), randomLongBetween(0, 10)); + indexSettings.updateIndexMetaData(IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(settings).build()); + engine.onSettingsChanged(); + } + if (rarely()) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(true, true); + assertThat(Long.parseLong(engine.getLastCommittedSegmentInfos().userData.get(Engine.MIN_RETAINED_SEQNO)), + equalTo(engine.getMinRetainedSeqNo())); + } + if (rarely()) { + engine.forceMerge(randomBoolean()); + } + try (Closeable ignored = engine.acquireRetentionLockForPeerRecovery()) { + long minRetainSeqNos = engine.getMinRetainedSeqNo(); + assertThat(minRetainSeqNos, lessThanOrEqualTo(globalCheckpoint.get() + 1)); + Long[] expectedOps = existingSeqNos.stream().filter(seqno -> seqno >= minRetainSeqNos).toArray(Long[]::new); + Set actualOps = readAllOperationsInLucene(engine, createMapperService("test")).stream() + .map(Translog.Operation::seqNo).collect(Collectors.toSet()); + assertThat(actualOps, containsInAnyOrder(expectedOps)); + } + try (Engine.IndexCommitRef commitRef = engine.acquireSafeIndexCommit()) { + IndexCommit safeCommit = commitRef.getIndexCommit(); + if (safeCommit.getUserData().containsKey(Engine.MIN_RETAINED_SEQNO)) { + lastMinRetainedSeqNo = Long.parseLong(safeCommit.getUserData().get(Engine.MIN_RETAINED_SEQNO)); + } + } + } + if (randomBoolean()) { + engine.close(); + } else { + engine.flushAndClose(); + } + trimUnsafeCommits(engine.config()); + try (InternalEngine recoveringEngine = new InternalEngine(engine.config())) { + assertThat(recoveringEngine.getMinRetainedSeqNo(), equalTo(lastMinRetainedSeqNo)); + } + } + + public void testLastRefreshCheckpoint() throws Exception { + AtomicBoolean done = new AtomicBoolean(); + Thread[] refreshThreads = new Thread[between(1, 8)]; + CountDownLatch latch = new CountDownLatch(refreshThreads.length); + for (int i = 0; i < refreshThreads.length; i++) { + latch.countDown(); + refreshThreads[i] = new Thread(() -> { + while (done.get() == false) { + long checkPointBeforeRefresh = engine.getLocalCheckpoint(); + engine.refresh("test", randomFrom(Engine.SearcherScope.values())); + assertThat(engine.lastRefreshedCheckpoint(), greaterThanOrEqualTo(checkPointBeforeRefresh)); + } + }); + refreshThreads[i].start(); + } + latch.await(); + List ops = generateSingleDocHistory(true, VersionType.EXTERNAL, 1, 10, 1000, "1"); + concurrentlyApplyOps(ops, engine); + done.set(true); + for (Thread thread : refreshThreads) { + thread.join(); + } + engine.refresh("test"); + assertThat(engine.lastRefreshedCheckpoint(), equalTo(engine.getLocalCheckpoint())); + } + private static void trimUnsafeCommits(EngineConfig config) throws IOException { final Store store = config.getStore(); final TranslogConfig translogConfig = config.getTranslogConfig(); diff --git a/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java b/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java new file mode 100644 index 000000000000..2d097366a272 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/engine/LuceneChangesSnapshotTests.java @@ -0,0 +1,289 @@ +/* + * 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.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.internal.io.IOUtils; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.store.Store; +import org.elasticsearch.index.translog.SnapshotMatchers; +import org.elasticsearch.index.translog.Translog; +import org.elasticsearch.test.IndexSettingsModule; +import org.junit.Before; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class LuceneChangesSnapshotTests extends EngineTestCase { + private MapperService mapperService; + + @Before + public void createMapper() throws Exception { + mapperService = createMapperService("test"); + } + + @Override + protected Settings indexSettings() { + return Settings.builder().put(super.indexSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) // always enable soft-deletes + .build(); + } + + public void testBasics() throws Exception { + long fromSeqNo = randomNonNegativeLong(); + long toSeqNo = randomLongBetween(fromSeqNo, Long.MAX_VALUE); + // Empty engine + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, true)) { + IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); + assertThat(error.getMessage(), + containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); + } + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, false)) { + assertThat(snapshot, SnapshotMatchers.size(0)); + } + int numOps = between(1, 100); + int refreshedSeqNo = -1; + for (int i = 0; i < numOps; i++) { + String id = Integer.toString(randomIntBetween(i, i + 5)); + ParsedDocument doc = createParsedDoc(id, null, randomBoolean()); + if (randomBoolean()) { + engine.index(indexForDoc(doc)); + } else { + engine.delete(new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get())); + } + if (rarely()) { + if (randomBoolean()) { + engine.flush(); + } else { + engine.refresh("test"); + } + refreshedSeqNo = i; + } + } + if (refreshedSeqNo == -1) { + fromSeqNo = between(0, numOps); + toSeqNo = randomLongBetween(fromSeqNo, numOps * 2); + + Engine.Searcher searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, false)) { + searcher = null; + assertThat(snapshot, SnapshotMatchers.size(0)); + } finally { + IOUtils.close(searcher); + } + + searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { + searcher = null; + IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); + assertThat(error.getMessage(), + containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); + }finally { + IOUtils.close(searcher); + } + } else { + fromSeqNo = randomLongBetween(0, refreshedSeqNo); + toSeqNo = randomLongBetween(refreshedSeqNo + 1, numOps * 2); + Engine.Searcher searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, false)) { + searcher = null; + assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, refreshedSeqNo)); + } finally { + IOUtils.close(searcher); + } + searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { + searcher = null; + IllegalStateException error = expectThrows(IllegalStateException.class, () -> drainAll(snapshot)); + assertThat(error.getMessage(), + containsString("Not all operations between from_seqno [" + fromSeqNo + "] and to_seqno [" + toSeqNo + "] found")); + }finally { + IOUtils.close(searcher); + } + toSeqNo = randomLongBetween(fromSeqNo, refreshedSeqNo); + searcher = engine.acquireSearcher("test", Engine.SearcherScope.INTERNAL); + try (Translog.Snapshot snapshot = new LuceneChangesSnapshot( + searcher, mapperService, between(1, LuceneChangesSnapshot.DEFAULT_BATCH_SIZE), fromSeqNo, toSeqNo, true)) { + searcher = null; + assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, toSeqNo)); + } finally { + IOUtils.close(searcher); + } + } + // Get snapshot via engine will auto refresh + fromSeqNo = randomLongBetween(0, numOps - 1); + toSeqNo = randomLongBetween(fromSeqNo, numOps - 1); + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, randomBoolean())) { + assertThat(snapshot, SnapshotMatchers.containsSeqNoRange(fromSeqNo, toSeqNo)); + } + } + + public void testDedupByPrimaryTerm() throws Exception { + Map latestOperations = new HashMap<>(); + List terms = Arrays.asList(between(1, 1000), between(1000, 2000)); + int totalOps = 0; + for (long term : terms) { + final List ops = generateSingleDocHistory(true, + randomFrom(VersionType.INTERNAL, VersionType.EXTERNAL, VersionType.EXTERNAL_GTE), term, 2, 20, "1"); + primaryTerm.set(Math.max(primaryTerm.get(), term)); + engine.rollTranslogGeneration(); + for (Engine.Operation op : ops) { + // We need to simulate a rollback here as only ops after local checkpoint get into the engine + if (op.seqNo() <= engine.getLocalCheckpointTracker().getCheckpoint()) { + engine.getLocalCheckpointTracker().resetCheckpoint(randomLongBetween(-1, op.seqNo() - 1)); + engine.rollTranslogGeneration(); + } + if (op instanceof Engine.Index) { + engine.index((Engine.Index) op); + } else if (op instanceof Engine.Delete) { + engine.delete((Engine.Delete) op); + } + latestOperations.put(op.seqNo(), op.primaryTerm()); + if (rarely()) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(); + } + totalOps++; + } + } + long maxSeqNo = engine.getLocalCheckpointTracker().getMaxSeqNo(); + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapperService, 0, maxSeqNo, false)) { + Translog.Operation op; + while ((op = snapshot.next()) != null) { + assertThat(op.toString(), op.primaryTerm(), equalTo(latestOperations.get(op.seqNo()))); + } + assertThat(snapshot.skippedOperations(), equalTo(totalOps - latestOperations.size())); + } + } + + public void testUpdateAndReadChangesConcurrently() throws Exception { + Follower[] followers = new Follower[between(1, 3)]; + CountDownLatch readyLatch = new CountDownLatch(followers.length + 1); + AtomicBoolean isDone = new AtomicBoolean(); + for (int i = 0; i < followers.length; i++) { + followers[i] = new Follower(engine, isDone, readyLatch); + followers[i].start(); + } + boolean onPrimary = randomBoolean(); + List operations = new ArrayList<>(); + int numOps = scaledRandomIntBetween(1, 1000); + for (int i = 0; i < numOps; i++) { + String id = Integer.toString(randomIntBetween(1, 10)); + ParsedDocument doc = createParsedDoc(id, randomAlphaOfLengthBetween(1, 5), randomBoolean()); + final Engine.Operation op; + if (onPrimary) { + if (randomBoolean()) { + op = new Engine.Index(newUid(doc), primaryTerm.get(), doc); + } else { + op = new Engine.Delete(doc.type(), doc.id(), newUid(doc.id()), primaryTerm.get()); + } + } else { + if (randomBoolean()) { + op = replicaIndexForDoc(doc, randomNonNegativeLong(), i, randomBoolean()); + } else { + op = replicaDeleteForDoc(doc.id(), randomNonNegativeLong(), i, randomNonNegativeLong()); + } + } + operations.add(op); + } + readyLatch.countDown(); + concurrentlyApplyOps(operations, engine); + assertThat(engine.getLocalCheckpointTracker().getCheckpoint(), equalTo(operations.size() - 1L)); + isDone.set(true); + for (Follower follower : followers) { + follower.join(); + } + } + + class Follower extends Thread { + private final Engine leader; + private final TranslogHandler translogHandler; + private final AtomicBoolean isDone; + private final CountDownLatch readLatch; + + Follower(Engine leader, AtomicBoolean isDone, CountDownLatch readLatch) { + this.leader = leader; + this.isDone = isDone; + this.readLatch = readLatch; + this.translogHandler = new TranslogHandler(xContentRegistry(), IndexSettingsModule.newIndexSettings(shardId.getIndexName(), + engine.engineConfig.getIndexSettings().getSettings())); + } + + void pullOperations(Engine follower) throws IOException { + long leaderCheckpoint = leader.getLocalCheckpoint(); + long followerCheckpoint = follower.getLocalCheckpoint(); + if (followerCheckpoint < leaderCheckpoint) { + long fromSeqNo = followerCheckpoint + 1; + long batchSize = randomLongBetween(0, 100); + long toSeqNo = Math.min(fromSeqNo + batchSize, leaderCheckpoint); + try (Translog.Snapshot snapshot = leader.newChangesSnapshot("test", mapperService, fromSeqNo, toSeqNo, true)) { + translogHandler.run(follower, snapshot); + } + } + } + + @Override + public void run() { + try (Store store = createStore(); + InternalEngine follower = createEngine(store, createTempDir())) { + readLatch.countDown(); + readLatch.await(); + while (isDone.get() == false || + follower.getLocalCheckpointTracker().getCheckpoint() < leader.getLocalCheckpoint()) { + pullOperations(follower); + } + assertConsistentHistoryBetweenTranslogAndLuceneIndex(follower, mapperService); + assertThat(getDocIds(follower, true), equalTo(getDocIds(leader, true))); + } catch (Exception ex) { + throw new AssertionError(ex); + } + } + } + + private List drainAll(Translog.Snapshot snapshot) throws IOException { + List operations = new ArrayList<>(); + Translog.Operation op; + while ((op = snapshot.next()) != null) { + final Translog.Operation newOp = op; + logger.error("Reading [{}]", op); + assert operations.stream().allMatch(o -> o.seqNo() < newOp.seqNo()) : "Operations [" + operations + "], op [" + op + "]"; + operations.add(newOp); + } + return operations; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java new file mode 100644 index 000000000000..c46b47b87d06 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/engine/RecoverySourcePruneMergePolicyTests.java @@ -0,0 +1,161 @@ +/* + * 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.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.CodecReader; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.LeafReader; +import org.apache.lucene.index.MergePolicy; +import org.apache.lucene.index.NumericDocValues; +import org.apache.lucene.index.SegmentCommitInfo; +import org.apache.lucene.index.SegmentInfos; +import org.apache.lucene.index.StandardDirectoryReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.InfoStream; +import org.apache.lucene.util.NullInfoStream; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +public class RecoverySourcePruneMergePolicyTests extends ESTestCase { + + public void testPruneAll() throws IOException { + try (Directory dir = newDirectory()) { + IndexWriterConfig iwc = newIndexWriterConfig(); + RecoverySourcePruneMergePolicy mp = new RecoverySourcePruneMergePolicy("extra_source", MatchNoDocsQuery::new, + newLogMergePolicy()); + iwc.setMergePolicy(mp); + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + for (int i = 0; i < 20; i++) { + if (i > 0 && randomBoolean()) { + writer.flush(); + } + Document doc = new Document(); + doc.add(new StoredField("source", "hello world")); + doc.add(new StoredField("extra_source", "hello world")); + doc.add(new NumericDocValuesField("extra_source", 1)); + writer.addDocument(doc); + } + writer.forceMerge(1); + writer.commit(); + try (DirectoryReader reader = DirectoryReader.open(writer)) { + for (int i = 0; i < reader.maxDoc(); i++) { + Document document = reader.document(i); + assertEquals(1, document.getFields().size()); + assertEquals("source", document.getFields().get(0).name()); + } + assertEquals(1, reader.leaves().size()); + LeafReader leafReader = reader.leaves().get(0).reader(); + NumericDocValues extra_source = leafReader.getNumericDocValues("extra_source"); + if (extra_source != null) { + assertEquals(DocIdSetIterator.NO_MORE_DOCS, extra_source.nextDoc()); + } + if (leafReader instanceof CodecReader && reader instanceof StandardDirectoryReader) { + CodecReader codecReader = (CodecReader) leafReader; + StandardDirectoryReader sdr = (StandardDirectoryReader) reader; + SegmentInfos segmentInfos = sdr.getSegmentInfos(); + MergePolicy.MergeSpecification forcedMerges = mp.findForcedDeletesMerges(segmentInfos, + new MergePolicy.MergeContext() { + @Override + public int numDeletesToMerge(SegmentCommitInfo info) { + return info.info.maxDoc() - 1; + } + + @Override + public int numDeletedDocs(SegmentCommitInfo info) { + return info.info.maxDoc() - 1; + } + + @Override + public InfoStream getInfoStream() { + return new NullInfoStream(); + } + + @Override + public Set getMergingSegments() { + return Collections.emptySet(); + } + }); + // don't wrap if there is nothing to do + assertSame(codecReader, forcedMerges.merges.get(0).wrapForMerge(codecReader)); + } + } + } + } + } + + + public void testPruneSome() throws IOException { + try (Directory dir = newDirectory()) { + IndexWriterConfig iwc = newIndexWriterConfig(); + iwc.setMergePolicy(new RecoverySourcePruneMergePolicy("extra_source", + () -> new TermQuery(new Term("even", "true")), iwc.getMergePolicy())); + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + for (int i = 0; i < 20; i++) { + if (i > 0 && randomBoolean()) { + writer.flush(); + } + Document doc = new Document(); + doc.add(new StringField("even", Boolean.toString(i % 2 == 0), Field.Store.YES)); + doc.add(new StoredField("source", "hello world")); + doc.add(new StoredField("extra_source", "hello world")); + doc.add(new NumericDocValuesField("extra_source", 1)); + writer.addDocument(doc); + } + writer.forceMerge(1); + writer.commit(); + try (DirectoryReader reader = DirectoryReader.open(writer)) { + assertEquals(1, reader.leaves().size()); + NumericDocValues extra_source = reader.leaves().get(0).reader().getNumericDocValues("extra_source"); + assertNotNull(extra_source); + for (int i = 0; i < reader.maxDoc(); i++) { + Document document = reader.document(i); + Set collect = document.getFields().stream().map(IndexableField::name).collect(Collectors.toSet()); + assertTrue(collect.contains("source")); + assertTrue(collect.contains("even")); + if (collect.size() == 3) { + assertTrue(collect.contains("extra_source")); + assertEquals("true", document.getField("even").stringValue()); + assertEquals(i, extra_source.nextDoc()); + } else { + assertEquals(2, document.getFields().size()); + } + } + assertEquals(DocIdSetIterator.NO_MORE_DOCS, extra_source.nextDoc()); + } + } + } + } +} diff --git a/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java new file mode 100644 index 000000000000..f35901003828 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/engine/SoftDeletesPolicyTests.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.engine; + +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static org.hamcrest.Matchers.equalTo; + +public class SoftDeletesPolicyTests extends ESTestCase { + /** + * Makes sure we won't advance the retained seq# if the retention lock is held + */ + public void testSoftDeletesRetentionLock() { + long retainedOps = between(0, 10000); + AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + long safeCommitCheckpoint = globalCheckpoint.get(); + SoftDeletesPolicy policy = new SoftDeletesPolicy(globalCheckpoint::get, between(1, 10000), retainedOps); + long minRetainedSeqNo = policy.getMinRetainedSeqNo(); + List locks = new ArrayList<>(); + int iters = scaledRandomIntBetween(10, 1000); + for (int i = 0; i < iters; i++) { + if (randomBoolean()) { + locks.add(policy.acquireRetentionLock()); + } + // Advances the global checkpoint and the local checkpoint of a safe commit + globalCheckpoint.addAndGet(between(0, 1000)); + safeCommitCheckpoint = randomLongBetween(safeCommitCheckpoint, globalCheckpoint.get()); + policy.setLocalCheckpointOfSafeCommit(safeCommitCheckpoint); + if (rarely()) { + retainedOps = between(0, 10000); + policy.setRetentionOperations(retainedOps); + } + // Release some locks + List releasingLocks = randomSubsetOf(locks); + locks.removeAll(releasingLocks); + releasingLocks.forEach(Releasable::close); + + // We only expose the seqno to the merge policy if the retention lock is not held. + policy.getRetentionQuery(); + if (locks.isEmpty()) { + long retainedSeqNo = Math.min(safeCommitCheckpoint, globalCheckpoint.get() - retainedOps) + 1; + minRetainedSeqNo = Math.max(minRetainedSeqNo, retainedSeqNo); + } + assertThat(policy.getMinRetainedSeqNo(), equalTo(minRetainedSeqNo)); + } + + locks.forEach(Releasable::close); + long retainedSeqNo = Math.min(safeCommitCheckpoint, globalCheckpoint.get() - retainedOps) + 1; + minRetainedSeqNo = Math.max(minRetainedSeqNo, retainedSeqNo); + assertThat(policy.getMinRetainedSeqNo(), equalTo(minRetainedSeqNo)); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java index 76ca6aa7ea8d..5a46b9a889fd 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.ParseContext.Document; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -311,15 +312,18 @@ DocumentMapper createDummyMapping(MapperService mapperService) throws Exception // creates an object mapper, which is about 100x harder than it should be.... ObjectMapper createObjectMapper(MapperService mapperService, String name) throws Exception { - ParseContext context = new ParseContext.InternalParseContext( - Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(), + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); + ParseContext context = new ParseContext.InternalParseContext(settings, mapperService.documentMapperParser(), mapperService.documentMapper("type"), null, null); String[] nameParts = name.split("\\."); for (int i = 0; i < nameParts.length - 1; ++i) { context.path().add(nameParts[i]); } Mapper.Builder builder = new ObjectMapper.Builder(nameParts[nameParts.length - 1]).enabled(true); - Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings(), context.path()); + Mapper.BuilderContext builderContext = new Mapper.BuilderContext(context.indexSettings().getSettings(), context.path()); return (ObjectMapper)builder.build(builderContext); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java index cb2ed785699c..b11e4876f9ea 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicMappingTests.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.BooleanFieldMapper.BooleanFieldType; import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType; import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType; @@ -215,7 +216,10 @@ private String serialize(ToXContent mapper) throws Exception { } private Mapper parse(DocumentMapper mapper, DocumentMapperParser parser, XContentBuilder builder) throws Exception { - Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(); + IndexMetaData build = IndexMetaData.builder("") + .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)) + .numberOfShards(1).numberOfReplicas(0).build(); + IndexSettings settings = new IndexSettings(build, Settings.EMPTY); SourceToParse source = SourceToParse.source("test", mapper.type(), "some_id", BytesReference.bytes(builder), builder.contentType()); try (XContentParser xContentParser = createParser(JsonXContent.jsonXContent, source.source())) { ParseContext.InternalParseContext ctx = new ParseContext.InternalParseContext(settings, parser, mapper, source, xContentParser); 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 1d1e423afc1b..fba71dd1e529 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/IndexLevelReplicationTests.java @@ -18,6 +18,7 @@ */ package org.elasticsearch.index.replication; +import org.apache.lucene.document.Field; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.Term; @@ -41,6 +42,7 @@ import org.elasticsearch.index.engine.InternalEngineTests; import org.elasticsearch.index.engine.SegmentsStats; import org.elasticsearch.index.engine.VersionConflictEngineException; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; @@ -140,7 +142,9 @@ public void cleanFiles(int totalTranslogOps, Store.MetadataSnapshot sourceMetaDa } public void testInheritMaxValidAutoIDTimestampOnRecovery() throws Exception { - try (ReplicationGroup shards = createGroup(0)) { + //TODO: Enables this test with soft-deletes once we have timestamp + Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + try (ReplicationGroup shards = createGroup(0, settings)) { shards.startAll(); final IndexRequest indexRequest = new IndexRequest(index.getName(), "type").source("{}", XContentType.JSON); indexRequest.onRetry(); // force an update of the timestamp @@ -346,7 +350,13 @@ public void testDocumentFailureReplication() throws Exception { final AtomicBoolean throwAfterIndexedOneDoc = new AtomicBoolean(); // need one document to trigger delete in IW. @Override public long addDocument(Iterable doc) throws IOException { - if (throwAfterIndexedOneDoc.getAndSet(true)) { + boolean isTombstone = false; + for (IndexableField field : doc) { + if (SeqNoFieldMapper.TOMBSTONE_NAME.equals(field.name())) { + isTombstone = true; + } + } + if (isTombstone == false && throwAfterIndexedOneDoc.getAndSet(true)) { throw indexException; } else { return super.addDocument(doc); @@ -356,6 +366,10 @@ public long addDocument(Iterable doc) throws IOExcepti public long deleteDocuments(Term... terms) throws IOException { throw deleteException; } + @Override + public long softUpdateDocument(Term term, Iterable doc, Field...fields) throws IOException { + throw deleteException; // a delete uses softUpdateDocument API if soft-deletes enabled + } }, null, null, config); try (ReplicationGroup shards = new ReplicationGroup(buildIndexMetaData(0)) { @Override @@ -390,6 +404,9 @@ public long deleteDocuments(Term... terms) throws IOException { try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } + try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } } // unlike previous failures, these two failures replicated directly from the replication channel. indexResp = shards.index(new IndexRequest(index.getName(), "type", "any").source("{}", XContentType.JSON)); @@ -404,6 +421,9 @@ public long deleteDocuments(Term... terms) throws IOException { try (Translog.Snapshot snapshot = getTranslog(shard).newSnapshot()) { assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); } + try (Translog.Snapshot snapshot = shard.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.containsOperationsInAnyOrder(expectedTranslogOps)); + } } shards.assertAllEqual(1); } @@ -501,8 +521,9 @@ public void testSeqNoCollision() throws Exception { recoverReplica(replica3, replica2, true); try (Translog.Snapshot snapshot = getTranslog(replica3).newSnapshot()) { assertThat(snapshot.totalOperations(), equalTo(initDocs + 1)); - assertThat(snapshot.next(), equalTo(op2)); - assertThat("Remaining of snapshot should contain init operations", snapshot, containsOperationsInAnyOrder(initOperations)); + 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. 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 2d198c32ba74..28122665e9bb 100644 --- a/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java +++ b/server/src/test/java/org/elasticsearch/index/replication/RecoveryDuringReplicationTests.java @@ -98,7 +98,8 @@ public void testIndexingDuringFileRecovery() throws Exception { } public void testRecoveryOfDisconnectedReplica() throws Exception { - try (ReplicationGroup shards = createGroup(1)) { + Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + try (ReplicationGroup shards = createGroup(1, settings)) { shards.startAll(); int docs = shards.indexDocs(randomInt(50)); shards.flush(); @@ -266,6 +267,7 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { builder.settings(Settings.builder().put(newPrimary.indexSettings().getSettings()) .put(IndexSettings.INDEX_TRANSLOG_RETENTION_AGE_SETTING.getKey(), "-1") .put(IndexSettings.INDEX_TRANSLOG_RETENTION_SIZE_SETTING.getKey(), "-1") + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) ); newPrimary.indexSettings().updateIndexMetaData(builder.build()); newPrimary.onSettingsChanged(); @@ -275,7 +277,12 @@ public void testRecoveryAfterPrimaryPromotion() throws Exception { shards.syncGlobalCheckpoint(); assertThat(newPrimary.getLastSyncedGlobalCheckpoint(), equalTo(newPrimary.seqNoStats().getMaxSeqNo())); }); - newPrimary.flush(new FlushRequest()); + newPrimary.flush(new FlushRequest().force(true)); + if (replica.indexSettings().isSoftDeleteEnabled()) { + // We need an extra flush to advance the min_retained_seqno on the new primary so ops-based won't happen. + // The min_retained_seqno only advances when a merge asks for the retention query. + newPrimary.flush(new FlushRequest().force(true)); + } uncommittedOpsOnPrimary = shards.indexDocs(randomIntBetween(0, 10)); totalDocs += uncommittedOpsOnPrimary; } 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 2228e1b017fd..50f95bf4d473 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -22,6 +22,7 @@ import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.index.IndexableField; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TermQuery; @@ -30,6 +31,7 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.FilterDirectory; import org.apache.lucene.store.IOContext; +import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.Constants; import org.elasticsearch.Assertions; import org.elasticsearch.Version; @@ -89,8 +91,13 @@ import org.elasticsearch.index.fielddata.IndexFieldDataService; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.ParseContext; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; @@ -160,6 +167,7 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.lessThan; @@ -237,7 +245,8 @@ public void testFailShard() throws Exception { assertNotNull(shardPath); // fail shard shard.failShard("test shard fail", new CorruptIndexException("", "")); - closeShards(shard); + shard.close("do not assert history", false); + shard.store().close(); // check state file still exists ShardStateMetaData shardStateMetaData = load(logger, shardPath.getShardStatePath()); assertEquals(shardStateMetaData, getShardStateMetadata(shard)); @@ -2394,7 +2403,8 @@ public void testRecoverFromLocalShard() throws IOException { public void testDocStats() throws IOException, InterruptedException { IndexShard indexShard = null; try { - indexShard = newStartedShard(); + indexShard = newStartedShard( + Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0).build()); final long numDocs = randomIntBetween(2, 32); // at least two documents so we have docs to delete final long numDocsToDelete = randomLongBetween(1, numDocs); for (int i = 0; i < numDocs; i++) { @@ -2424,7 +2434,16 @@ public void testDocStats() throws IOException, InterruptedException { deleteDoc(indexShard, "_doc", id); indexDoc(indexShard, "_doc", id); } - + // Need to update and sync the global checkpoint as the soft-deletes retention MergePolicy depends on it. + if (indexShard.indexSettings.isSoftDeleteEnabled()) { + if (indexShard.routingEntry().primary()) { + indexShard.updateGlobalCheckpointForShard(indexShard.routingEntry().allocationId().getId(), + indexShard.getLocalCheckpoint()); + } else { + indexShard.updateGlobalCheckpointOnReplica(indexShard.getLocalCheckpoint(), "test"); + } + indexShard.sync(); + } // flush the buffered deletes final FlushRequest flushRequest = new FlushRequest(); flushRequest.force(false); @@ -2962,6 +2981,7 @@ public void testSegmentMemoryTrackedInBreaker() throws Exception { assertThat(breaker.getUsed(), greaterThan(preRefreshBytes)); indexDoc(primary, "_doc", "4", "{\"foo\": \"potato\"}"); + indexDoc(primary, "_doc", "5", "{\"foo\": \"potato\"}"); // Forces a refresh with the INTERNAL scope ((InternalEngine) primary.getEngine()).writeIndexingBuffer(); @@ -2973,6 +2993,13 @@ public void testSegmentMemoryTrackedInBreaker() throws Exception { // Deleting a doc causes its memory to be freed from the breaker deleteDoc(primary, "_doc", "0"); + // Here we are testing that a fully deleted segment should be dropped and its memory usage is freed. + // In order to instruct the merge policy not to keep a fully deleted segment, + // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. + if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { + primary.sync(); + flushShard(primary); + } primary.refresh("force refresh"); ss = primary.segmentStats(randomBoolean()); @@ -3064,6 +3091,7 @@ public void testSegmentMemoryTrackedWithRandomSearchers() throws Exception { // Close remaining searchers IOUtils.close(searchers); + primary.refresh("test"); SegmentsStats ss = primary.segmentStats(randomBoolean()); CircuitBreaker breaker = primary.circuitBreakerService.getBreaker(CircuitBreaker.ACCOUNTING); @@ -3181,4 +3209,28 @@ public void testOnCloseStats() throws IOException { } + public void testSupplyTombstoneDoc() throws Exception { + IndexShard shard = newStartedShard(); + String id = randomRealisticUnicodeOfLengthBetween(1, 10); + ParsedDocument deleteTombstone = shard.getEngine().config().getTombstoneDocSupplier().newDeleteTombstoneDoc("doc", id); + assertThat(deleteTombstone.docs(), hasSize(1)); + ParseContext.Document deleteDoc = deleteTombstone.docs().get(0); + assertThat(deleteDoc.getFields().stream().map(IndexableField::name).collect(Collectors.toList()), + containsInAnyOrder(IdFieldMapper.NAME, VersionFieldMapper.NAME, + SeqNoFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME, SeqNoFieldMapper.TOMBSTONE_NAME)); + assertThat(deleteDoc.getField(IdFieldMapper.NAME).binaryValue(), equalTo(Uid.encodeId(id))); + assertThat(deleteDoc.getField(SeqNoFieldMapper.TOMBSTONE_NAME).numericValue().longValue(), equalTo(1L)); + + final String reason = randomUnicodeOfLength(200); + ParsedDocument noopTombstone = shard.getEngine().config().getTombstoneDocSupplier().newNoopTombstoneDoc(reason); + assertThat(noopTombstone.docs(), hasSize(1)); + ParseContext.Document noopDoc = noopTombstone.docs().get(0); + assertThat(noopDoc.getFields().stream().map(IndexableField::name).collect(Collectors.toList()), + containsInAnyOrder(VersionFieldMapper.NAME, SourceFieldMapper.NAME, SeqNoFieldMapper.TOMBSTONE_NAME, + SeqNoFieldMapper.NAME, SeqNoFieldMapper.NAME, SeqNoFieldMapper.PRIMARY_TERM_NAME)); + assertThat(noopDoc.getField(SeqNoFieldMapper.TOMBSTONE_NAME).numericValue().longValue(), equalTo(1L)); + assertThat(noopDoc.getField(SourceFieldMapper.NAME).binaryValue(), equalTo(new BytesRef(reason))); + + closeShards(shard); + } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java index ae2cc84e4870..29b16ca28f4d 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java @@ -106,17 +106,22 @@ public void testSyncerSendsOffCorrectDocuments() throws Exception { .isPresent(), is(false)); } - - assertEquals(globalCheckPoint == numDocs - 1 ? 0 : numDocs, resyncTask.getTotalOperations()); if (syncNeeded && globalCheckPoint < numDocs - 1) { - long skippedOps = globalCheckPoint + 1; // everything up to global checkpoint included - assertEquals(skippedOps, resyncTask.getSkippedOperations()); - assertEquals(numDocs - skippedOps, resyncTask.getResyncedOperations()); + if (shard.indexSettings.isSoftDeleteEnabled()) { + assertThat(resyncTask.getSkippedOperations(), equalTo(0)); + assertThat(resyncTask.getResyncedOperations(), equalTo(resyncTask.getTotalOperations())); + assertThat(resyncTask.getTotalOperations(), equalTo(Math.toIntExact(numDocs - 1 - globalCheckPoint))); + } else { + int skippedOps = Math.toIntExact(globalCheckPoint + 1); // everything up to global checkpoint included + assertThat(resyncTask.getSkippedOperations(), equalTo(skippedOps)); + assertThat(resyncTask.getResyncedOperations(), equalTo(numDocs - skippedOps)); + assertThat(resyncTask.getTotalOperations(), equalTo(globalCheckPoint == numDocs - 1 ? 0 : numDocs)); + } } else { - assertEquals(0, resyncTask.getSkippedOperations()); - assertEquals(0, resyncTask.getResyncedOperations()); + assertThat(resyncTask.getSkippedOperations(), equalTo(0)); + assertThat(resyncTask.getResyncedOperations(), equalTo(0)); + assertThat(resyncTask.getTotalOperations(), equalTo(0)); } - closeShards(shard); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java index 774b272121a5..b93f170174c3 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/RefreshListenersTests.java @@ -42,6 +42,7 @@ import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.engine.EngineConfig; +import org.elasticsearch.index.engine.EngineTestCase; import org.elasticsearch.index.engine.InternalEngine; import org.elasticsearch.index.fieldvisitor.SingleFieldsVisitor; import org.elasticsearch.index.mapper.IdFieldMapper; @@ -130,7 +131,8 @@ public void onFailedEngine(String reason, @Nullable Exception e) { indexSettings, null, store, newMergePolicy(), iwc.getAnalyzer(), iwc.getSimilarity(), new CodecService(null, logger), eventListener, IndexSearcher.getDefaultQueryCache(), IndexSearcher.getDefaultQueryCachingPolicy(), translogConfig, TimeValue.timeValueMinutes(5), Collections.singletonList(listeners), Collections.emptyList(), null, - (e, s) -> 0, new NoneCircuitBreakerService(), () -> SequenceNumbers.NO_OPS_PERFORMED, () -> primaryTerm); + (e, s) -> 0, new NoneCircuitBreakerService(), () -> SequenceNumbers.NO_OPS_PERFORMED, () -> primaryTerm, + EngineTestCase.tombstoneDocSupplier()); engine = new InternalEngine(config); engine.recoverFromTranslog(Long.MAX_VALUE); listeners.setCurrentRefreshLocationSupplier(engine::getTranslogLastWriteLocation); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java index 89a8813e3e07..81afab4bb8f7 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java @@ -67,6 +67,7 @@ import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportRequestOptions; import org.elasticsearch.transport.TransportService; +import org.junit.After; import java.io.IOException; import java.util.ArrayList; @@ -110,6 +111,11 @@ protected Collection> nodePlugins() { RecoverySettingsChunkSizePlugin.class); } + @After + public void assertConsistentHistoryInLuceneIndex() throws Exception { + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); + } + private void assertRecoveryStateWithoutStage(RecoveryState state, int shardId, RecoverySource recoverySource, boolean primary, String sourceNode, String targetNode) { assertThat(state.getShardId().getId(), equalTo(shardId)); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java index 4b1419375e6e..b6f5a7b64516 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetServiceTests.java @@ -25,6 +25,7 @@ import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.NoMergePolicy; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.IndexShardTestCase; @@ -91,6 +92,7 @@ public void testGetStartingSeqNo() throws Exception { replica.close("test", false); final List commits = DirectoryReader.listCommits(replica.store().directory()); IndexWriterConfig iwc = new IndexWriterConfig(null) + .setSoftDeletesField(Lucene.SOFT_DELETES_FIELD) .setCommitOnClose(false) .setMergePolicy(NoMergePolicy.INSTANCE) .setOpenMode(IndexWriterConfig.OpenMode.APPEND); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java index f0644b029c3d..0351111c305c 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoverySourceHandlerTests.java @@ -411,12 +411,6 @@ public void testThrowExceptionOnPrimaryRelocatedBeforePhase1Started() throws IOE recoverySettings.getChunkSize().bytesAsInt(), Settings.EMPTY) { - - @Override - boolean isTranslogReadyForSequenceNumberBasedRecovery() throws IOException { - return randomBoolean(); - } - @Override public void phase1(final IndexCommit snapshot, final Supplier translogOps) { phase1Called.set(true); diff --git a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java index 5547a629ab2a..45535e19672c 100644 --- a/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java +++ b/server/src/test/java/org/elasticsearch/indices/recovery/RecoveryTests.java @@ -34,6 +34,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.mapper.SourceToParse; @@ -63,13 +64,13 @@ public void testTranslogHistoryTransferred() throws Exception { int docs = shards.indexDocs(10); getTranslog(shards.getPrimary()).rollGeneration(); shards.flush(); - if (randomBoolean()) { - docs += shards.indexDocs(10); - } + int moreDocs = shards.indexDocs(randomInt(10)); shards.addReplica(); shards.startAll(); final IndexShard replica = shards.getReplicas().get(0); - assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(docs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? moreDocs : docs + moreDocs)); + shards.assertAllEqual(docs + moreDocs); } } @@ -101,12 +102,12 @@ public void testRetentionPolicyChangeDuringRecovery() throws Exception { // rolling/flushing is async assertBusy(() -> { assertThat(replica.getLastSyncedGlobalCheckpoint(), equalTo(19L)); - assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(0)); + assertThat(getTranslog(replica).totalOperations(), equalTo(0)); }); } } - public void testRecoveryWithOutOfOrderDelete() throws Exception { + public void testRecoveryWithOutOfOrderDeleteWithTranslog() throws Exception { /* * The flow of this test: * - delete #1 @@ -118,7 +119,8 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { * - index #5 * - If flush and the translog retention disabled, delete #1 will be removed while index #0 is still retained and replayed. */ - try (ReplicationGroup shards = createGroup(1)) { + Settings settings = Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false).build(); + try (ReplicationGroup shards = createGroup(1, settings)) { shards.startAll(); // create out of order delete and index op on replica final IndexShard orgReplica = shards.getReplicas().get(0); @@ -170,7 +172,63 @@ public void testRecoveryWithOutOfOrderDelete() throws Exception { shards.recoverReplica(newReplica); shards.assertAllEqual(3); - assertThat(newReplica.estimateTranslogOperationsFromMinSeq(0), equalTo(translogOps)); + assertThat(getTranslog(newReplica).totalOperations(), equalTo(translogOps)); + } + } + + public void testRecoveryWithOutOfOrderDeleteWithSoftDeletes() throws Exception { + Settings settings = Settings.builder() + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 10) + // If soft-deletes is enabled, delete#1 will be reclaimed because its segment (segment_1) is fully deleted + // index#0 will be retained if merge is disabled; otherwise it will be reclaimed because gcp=3 and retained_ops=0 + .put(MergePolicyConfig.INDEX_MERGE_ENABLED, false).build(); + try (ReplicationGroup shards = createGroup(1, settings)) { + shards.startAll(); + // create out of order delete and index op on replica + final IndexShard orgReplica = shards.getReplicas().get(0); + final String indexName = orgReplica.shardId().getIndexName(); + + // delete #1 + orgReplica.applyDeleteOperationOnReplica(1, 2, "type", "id"); + orgReplica.flush(new FlushRequest().force(true)); // isolate delete#1 in its own translog generation and lucene segment + // index #0 + orgReplica.applyIndexOperationOnReplica(0, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id", new BytesArray("{}"), XContentType.JSON)); + // index #3 + orgReplica.applyIndexOperationOnReplica(3, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-3", new BytesArray("{}"), XContentType.JSON)); + // Flushing a new commit with local checkpoint=1 allows to delete the translog gen #1. + orgReplica.flush(new FlushRequest().force(true).waitIfOngoing(true)); + // index #2 + orgReplica.applyIndexOperationOnReplica(2, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-2", new BytesArray("{}"), XContentType.JSON)); + orgReplica.updateGlobalCheckpointOnReplica(3L, "test"); + // index #5 -> force NoOp #4. + orgReplica.applyIndexOperationOnReplica(5, 1, IndexRequest.UNSET_AUTO_GENERATED_TIMESTAMP, false, + SourceToParse.source(indexName, "type", "id-5", new BytesArray("{}"), XContentType.JSON)); + + if (randomBoolean()) { + if (randomBoolean()) { + logger.info("--> flushing shard (translog/soft-deletes will be trimmed)"); + IndexMetaData.Builder builder = IndexMetaData.builder(orgReplica.indexSettings().getIndexMetaData()); + builder.settings(Settings.builder().put(orgReplica.indexSettings().getSettings()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0)); + orgReplica.indexSettings().updateIndexMetaData(builder.build()); + orgReplica.onSettingsChanged(); + } + flushShard(orgReplica); + } + + final IndexShard orgPrimary = shards.getPrimary(); + shards.promoteReplicaToPrimary(orgReplica).get(); // wait for primary/replica sync to make sure seq# gap is closed. + + IndexShard newReplica = shards.addReplicaWithExistingPath(orgPrimary.shardPath(), orgPrimary.routingEntry().currentNodeId()); + shards.recoverReplica(newReplica); + shards.assertAllEqual(3); + try (Translog.Snapshot snapshot = newReplica.getHistoryOperations("test", 0)) { + assertThat(snapshot, SnapshotMatchers.size(6)); + } } } @@ -222,7 +280,8 @@ public void testDifferentHistoryUUIDDisablesOPsRecovery() throws Exception { shards.recoverReplica(newReplica); // file based recovery should be made assertThat(newReplica.recoveryState().getIndex().fileDetails(), not(empty())); - assertThat(newReplica.estimateTranslogOperationsFromMinSeq(0), equalTo(numDocs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(newReplica).totalOperations(), equalTo(softDeletesEnabled ? nonFlushedDocs : numDocs)); // history uuid was restored assertThat(newReplica.getHistoryUUID(), equalTo(historyUUID)); @@ -326,7 +385,8 @@ public void testShouldFlushAfterPeerRecovery() throws Exception { shards.recoverReplica(replica); // Make sure the flushing will eventually be completed (eg. `shouldPeriodicallyFlush` is false) assertBusy(() -> assertThat(getEngine(replica).shouldPeriodicallyFlush(), equalTo(false))); - assertThat(replica.estimateTranslogOperationsFromMinSeq(0), equalTo(numDocs)); + boolean softDeletesEnabled = replica.indexSettings().isSoftDeleteEnabled(); + assertThat(getTranslog(replica).totalOperations(), equalTo(softDeletesEnabled ? 0 : numDocs)); shards.assertAllEqual(numDocs); } } diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index fa591411bba1..ce162b9600cf 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -43,6 +43,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexModule; +import org.elasticsearch.index.IndexService; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.MergeSchedulerConfig; @@ -50,6 +51,7 @@ import org.elasticsearch.index.cache.query.QueryCacheStats; import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.indices.IndicesQueryCache; import org.elasticsearch.indices.IndicesRequestCache; @@ -69,6 +71,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -115,6 +118,7 @@ public Settings indexSettings() { return Settings.builder().put(super.indexSettings()) .put(IndexModule.INDEX_QUERY_CACHE_EVERYTHING_SETTING.getKey(), true) .put(IndexModule.INDEX_QUERY_CACHE_ENABLED_SETTING.getKey(), true) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), 0) .build(); } @@ -1006,10 +1010,15 @@ private void assertCumulativeQueryCacheStats(IndicesStatsResponse response) { @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32506") public void testFilterCacheStats() throws Exception { - assertAcked(prepareCreate("index").setSettings(Settings.builder().put(indexSettings()).put("number_of_replicas", 0).build()).get()); - indexRandom(true, + Settings settings = Settings.builder().put(indexSettings()).put("number_of_replicas", 0).build(); + assertAcked(prepareCreate("index").setSettings(settings).get()); + indexRandom(false, true, client().prepareIndex("index", "type", "1").setSource("foo", "bar"), client().prepareIndex("index", "type", "2").setSource("foo", "baz")); + if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { + persistGlobalCheckpoint("index"); // Need to persist the global checkpoint for the soft-deletes retention MP. + } + refresh(); ensureGreen(); IndicesStatsResponse response = client().admin().indices().prepareStats("index").setQueryCache(true).get(); @@ -1040,6 +1049,13 @@ public void testFilterCacheStats() throws Exception { assertEquals(DocWriteResponse.Result.DELETED, client().prepareDelete("index", "type", "1").get().getResult()); assertEquals(DocWriteResponse.Result.DELETED, client().prepareDelete("index", "type", "2").get().getResult()); + // Here we are testing that a fully deleted segment should be dropped and its cached is evicted. + // In order to instruct the merge policy not to keep a fully deleted segment, + // we need to flush and make that commit safe so that the SoftDeletesPolicy can drop everything. + if (IndexSettings.INDEX_SOFT_DELETES_SETTING.get(settings)) { + persistGlobalCheckpoint("index"); + flush("index"); + } refresh(); response = client().admin().indices().prepareStats("index").setQueryCache(true).get(); assertCumulativeQueryCacheStats(response); @@ -1173,4 +1189,21 @@ public void testConcurrentIndexingAndStatsRequests() throws BrokenBarrierExcepti assertThat(executionFailures.get(), emptyCollectionOf(Exception.class)); } + + /** + * Persist the global checkpoint on all shards of the given index into disk. + * This makes sure that the persisted global checkpoint on those shards will equal to the in-memory value. + */ + private void persistGlobalCheckpoint(String index) throws Exception { + final Set nodes = internalCluster().nodesInclude(index); + for (String node : nodes) { + final IndicesService indexServices = internalCluster().getInstance(IndicesService.class, node); + for (IndexService indexService : indexServices) { + for (IndexShard indexShard : indexService) { + indexShard.sync(); + assertThat(indexShard.getLastSyncedGlobalCheckpoint(), equalTo(indexShard.getGlobalCheckpoint())); + } + } + } + } } diff --git a/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java b/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java index 23c56688e00b..c25cad61e074 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java +++ b/server/src/test/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java @@ -27,6 +27,7 @@ import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.snapshots.mockstore.MockRepository; import org.elasticsearch.test.ESIntegTestCase; +import org.junit.After; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -58,6 +59,11 @@ protected Collection> nodePlugins() { return Arrays.asList(MockRepository.Plugin.class); } + @After + public void assertConsistentHistoryInLuceneIndex() throws Exception { + internalCluster().assertConsistentHistoryBetweenTranslogAndLuceneIndex(); + } + public static long getFailureCount(String repository) { long failureCount = 0; for (RepositoriesService repositoriesService : diff --git a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java index 1230d594b98a..632a1ecbee1a 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java @@ -122,6 +122,7 @@ import static org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider.SETTING_ALLOCATION_MAX_RETRY; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.IndexSettings.INDEX_REFRESH_INTERVAL_SETTING; +import static org.elasticsearch.index.IndexSettings.INDEX_SOFT_DELETES_SETTING; import static org.elasticsearch.index.query.QueryBuilders.matchQuery; import static org.elasticsearch.index.shard.IndexShardTests.getEngineFromShard; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -2048,7 +2049,9 @@ public void testSnapshotMoreThanOnce() throws ExecutionException, InterruptedExc .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); // only one shard - assertAcked(prepareCreate("test").setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1))); + final Settings indexSettings = Settings.builder() + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1).build(); + assertAcked(prepareCreate("test").setSettings(indexSettings)); ensureGreen(); logger.info("--> indexing"); @@ -2094,7 +2097,13 @@ public void testSnapshotMoreThanOnce() throws ExecutionException, InterruptedExc SnapshotStatus snapshotStatus = client.admin().cluster().prepareSnapshotStatus("test-repo").setSnapshots("test-2").get().getSnapshots().get(0); List shards = snapshotStatus.getShards(); for (SnapshotIndexShardStatus status : shards) { - assertThat(status.getStats().getProcessedFileCount(), equalTo(2)); // we flush before the snapshot such that we have to process the segments_N files plus the .del file + // we flush before the snapshot such that we have to process the segments_N files plus the .del file + if (INDEX_SOFT_DELETES_SETTING.get(indexSettings)) { + // soft-delete generates DV files. + assertThat(status.getStats().getProcessedFileCount(), greaterThan(2)); + } else { + assertThat(status.getStats().getProcessedFileCount(), equalTo(2)); + } } } } diff --git a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java index caf4f725fa45..588118db4aef 100644 --- a/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java +++ b/server/src/test/java/org/elasticsearch/versioning/SimpleVersioningIT.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.VersionType; @@ -785,4 +786,26 @@ public void testGCDeletesZero() throws Exception { .getVersion(), equalTo(-1L)); } + + public void testSpecialVersioning() { + internalCluster().ensureAtLeastNumDataNodes(2); + createIndex("test", Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0).build()); + IndexResponse doc1 = client().prepareIndex("test", "type", "1").setSource("field", "value1") + .setVersion(0).setVersionType(VersionType.EXTERNAL).execute().actionGet(); + assertThat(doc1.getVersion(), equalTo(0L)); + IndexResponse doc2 = client().prepareIndex("test", "type", "1").setSource("field", "value2") + .setVersion(Versions.MATCH_ANY).setVersionType(VersionType.INTERNAL).execute().actionGet(); + assertThat(doc2.getVersion(), equalTo(1L)); + client().prepareDelete("test", "type", "1").get(); //v2 + IndexResponse doc3 = client().prepareIndex("test", "type", "1").setSource("field", "value3") + .setVersion(Versions.MATCH_DELETED).setVersionType(VersionType.INTERNAL).execute().actionGet(); + assertThat(doc3.getVersion(), equalTo(3L)); + IndexResponse doc4 = client().prepareIndex("test", "type", "1").setSource("field", "value4") + .setVersion(4L).setVersionType(VersionType.EXTERNAL_GTE).execute().actionGet(); + assertThat(doc4.getVersion(), equalTo(4L)); + // Make sure that these versions are replicated correctly + client().admin().indices().prepareUpdateSettings("test") + .setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)).get(); + ensureGreen("test"); + } } 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 b5ba5f18b395..b558cd1ba900 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 @@ -19,14 +19,18 @@ package org.elasticsearch.index.engine; +import org.apache.logging.log4j.Logger; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.codecs.Codec; +import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.NumericDocValuesField; import org.apache.lucene.document.StoredField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; +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.Term; @@ -34,32 +38,41 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.ReferenceManager; import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TotalHitCountCollector; import org.apache.lucene.store.Directory; +import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.cluster.ClusterModule; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.routing.AllocationId; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.lucene.uid.Versions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.MapperTestUtils; +import org.elasticsearch.index.VersionType; import org.elasticsearch.index.codec.CodecService; import org.elasticsearch.index.mapper.IdFieldMapper; +import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.Mapping; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.Uid; +import org.elasticsearch.index.mapper.VersionFieldMapper; import org.elasticsearch.index.seqno.LocalCheckpointTracker; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.SequenceNumbers; @@ -80,17 +93,30 @@ import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.LongSupplier; import java.util.function.ToLongBiFunction; +import java.util.stream.Collectors; import static java.util.Collections.emptyList; +import static java.util.Collections.shuffle; +import static org.elasticsearch.index.engine.Engine.Operation.Origin.PRIMARY; +import static org.elasticsearch.index.engine.Engine.Operation.Origin.REPLICA; import static org.elasticsearch.index.translog.TranslogDeletionPolicies.createTranslogDeletionPolicy; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; public abstract class EngineTestCase extends ESTestCase { @@ -128,6 +154,20 @@ protected static void assertVisibleCount(Engine engine, int numDocs, boolean ref } } + protected Settings indexSettings() { + // TODO randomize more settings + return Settings.builder() + .put(IndexSettings.INDEX_GC_DELETES_SETTING.getKey(), "1h") // make sure this doesn't kick in on us + .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), codecName) + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.getKey(), + between(10, 10 * IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.get(Settings.EMPTY))) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + .build(); + } + @Override @Before public void setUp() throws Exception { @@ -142,13 +182,7 @@ public void setUp() throws Exception { } else { codecName = "default"; } - defaultSettings = IndexSettingsModule.newIndexSettings("test", Settings.builder() - .put(IndexSettings.INDEX_GC_DELETES_SETTING.getKey(), "1h") // make sure this doesn't kick in on us - .put(EngineConfig.INDEX_CODEC_SETTING.getKey(), codecName) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.getKey(), - between(10, 10 * IndexSettings.MAX_REFRESH_LISTENERS_PER_SHARD.get(Settings.EMPTY))) - .build()); // TODO randomize more settings + defaultSettings = IndexSettingsModule.newIndexSettings("test", indexSettings()); threadPool = new TestThreadPool(getClass().getName()); store = createStore(); storeReplica = createStore(); @@ -180,7 +214,7 @@ public EngineConfig copy(EngineConfig config, LongSupplier globalCheckpointSuppl new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), config.getTranslogConfig(), config.getFlushMergesAfter(), config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), - config.getCircuitBreakerService(), globalCheckpointSupplier, config.getPrimaryTermSupplier()); + config.getCircuitBreakerService(), globalCheckpointSupplier, config.getPrimaryTermSupplier(), tombstoneDocSupplier()); } public EngineConfig copy(EngineConfig config, Analyzer analyzer) { @@ -189,7 +223,18 @@ public EngineConfig copy(EngineConfig config, Analyzer analyzer) { new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), config.getTranslogConfig(), config.getFlushMergesAfter(), config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), - config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier()); + config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier(), + config.getTombstoneDocSupplier()); + } + + public EngineConfig copy(EngineConfig config, MergePolicy mergePolicy) { + return new EngineConfig(config.getShardId(), config.getAllocationId(), config.getThreadPool(), config.getIndexSettings(), + config.getWarmer(), config.getStore(), mergePolicy, config.getAnalyzer(), config.getSimilarity(), + new CodecService(null, logger), config.getEventListener(), config.getQueryCache(), config.getQueryCachingPolicy(), + config.getTranslogConfig(), config.getFlushMergesAfter(), + config.getExternalRefreshListener(), Collections.emptyList(), config.getIndexSort(), config.getTranslogRecoveryRunner(), + config.getCircuitBreakerService(), config.getGlobalCheckpointSupplier(), config.getPrimaryTermSupplier(), + config.getTombstoneDocSupplier()); } @Override @@ -198,9 +243,11 @@ public void tearDown() throws Exception { super.tearDown(); if (engine != null && engine.isClosed.get() == false) { engine.getTranslog().getDeletionPolicy().assertNoOpenTranslogRefs(); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, createMapperService("test")); } if (replicaEngine != null && replicaEngine.isClosed.get() == false) { replicaEngine.getTranslog().getDeletionPolicy().assertNoOpenTranslogRefs(); + assertConsistentHistoryBetweenTranslogAndLuceneIndex(replicaEngine, createMapperService("test")); } IOUtils.close( replicaEngine, storeReplica, @@ -228,8 +275,18 @@ public static ParsedDocument createParsedDoc(String id, String routing) { return testParsedDocument(id, routing, testDocumentWithTextField(), new BytesArray("{ \"value\" : \"test\" }"), null); } + public static ParsedDocument createParsedDoc(String id, String routing, boolean recoverySource) { + return testParsedDocument(id, routing, testDocumentWithTextField(), new BytesArray("{ \"value\" : \"test\" }"), null, + recoverySource); + } + protected static ParsedDocument testParsedDocument( String id, String routing, ParseContext.Document document, BytesReference source, Mapping mappingUpdate) { + return testParsedDocument(id, routing, document, source, mappingUpdate, false); + } + protected static ParsedDocument testParsedDocument( + String id, String routing, ParseContext.Document document, BytesReference source, Mapping mappingUpdate, + boolean recoverySource) { Field uidField = new Field("_id", Uid.encodeId(id), IdFieldMapper.Defaults.FIELD_TYPE); Field versionField = new NumericDocValuesField("_version", 0); SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); @@ -239,11 +296,57 @@ protected static ParsedDocument testParsedDocument( document.add(seqID.seqNoDocValue); document.add(seqID.primaryTerm); BytesRef ref = source.toBytesRef(); - document.add(new StoredField(SourceFieldMapper.NAME, ref.bytes, ref.offset, ref.length)); + if (recoverySource) { + document.add(new StoredField(SourceFieldMapper.RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length)); + document.add(new NumericDocValuesField(SourceFieldMapper.RECOVERY_SOURCE_NAME, 1)); + } else { + document.add(new StoredField(SourceFieldMapper.NAME, ref.bytes, ref.offset, ref.length)); + } return new ParsedDocument(versionField, seqID, id, "test", routing, Arrays.asList(document), source, XContentType.JSON, mappingUpdate); } + /** + * Creates a tombstone document that only includes uid, seq#, term and version fields. + */ + public static EngineConfig.TombstoneDocSupplier tombstoneDocSupplier(){ + return new EngineConfig.TombstoneDocSupplier() { + @Override + public ParsedDocument newDeleteTombstoneDoc(String type, String id) { + final ParseContext.Document doc = new ParseContext.Document(); + Field uidField = new Field(IdFieldMapper.NAME, Uid.encodeId(id), IdFieldMapper.Defaults.FIELD_TYPE); + doc.add(uidField); + Field versionField = new NumericDocValuesField(VersionFieldMapper.NAME, 0); + doc.add(versionField); + SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); + doc.add(seqID.seqNo); + doc.add(seqID.seqNoDocValue); + doc.add(seqID.primaryTerm); + seqID.tombstoneField.setLongValue(1); + doc.add(seqID.tombstoneField); + return new ParsedDocument(versionField, seqID, id, type, null, + Collections.singletonList(doc), new BytesArray("{}"), XContentType.JSON, null); + } + + @Override + public ParsedDocument newNoopTombstoneDoc(String reason) { + final ParseContext.Document doc = new ParseContext.Document(); + SeqNoFieldMapper.SequenceIDFields seqID = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); + doc.add(seqID.seqNo); + doc.add(seqID.seqNoDocValue); + doc.add(seqID.primaryTerm); + seqID.tombstoneField.setLongValue(1); + doc.add(seqID.tombstoneField); + Field versionField = new NumericDocValuesField(VersionFieldMapper.NAME, 0); + doc.add(versionField); + BytesRef byteRef = new BytesRef(reason); + doc.add(new StoredField(SourceFieldMapper.NAME, byteRef.bytes, byteRef.offset, byteRef.length)); + return new ParsedDocument(versionField, seqID, null, null, null, + Collections.singletonList(doc), null, XContentType.JSON, null); + } + }; + } + protected Store createStore() throws IOException { return createStore(newDirectory()); } @@ -461,7 +564,7 @@ public void onFailedEngine(String reason, @Nullable Exception e) { new NoneCircuitBreakerService(), globalCheckpointSupplier == null ? new ReplicationTracker(shardId, allocationId.getId(), indexSettings, SequenceNumbers.NO_OPS_PERFORMED, update -> {}) : - globalCheckpointSupplier, primaryTerm::get); + globalCheckpointSupplier, primaryTerm::get, tombstoneDocSupplier()); return config; } @@ -474,7 +577,7 @@ protected static BytesArray bytesArray(String string) { return new BytesArray(string.getBytes(Charset.defaultCharset())); } - protected Term newUid(String id) { + protected static Term newUid(String id) { return new Term("_id", Uid.encodeId(id)); } @@ -499,6 +602,279 @@ protected Engine.Index replicaIndexForDoc(ParsedDocument doc, long version, long protected Engine.Delete replicaDeleteForDoc(String id, long version, long seqNo, long startTime) { return new Engine.Delete("test", id, newUid(id), seqNo, 1, version, null, Engine.Operation.Origin.REPLICA, startTime); } + protected static void assertVisibleCount(InternalEngine engine, int numDocs) throws IOException { + assertVisibleCount(engine, numDocs, true); + } + + protected static void assertVisibleCount(InternalEngine engine, int numDocs, boolean refresh) throws IOException { + if (refresh) { + engine.refresh("test"); + } + try (Engine.Searcher searcher = engine.acquireSearcher("test")) { + final TotalHitCountCollector collector = new TotalHitCountCollector(); + searcher.searcher().search(new MatchAllDocsQuery(), collector); + assertThat(collector.getTotalHits(), equalTo(numDocs)); + } + } + + public static List generateSingleDocHistory(boolean forReplica, VersionType versionType, + long primaryTerm, int minOpCount, int maxOpCount, String docId) { + final int numOfOps = randomIntBetween(minOpCount, maxOpCount); + final List ops = new ArrayList<>(); + final Term id = newUid(docId); + final int startWithSeqNo = 0; + final String valuePrefix = (forReplica ? "r_" : "p_" ) + docId + "_"; + final boolean incrementTermWhenIntroducingSeqNo = randomBoolean(); + for (int i = 0; i < numOfOps; i++) { + final Engine.Operation op; + final long version; + switch (versionType) { + case INTERNAL: + version = forReplica ? i : Versions.MATCH_ANY; + break; + case EXTERNAL: + version = i; + break; + case EXTERNAL_GTE: + version = randomBoolean() ? Math.max(i - 1, 0) : i; + break; + case FORCE: + version = randomNonNegativeLong(); + break; + default: + throw new UnsupportedOperationException("unknown version type: " + versionType); + } + if (randomBoolean()) { + op = new Engine.Index(id, testParsedDocument(docId, null, testDocumentWithTextField(valuePrefix + i), B_1, null), + forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, + forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, + version, + forReplica ? null : versionType, + forReplica ? REPLICA : PRIMARY, + System.currentTimeMillis(), -1, false + ); + } else { + op = new Engine.Delete("test", docId, id, + forReplica && i >= startWithSeqNo ? i * 2 : SequenceNumbers.UNASSIGNED_SEQ_NO, + forReplica && i >= startWithSeqNo && incrementTermWhenIntroducingSeqNo ? primaryTerm + 1 : primaryTerm, + version, + forReplica ? null : versionType, + forReplica ? REPLICA : PRIMARY, + System.currentTimeMillis()); + } + ops.add(op); + } + return ops; + } + + public static void assertOpsOnReplica( + final List ops, + final InternalEngine replicaEngine, + boolean shuffleOps, + final Logger logger) throws IOException { + final Engine.Operation lastOp = ops.get(ops.size() - 1); + final String lastFieldValue; + if (lastOp instanceof Engine.Index) { + Engine.Index index = (Engine.Index) lastOp; + lastFieldValue = index.docs().get(0).get("value"); + } else { + // delete + lastFieldValue = null; + } + if (shuffleOps) { + int firstOpWithSeqNo = 0; + while (firstOpWithSeqNo < ops.size() && ops.get(firstOpWithSeqNo).seqNo() < 0) { + firstOpWithSeqNo++; + } + // shuffle ops but make sure legacy ops are first + shuffle(ops.subList(0, firstOpWithSeqNo), random()); + shuffle(ops.subList(firstOpWithSeqNo, ops.size()), random()); + } + boolean firstOp = true; + for (Engine.Operation op : ops) { + logger.info("performing [{}], v [{}], seq# [{}], term [{}]", + op.operationType().name().charAt(0), op.version(), op.seqNo(), op.primaryTerm()); + if (op instanceof Engine.Index) { + Engine.IndexResult result = replicaEngine.index((Engine.Index) op); + // replicas don't really care to about creation status of documents + // this allows to ignore the case where a document was found in the live version maps in + // a delete state and return false for the created flag in favor of code simplicity + // as deleted or not. This check is just signal regression so a decision can be made if it's + // intentional + assertThat(result.isCreated(), equalTo(firstOp)); + assertThat(result.getVersion(), equalTo(op.version())); + assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); + + } else { + Engine.DeleteResult result = replicaEngine.delete((Engine.Delete) op); + // Replicas don't really care to about found status of documents + // this allows to ignore the case where a document was found in the live version maps in + // a delete state and return true for the found flag in favor of code simplicity + // his check is just signal regression so a decision can be made if it's + // intentional + assertThat(result.isFound(), equalTo(firstOp == false)); + assertThat(result.getVersion(), equalTo(op.version())); + assertThat(result.getResultType(), equalTo(Engine.Result.Type.SUCCESS)); + } + if (randomBoolean()) { + replicaEngine.refresh("test"); + } + if (randomBoolean()) { + replicaEngine.flush(); + replicaEngine.refresh("test"); + } + firstOp = false; + } + + assertVisibleCount(replicaEngine, lastFieldValue == null ? 0 : 1); + if (lastFieldValue != null) { + try (Engine.Searcher searcher = replicaEngine.acquireSearcher("test")) { + final TotalHitCountCollector collector = new TotalHitCountCollector(); + searcher.searcher().search(new TermQuery(new Term("value", lastFieldValue)), collector); + assertThat(collector.getTotalHits(), equalTo(1)); + } + } + } + + protected void concurrentlyApplyOps(List ops, InternalEngine engine) throws InterruptedException { + Thread[] thread = new Thread[randomIntBetween(3, 5)]; + CountDownLatch startGun = new CountDownLatch(thread.length); + AtomicInteger offset = new AtomicInteger(-1); + for (int i = 0; i < thread.length; i++) { + thread[i] = new Thread(() -> { + startGun.countDown(); + try { + startGun.await(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + int docOffset; + while ((docOffset = offset.incrementAndGet()) < ops.size()) { + try { + final Engine.Operation op = ops.get(docOffset); + if (op instanceof Engine.Index) { + engine.index((Engine.Index) op); + } else if (op instanceof Engine.Delete){ + engine.delete((Engine.Delete) op); + } else { + engine.noOp((Engine.NoOp) op); + } + if ((docOffset + 1) % 4 == 0) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(); + } + } catch (IOException e) { + throw new AssertionError(e); + } + } + }); + thread[i].start(); + } + for (int i = 0; i < thread.length; i++) { + thread[i].join(); + } + } + + /** + * Gets all docId from the given engine. + */ + public static Set 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<>(); + for (LeafReaderContext leafContext : searcher.reader().leaves()) { + LeafReader reader = leafContext.reader(); + 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))); + } + } + } + return ids; + } + } + + /** + * Reads all engine operations that have been processed by the engine from Lucene index. + * The returned operations are sorted and de-duplicated, thus each sequence number will be have at most one operation. + */ + public static List readAllOperationsInLucene(Engine engine, MapperService mapper) throws IOException { + final List operations = new ArrayList<>(); + long maxSeqNo = Math.max(0, ((InternalEngine)engine).getLocalCheckpointTracker().getMaxSeqNo()); + try (Translog.Snapshot snapshot = engine.newChangesSnapshot("test", mapper, 0, maxSeqNo, false)) { + Translog.Operation op; + while ((op = snapshot.next()) != null){ + operations.add(op); + } + } + return operations; + } + + /** + * 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) { + return; + } + final long maxSeqNo = ((InternalEngine) engine).getLocalCheckpointTracker().getMaxSeqNo(); + if (maxSeqNo < 0) { + return; // nothing to check + } + final Map translogOps = new HashMap<>(); + try (Translog.Snapshot snapshot = EngineTestCase.getTranslog(engine).newSnapshot()) { + Translog.Operation op; + while ((op = snapshot.next()) != null) { + translogOps.put(op.seqNo(), op); + } + } + final Map luceneOps = readAllOperationsInLucene(engine, mapper).stream() + .collect(Collectors.toMap(Translog.Operation::seqNo, Function.identity())); + final long globalCheckpoint = EngineTestCase.getTranslog(engine).getLastSyncedGlobalCheckpoint(); + final long retainedOps = engine.config().getIndexSettings().getSoftDeleteRetentionOperations(); + final long seqNoForRecovery; + try (Engine.IndexCommitRef safeCommit = engine.acquireSafeIndexCommit()) { + seqNoForRecovery = Long.parseLong(safeCommit.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1; + } + final long minSeqNoToRetain = Math.min(seqNoForRecovery, globalCheckpoint + 1 - retainedOps); + for (Translog.Operation translogOp : translogOps.values()) { + final Translog.Operation luceneOp = luceneOps.get(translogOp.seqNo()); + if (luceneOp == null) { + if (minSeqNoToRetain <= translogOp.seqNo() && translogOp.seqNo() <= maxSeqNo) { + fail("Operation not found seq# [" + translogOp.seqNo() + "], global checkpoint [" + globalCheckpoint + "], " + + "retention policy [" + retainedOps + "], maxSeqNo [" + maxSeqNo + "], translog op [" + translogOp + "]"); + } else { + continue; + } + } + assertThat(luceneOp, notNullValue()); + assertThat(luceneOp.toString(), luceneOp.primaryTerm(), equalTo(translogOp.primaryTerm())); + assertThat(luceneOp.opType(), equalTo(translogOp.opType())); + if (luceneOp.opType() == Translog.Operation.Type.INDEX) { + assertThat(luceneOp.getSource().source, equalTo(translogOp.getSource().source)); + } + } + } + + protected MapperService createMapperService(String type) throws IOException { + IndexMetaData indexMetaData = IndexMetaData.builder("test") + .settings(Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1).put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 1)) + .putMapping(type, "{\"properties\": {}}") + .build(); + MapperService mapperService = MapperTestUtils.newMapperService(new NamedXContentRegistry(ClusterModule.getNamedXWriteables()), + createTempDir(), Settings.EMPTY, "test"); + mapperService.merge(indexMetaData, MapperService.MergeReason.MAPPING_UPDATE); + return mapperService; + } /** * Exposes a translog associated with the given engine for testing purpose. 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 3f1f5daf5148..f2afdff9c3a3 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 @@ -60,6 +60,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.InternalEngineFactory; import org.elasticsearch.index.seqno.GlobalCheckpointSyncAction; @@ -99,10 +100,14 @@ public abstract class ESIndexLevelReplicationTestCase extends IndexShardTestCase protected final Index index = new Index("test", "uuid"); private final ShardId shardId = new ShardId(index, 0); - private final Map indexMapping = Collections.singletonMap("type", "{ \"type\": {} }"); + protected final Map indexMapping = Collections.singletonMap("type", "{ \"type\": {} }"); protected ReplicationGroup createGroup(int replicas) throws IOException { - IndexMetaData metaData = buildIndexMetaData(replicas); + return createGroup(replicas, Settings.EMPTY); + } + + protected ReplicationGroup createGroup(int replicas, Settings settings) throws IOException { + IndexMetaData metaData = buildIndexMetaData(replicas, settings, indexMapping); return new ReplicationGroup(metaData); } @@ -111,9 +116,17 @@ protected IndexMetaData buildIndexMetaData(int replicas) throws IOException { } protected IndexMetaData buildIndexMetaData(int replicas, Map mappings) throws IOException { + return buildIndexMetaData(replicas, Settings.EMPTY, mappings); + } + + protected IndexMetaData buildIndexMetaData(int replicas, Settings indexSettings, Map mappings) throws IOException { Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, replicas) .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + .put(indexSettings) .build(); IndexMetaData.Builder metaData = IndexMetaData.builder(index.getName()) .settings(settings) @@ -146,7 +159,7 @@ protected class ReplicationGroup implements AutoCloseable, Iterable } }); - ReplicationGroup(final IndexMetaData indexMetaData) throws IOException { + protected ReplicationGroup(final IndexMetaData indexMetaData) throws IOException { final ShardRouting primaryRouting = this.createShardRouting("s0", true); primary = newShard(primaryRouting, indexMetaData, null, getEngineFactory(primaryRouting), () -> {}); replicas = new CopyOnWriteArrayList<>(); @@ -448,7 +461,7 @@ private void updateAllocationIDsOnPrimary() throws IOException { } } - abstract class ReplicationAction, + protected abstract class ReplicationAction, ReplicaRequest extends ReplicationRequest, Response extends ReplicationResponse> { private final Request request; @@ -456,7 +469,7 @@ abstract class ReplicationAction, private final ReplicationGroup replicationGroup; private final String opType; - ReplicationAction(Request request, ActionListener listener, ReplicationGroup group, String opType) { + protected ReplicationAction(Request request, ActionListener listener, ReplicationGroup group, String opType) { this.request = request; this.listener = listener; this.replicationGroup = group; @@ -582,11 +595,11 @@ public void markShardCopyAsStaleIfNeeded(ShardId shardId, String allocationId, R } } - class PrimaryResult implements ReplicationOperation.PrimaryResult { + protected class PrimaryResult implements ReplicationOperation.PrimaryResult { final ReplicaRequest replicaRequest; final Response finalResponse; - PrimaryResult(ReplicaRequest replicaRequest, Response finalResponse) { + public PrimaryResult(ReplicaRequest replicaRequest, Response finalResponse) { this.replicaRequest = replicaRequest; this.finalResponse = finalResponse; } 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 d2a84589669a..2f4a3dfd6c12 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 @@ -18,13 +18,8 @@ */ package org.elasticsearch.index.shard; -import org.apache.lucene.document.Document; import org.apache.lucene.index.IndexNotFoundException; -import org.apache.lucene.index.LeafReader; -import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.store.Directory; -import org.apache.lucene.util.Bits; -import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.flush.FlushRequest; import org.elasticsearch.action.index.IndexRequest; @@ -57,10 +52,8 @@ import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.EngineTestCase; import org.elasticsearch.index.engine.InternalEngineFactory; -import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.SourceToParse; -import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.seqno.ReplicationTracker; import org.elasticsearch.index.seqno.SequenceNumbers; import org.elasticsearch.index.similarity.SimilarityService; @@ -180,37 +173,63 @@ public Directory newDirectory() throws IOException { } /** - * creates a new initializing shard. The shard will have its own unique data path. + * Creates a new initializing shard. The shard will have its own unique data path. * - * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica - * (ready to recover from another shard) + * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica (ready to recover from + * another shard) */ protected IndexShard newShard(boolean primary) throws IOException { - ShardRouting shardRouting = TestShardRouting.newShardRouting(new ShardId("index", "_na_", 0), randomAlphaOfLength(10), primary, - ShardRoutingState.INITIALIZING, - primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE); - return newShard(shardRouting); + return newShard(primary, Settings.EMPTY, new InternalEngineFactory()); } /** - * creates a new initializing shard. The shard will have its own unique data path. + * Creates a new initializing shard. The shard will have its own unique data path. + * + * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica (ready to recover from + * another shard) + * @param settings the settings to use for this shard + * @param engineFactory the engine factory to use for this shard + */ + protected IndexShard newShard(boolean primary, Settings settings, EngineFactory engineFactory) throws IOException { + final RecoverySource recoverySource = + primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE; + final ShardRouting shardRouting = + TestShardRouting.newShardRouting( + new ShardId("index", "_na_", 0), randomAlphaOfLength(10), primary, ShardRoutingState.INITIALIZING, recoverySource); + return newShard(shardRouting, settings, engineFactory); + } + + protected IndexShard newShard(ShardRouting shardRouting, final IndexingOperationListener... listeners) throws IOException { + return newShard(shardRouting, Settings.EMPTY, new InternalEngineFactory(), listeners); + } + + /** + * Creates a new initializing shard. The shard will have its own unique data path. * - * @param shardRouting the {@link ShardRouting} to use for this shard - * @param listeners an optional set of listeners to add to the shard + * @param shardRouting the {@link ShardRouting} to use for this shard + * @param settings the settings to use for this shard + * @param engineFactory the engine factory to use for this shard + * @param listeners an optional set of listeners to add to the shard */ protected IndexShard newShard( final ShardRouting shardRouting, + final Settings settings, + final EngineFactory engineFactory, final IndexingOperationListener... listeners) throws IOException { assert shardRouting.initializing() : shardRouting; - 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(); + Settings indexSettings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + .put(settings) + .build(); IndexMetaData.Builder metaData = IndexMetaData.builder(shardRouting.getIndexName()) - .settings(settings) + .settings(indexSettings) .primaryTerm(0, primaryTerm) .putMapping("_doc", "{ \"properties\": {} }"); - return newShard(shardRouting, metaData.build(), listeners); + return newShard(shardRouting, metaData.build(), engineFactory, listeners); } /** @@ -225,7 +244,7 @@ protected IndexShard newShard(ShardId shardId, boolean primary, IndexingOperatio ShardRouting shardRouting = TestShardRouting.newShardRouting(shardId, randomAlphaOfLength(5), primary, ShardRoutingState.INITIALIZING, primary ? RecoverySource.StoreRecoverySource.EMPTY_STORE_INSTANCE : RecoverySource.PeerRecoverySource.INSTANCE); - return newShard(shardRouting, listeners); + return newShard(shardRouting, Settings.EMPTY, new InternalEngineFactory(), listeners); } /** @@ -265,9 +284,10 @@ protected IndexShard newShard(ShardId shardId, boolean primary, String nodeId, I * @param indexMetaData indexMetaData for the shard, including any mapping * @param listeners an optional set of listeners to add to the shard */ - protected IndexShard newShard(ShardRouting routing, IndexMetaData indexMetaData, IndexingOperationListener... listeners) + protected IndexShard newShard( + ShardRouting routing, IndexMetaData indexMetaData, EngineFactory engineFactory, IndexingOperationListener... listeners) throws IOException { - return newShard(routing, indexMetaData, null, new InternalEngineFactory(), () -> {}, listeners); + return newShard(routing, indexMetaData, null, engineFactory, () -> {}, listeners); } /** @@ -372,19 +392,39 @@ protected IndexShard reinitShard(IndexShard current, ShardRouting routing, Index } /** - * creates a new empyu shard and starts it. The shard will be either a replica or a primary. + * Creates a new empty shard and starts it. The shard will randomly be a replica or a primary. */ protected IndexShard newStartedShard() throws IOException { return newStartedShard(randomBoolean()); } /** - * creates a new empty shard and starts it. + * Creates a new empty shard and starts it + * @param settings the settings to use for this shard + */ + protected IndexShard newStartedShard(Settings settings) throws IOException { + return newStartedShard(randomBoolean(), settings, new InternalEngineFactory()); + } + + /** + * Creates a new empty shard and starts it. * * @param primary controls whether the shard will be a primary or a replica. */ - protected IndexShard newStartedShard(boolean primary) throws IOException { - IndexShard shard = newShard(primary); + protected IndexShard newStartedShard(final boolean primary) throws IOException { + return newStartedShard(primary, Settings.EMPTY, new InternalEngineFactory()); + } + + /** + * Creates a new empty shard with the specified settings and engine factory and starts it. + * + * @param primary controls whether the shard will be a primary or a replica. + * @param settings the settings to use for this shard + * @param engineFactory the engine factory to use for this shard + */ + protected IndexShard newStartedShard( + final boolean primary, final Settings settings, final EngineFactory engineFactory) throws IOException { + IndexShard shard = newShard(primary, settings, engineFactory); if (primary) { recoverShardFromStore(shard); } else { @@ -401,6 +441,7 @@ 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()); @@ -582,22 +623,7 @@ private Store.MetadataSnapshot getMetadataSnapshotOrEmpty(IndexShard replica) th } protected Set getShardDocUIDs(final IndexShard shard) throws IOException { - shard.refresh("get_uids"); - try (Engine.Searcher searcher = shard.acquireSearcher("test")) { - Set ids = new HashSet<>(); - for (LeafReaderContext leafContext : searcher.reader().leaves()) { - LeafReader reader = leafContext.reader(); - 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))); - } - } - } - return ids; - } + return EngineTestCase.getDocIds(shard.getEngine(), true); } protected void assertDocCount(IndexShard shard, int docDount) throws IOException { @@ -610,6 +636,12 @@ protected void assertDocs(IndexShard shard, String... ids) throws IOException { assertThat(shardDocUIDs, hasSize(ids.length)); } + public static void assertConsistentHistoryBetweenTranslogAndLucene(IndexShard shard) throws IOException { + final Engine engine = shard.getEngineOrNull(); + if (engine != null) { + EngineTestCase.assertConsistentHistoryBetweenTranslogAndLuceneIndex(engine, shard.mapperService()); + } + } protected Engine.IndexResult indexDoc(IndexShard shard, String type, String id) throws IOException { return indexDoc(shard, type, id, "{}"); @@ -653,11 +685,14 @@ protected void updateMappings(IndexShard shard, IndexMetaData indexMetadata) { } protected Engine.DeleteResult deleteDoc(IndexShard shard, String type, String id) throws IOException { + final Engine.DeleteResult result; if (shard.routingEntry().primary()) { - return shard.applyDeleteOperationOnPrimary(Versions.MATCH_ANY, type, id, VersionType.INTERNAL); + result = shard.applyDeleteOperationOnPrimary(Versions.MATCH_ANY, type, id, VersionType.INTERNAL); + shard.updateLocalCheckpointForShard(shard.routingEntry().allocationId().getId(), shard.getEngine().getLocalCheckpoint()); } else { - return shard.applyDeleteOperationOnReplica(shard.seqNoStats().getMaxSeqNo() + 1, 0L, type, id); + result = shard.applyDeleteOperationOnReplica(shard.seqNoStats().getMaxSeqNo() + 1, 0L, type, id); } + return result; } protected void flushShard(IndexShard shard) { 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 322e2a128c97..be9e40ab4209 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -723,6 +723,10 @@ public Settings indexSettings() { } // always default delayed allocation to 0 to make sure we have tests are not delayed builder.put(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), 0); + builder.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()); + if (randomBoolean()) { + builder.put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), between(0, 1000)); + } return builder.build(); } 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 9633f56dea94..19290f8cf118 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESSingleNodeTestCase.java @@ -41,6 +41,7 @@ import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.breaker.HierarchyCircuitBreakerService; import org.elasticsearch.node.MockNode; @@ -87,6 +88,14 @@ protected void startNode(long seed) throws Exception { .setOrder(0) .setSettings(Settings.builder().put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)).get(); + client().admin().indices() + .preparePutTemplate("random-soft-deletes-template") + .setPatterns(Collections.singletonList("*")) + .setOrder(0) + .setSettings(Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean()) + .put(IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.getKey(), + randomBoolean() ? IndexSettings.INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING.get(Settings.EMPTY) : between(0, 1000)) + ).get(); } private static void stopNode() throws IOException { 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 306f79e5e16e..4c813372fae3 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -1163,6 +1163,26 @@ private void assertOpenTranslogReferences() throws Exception { }); } + /** + * Asserts that the document history in Lucene index is consistent with Translog's on every index shard of the cluster. + * This assertion might be expensive, thus we prefer not to execute on every test but only interesting tests. + */ + public void assertConsistentHistoryBetweenTranslogAndLuceneIndex() throws IOException { + final Collection nodesAndClients = nodes.values(); + for (NodeAndClient nodeAndClient : nodesAndClients) { + IndicesService indexServices = getInstance(IndicesService.class, nodeAndClient.name); + for (IndexService indexService : indexServices) { + for (IndexShard indexShard : indexService) { + try { + IndexShardTestCase.assertConsistentHistoryBetweenTranslogAndLucene(indexShard); + } catch (AlreadyClosedException ignored) { + // shard is closed + } + } + } + } + } + private void randomlyResetClients() throws IOException { // only reset the clients on nightly tests, it causes heavy load... if (RandomizedTest.isNightly() && rarely(random)) { From 88d8cadf3ffb13026a5c5994a4ec0f981d3f5772 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 31 Aug 2018 00:07:11 -0400 Subject: [PATCH 252/283] Backout IcuTokenizerFactory change This change is no longer needed as it's handled in the upstream. --- .../elasticsearch/index/analysis/IcuTokenizerFactory.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/index/analysis/IcuTokenizerFactory.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/index/analysis/IcuTokenizerFactory.java index 0b84f538dcd7..3f8b9296aa02 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/index/analysis/IcuTokenizerFactory.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/index/analysis/IcuTokenizerFactory.java @@ -22,6 +22,7 @@ import com.ibm.icu.lang.UCharacter; import com.ibm.icu.lang.UProperty; import com.ibm.icu.lang.UScript; +import com.ibm.icu.text.BreakIterator; import com.ibm.icu.text.RuleBasedBreakIterator; import org.apache.lucene.analysis.Tokenizer; import org.apache.lucene.analysis.icu.segmentation.DefaultICUTokenizerConfig; @@ -79,7 +80,7 @@ private ICUTokenizerConfig getIcuConfig(Environment env, Settings settings) { if (tailored.isEmpty()) { return null; } else { - final RuleBasedBreakIterator breakers[] = new RuleBasedBreakIterator[UScript.CODE_LIMIT]; + final BreakIterator breakers[] = new BreakIterator[UScript.CODE_LIMIT]; for (Map.Entry entry : tailored.entrySet()) { int code = entry.getKey(); String resourcePath = entry.getValue(); @@ -104,7 +105,7 @@ public RuleBasedBreakIterator getBreakIterator(int script) { } //parse a single RBBi rule file - private RuleBasedBreakIterator parseRules(String filename, Environment env) throws IOException { + private BreakIterator parseRules(String filename, Environment env) throws IOException { final Path path = env.configFile().resolve(filename); String rules = Files.readAllLines(path) From 44ed5f6306038c7f80cbd6ffe403104248145d1e Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 31 Aug 2018 09:31:55 +0300 Subject: [PATCH 253/283] Enable forbiddenapis server java9 (#33245) --- .../groovy/org/elasticsearch/gradle/BuildPlugin.groovy | 3 ++- .../gradle/precommit/PrecommitTasks.groovy | 2 +- libs/core/build.gradle | 9 +++++---- server/build.gradle | 10 +++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 4c4a8cbe8810..6a9d4076eef8 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -38,7 +38,6 @@ import org.gradle.api.artifacts.ModuleDependency import org.gradle.api.artifacts.ModuleVersionIdentifier import org.gradle.api.artifacts.ProjectDependency import org.gradle.api.artifacts.ResolvedArtifact -import org.gradle.api.artifacts.SelfResolvingDependency import org.gradle.api.artifacts.dsl.RepositoryHandler import org.gradle.api.execution.TaskExecutionGraph import org.gradle.api.plugins.JavaPlugin @@ -212,6 +211,7 @@ class BuildPlugin implements Plugin { project.rootProject.ext.minimumRuntimeVersion = minimumRuntimeVersion project.rootProject.ext.inFipsJvm = inFipsJvm project.rootProject.ext.gradleJavaVersion = JavaVersion.toVersion(gradleJavaVersion) + project.rootProject.ext.java9Home = findJavaHome("9") } project.targetCompatibility = project.rootProject.ext.minimumRuntimeVersion @@ -225,6 +225,7 @@ class BuildPlugin implements Plugin { project.ext.javaVersions = project.rootProject.ext.javaVersions project.ext.inFipsJvm = project.rootProject.ext.inFipsJvm project.ext.gradleJavaVersion = project.rootProject.ext.gradleJavaVersion + project.ext.java9Home = project.rootProject.ext.java9Home } private static String getPaddedMajorVersion(JavaVersion compilerJavaVersionEnum) { diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index be7561853bbb..06557d4ccfdb 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -100,7 +100,7 @@ class PrecommitTasks { private static Task configureForbiddenApisCli(Project project) { Task forbiddenApisCli = project.tasks.create('forbiddenApis') - project.sourceSets.forEach { sourceSet -> + project.sourceSets.all { sourceSet -> forbiddenApisCli.dependsOn( project.tasks.create(sourceSet.getTaskName('forbiddenApis', null), ForbiddenApisCliTask) { ExportElasticsearchBuildResourcesTask buildResources = project.tasks.getByName('buildResources') diff --git a/libs/core/build.gradle b/libs/core/build.gradle index cc5c1e20fc16..9c90837bd80e 100644 --- a/libs/core/build.gradle +++ b/libs/core/build.gradle @@ -46,12 +46,13 @@ if (!isEclipse && !isIdea) { targetCompatibility = 9 } - /* Enable this when forbiddenapis was updated to 2.6. - * See: https://github.com/elastic/elasticsearch/issues/29292 forbiddenApisJava9 { - targetCompatibility = 9 + if (project.runtimeJavaVersion < JavaVersion.VERSION_1_9) { + targetCompatibility = JavaVersion.VERSION_1_9 + javaHome = project.java9Home + } + replaceSignatureFiles 'jdk-signatures' } - */ jar { metaInf { diff --git a/server/build.gradle b/server/build.gradle index edc3f427dfda..c01fb92b0508 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -58,13 +58,13 @@ if (!isEclipse && !isIdea) { sourceCompatibility = 9 targetCompatibility = 9 } - - /* Enable this when forbiddenapis was updated to 2.6. - * See: https://github.com/elastic/elasticsearch/issues/29292 + forbiddenApisJava9 { - targetCompatibility = 9 + if (project.runtimeJavaVersion < JavaVersion.VERSION_1_9) { + targetCompatibility = JavaVersion.VERSION_1_9 + javaHome = project.java9Home + } } - */ jar { metaInf { From c6cfa08a615f5b433cf64bef63220f4a22727115 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 31 Aug 2018 08:40:27 +0200 Subject: [PATCH 254/283] MINOR: Remove Dead Code from PathTrie (#33280) * The array size checks are redundant since the array sizes are checked earlier in those methods too * The removed methods are just not used anywhere --- .../elasticsearch/common/path/PathTrie.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/path/PathTrie.java b/server/src/main/java/org/elasticsearch/common/path/PathTrie.java index 5243809c64a1..08787cea9df7 100644 --- a/server/src/main/java/org/elasticsearch/common/path/PathTrie.java +++ b/server/src/main/java/org/elasticsearch/common/path/PathTrie.java @@ -104,24 +104,12 @@ public void updateKeyWithNamedWildcard(String key) { namedWildcard = key.substring(key.indexOf('{') + 1, key.indexOf('}')); } - public boolean isWildcard() { - return isWildcard; - } - - public synchronized void addChild(TrieNode child) { - addInnerChild(child.key, child); - } - private void addInnerChild(String key, TrieNode child) { Map newChildren = new HashMap<>(children); newChildren.put(key, child); children = unmodifiableMap(newChildren); } - public TrieNode getChild(String key) { - return children.get(key); - } - public synchronized void insert(String[] path, int index, T value) { if (index >= path.length) return; @@ -302,7 +290,7 @@ public void insert(String path, T value) { } int index = 0; // Supports initial delimiter. - if (strings.length > 0 && strings[0].isEmpty()) { + if (strings[0].isEmpty()) { index = 1; } root.insert(strings, index, value); @@ -327,7 +315,7 @@ public void insertOrUpdate(String path, T value, BiFunction updater) { } int index = 0; // Supports initial delimiter. - if (strings.length > 0 && strings[0].isEmpty()) { + if (strings[0].isEmpty()) { index = 1; } root.insertOrUpdate(strings, index, value, updater); @@ -352,7 +340,7 @@ public T retrieve(String path, Map params, TrieMatchingMode trie int index = 0; // Supports initial delimiter. - if (strings.length > 0 && strings[0].isEmpty()) { + if (strings[0].isEmpty()) { index = 1; } From 73eb4cbbbe1c38508f6fc303ca300c508952b507 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Fri, 31 Aug 2018 10:45:25 +0300 Subject: [PATCH 255/283] SQL: Support multi-index format as table identifier (#33278) Extend tableIdentifier to support multi-index format; not just * but also enumeration and exclusion Fix #33162 --- .../sql/analysis/index/IndexResolver.java | 3 +- .../xpack/sql/execution/search/Querier.java | 3 +- .../xpack/sql/parser/IdentifierBuilder.java | 14 ------- .../sql/parser/IdentifierBuilderTests.java | 38 ------------------- .../sql/src/main/resources/command.csv-spec | 24 ++++++++++++ 5 files changed, 28 insertions(+), 54 deletions(-) delete mode 100644 x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilderTests.java diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java index 10586c991b1a..b11542d40ed5 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/index/IndexResolver.java @@ -19,6 +19,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.cluster.metadata.AliasMetaData; import org.elasticsearch.cluster.metadata.MappingMetaData; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.xpack.sql.type.EsField; @@ -300,7 +301,7 @@ public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, Ac private static GetIndexRequest createGetIndexRequest(String index) { return new GetIndexRequest() .local(true) - .indices(index) + .indices(Strings.commaDelimitedListToStringArray(index)) .features(Feature.MAPPINGS) //lenient because we throw our own errors looking at the response e.g. if something was not resolved //also because this way security doesn't throw authorization exceptions but rather honours ignore_unavailable diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java index 055e34758cc7..d0bff77a6485 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.client.Client; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.CollectionUtils; @@ -92,7 +93,7 @@ public void query(Schema schema, QueryContainer query, String index, ActionListe log.trace("About to execute query {} on {}", StringUtils.toString(sourceBuilder), index); } - SearchRequest search = prepareRequest(client, sourceBuilder, timeout, index); + SearchRequest search = prepareRequest(client, sourceBuilder, timeout, Strings.commaDelimitedListToStringArray(index)); ActionListener l; if (query.isAggsOnly()) { diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java index 8c79ae1ef059..f09f543c6ffa 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java @@ -21,23 +21,9 @@ public TableIdentifier visitTableIdentifier(TableIdentifierContext ctx) { ParseTree tree = ctx.name != null ? ctx.name : ctx.TABLE_IDENTIFIER(); String index = tree.getText(); - validateIndex(index, source); return new TableIdentifier(source, visitIdentifier(ctx.catalog), index); } - // see https://github.com/elastic/elasticsearch/issues/6736 - static void validateIndex(String index, Location source) { - for (int i = 0; i < index.length(); i++) { - char c = index.charAt(i); - if (Character.isUpperCase(c)) { - throw new ParsingException(source, "Invalid index name (needs to be lowercase) {}", index); - } - if (c == '\\' || c == '/' || c == '<' || c == '>' || c == '|' || c == ',' || c == ' ') { - throw new ParsingException(source, "Invalid index name (illegal character {}) {}", c, index); - } - } - } - @Override public String visitIdentifier(IdentifierContext ctx) { return ctx == null ? null : ctx.getText(); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilderTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilderTests.java deleted file mode 100644 index ec8b8abc51f2..000000000000 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilderTests.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.sql.parser; - -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.sql.tree.Location; - -import static org.hamcrest.Matchers.is; - -public class IdentifierBuilderTests extends ESTestCase { - - private static Location L = new Location(1, 10); - - public void testTypicalIndex() throws Exception { - IdentifierBuilder.validateIndex("some-index", L); - } - - public void testInternalIndex() throws Exception { - IdentifierBuilder.validateIndex(".some-internal-index-2020-02-02", L); - } - - public void testIndexPattern() throws Exception { - IdentifierBuilder.validateIndex(".some-*", L); - } - - public void testInvalidIndex() throws Exception { - ParsingException pe = expectThrows(ParsingException.class, () -> IdentifierBuilder.validateIndex("some,index", L)); - assertThat(pe.getMessage(), is("line 1:12: Invalid index name (illegal character ,) some,index")); - } - - public void testUpperCasedIndex() throws Exception { - ParsingException pe = expectThrows(ParsingException.class, () -> IdentifierBuilder.validateIndex("thisIsAnIndex", L)); - assertThat(pe.getMessage(), is("line 1:12: Invalid index name (needs to be lowercase) thisIsAnIndex")); - } -} 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 89e86e887e14..a8f23e27ffac 100644 --- a/x-pack/qa/sql/src/main/resources/command.csv-spec +++ b/x-pack/qa/sql/src/main/resources/command.csv-spec @@ -162,3 +162,27 @@ last_name | VARCHAR last_name.keyword | VARCHAR salary | INTEGER ; + + +describeIncludeExclude +DESCRIBE "test_emp*,-test_alias*"; + +column:s | type:s +birth_date | TIMESTAMP +dep | STRUCT +dep.dep_id | VARCHAR +dep.dep_name | VARCHAR +dep.dep_name.keyword | VARCHAR +dep.from_date | TIMESTAMP +dep.to_date | TIMESTAMP +emp_no | INTEGER +first_name | VARCHAR +first_name.keyword | VARCHAR +gender | VARCHAR +hire_date | TIMESTAMP +languages | TINYINT +last_name | VARCHAR +last_name.keyword | VARCHAR +salary | INTEGER +; + From 7345878d33a00ce95eae929f79b2da0b326d182d Mon Sep 17 00:00:00 2001 From: David Roberts Date: Fri, 31 Aug 2018 08:48:45 +0100 Subject: [PATCH 256/283] [ML] Refactor delimited file structure detection (#33233) 1. Use the term "delimited" rather than "separated values" 2. Use a single factory class with arguments to specify the delimiter and identification constraints This change makes it easier to add support for other delimiter characters. --- .../CsvLogStructureFinderFactory.java | 35 ----- ....java => DelimitedLogStructureFinder.java} | 16 +-- .../DelimitedLogStructureFinderFactory.java | 57 ++++++++ .../ml/logstructurefinder/LogStructure.java | 124 ++++-------------- .../LogStructureFinderManager.java | 8 +- .../logstructurefinder/LogStructureUtils.java | 10 +- ...aratedValuesLogStructureFinderFactory.java | 38 ------ ...aratedValuesLogStructureFinderFactory.java | 37 ------ .../TsvLogStructureFinderFactory.java | 4 +- .../CsvLogStructureFinderFactoryTests.java | 38 ------ ...limitedLogStructureFinderFactoryTests.java | 93 +++++++++++++ ... => DelimitedLogStructureFinderTests.java} | 124 +++++++++--------- .../JsonLogStructureFinderFactoryTests.java | 8 +- .../JsonLogStructureFinderTests.java | 2 +- .../LogStructureFinderManagerTests.java | 2 +- .../LogStructureTestCase.java | 4 +- .../logstructurefinder/LogStructureTests.java | 8 +- ...dValuesLogStructureFinderFactoryTests.java | 23 ---- ...dValuesLogStructureFinderFactoryTests.java | 28 ---- .../TextLogStructureFinderFactoryTests.java | 4 +- .../TextLogStructureFinderTests.java | 2 +- .../TsvLogStructureFinderFactoryTests.java | 33 ----- .../XmlLogStructureFinderFactoryTests.java | 8 +- .../XmlLogStructureFinderTests.java | 2 +- 24 files changed, 278 insertions(+), 430 deletions(-) delete mode 100644 x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactory.java rename x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/{SeparatedValuesLogStructureFinder.java => DelimitedLogStructureFinder.java} (97%) create mode 100644 x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactory.java delete mode 100644 x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactory.java delete mode 100644 x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactory.java delete mode 100644 x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactoryTests.java create mode 100644 x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactoryTests.java rename x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/{SeparatedValuesLogStructureFinderTests.java => DelimitedLogStructureFinderTests.java} (65%) delete mode 100644 x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactoryTests.java delete mode 100644 x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactoryTests.java delete mode 100644 x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactoryTests.java diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactory.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactory.java deleted file mode 100644 index cb9e6537252c..000000000000 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactory.java +++ /dev/null @@ -1,35 +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.ml.logstructurefinder; - -import org.supercsv.prefs.CsvPreference; - -import java.io.IOException; -import java.util.List; - -public class CsvLogStructureFinderFactory implements LogStructureFinderFactory { - - /** - * Rules are: - * - The file must be valid CSV - * - It must contain at least two complete records - * - There must be at least two fields per record (otherwise files with no commas could be treated as CSV!) - * - Every CSV record except the last must have the same number of fields - * The reason the last record is allowed to have fewer fields than the others is that - * it could have been truncated when the file was sampled. - */ - @Override - public boolean canCreateFromSample(List explanation, String sample) { - return SeparatedValuesLogStructureFinder.canCreateFromSample(explanation, sample, 2, CsvPreference.EXCEL_PREFERENCE, "CSV"); - } - - @Override - public LogStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws IOException { - return SeparatedValuesLogStructureFinder.makeSeparatedValuesLogStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, - CsvPreference.EXCEL_PREFERENCE, false); - } -} diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/SeparatedValuesLogStructureFinder.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinder.java similarity index 97% rename from x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/SeparatedValuesLogStructureFinder.java rename to x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinder.java index fd9d34096b2e..2f7bb41d0bae 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/SeparatedValuesLogStructureFinder.java +++ b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinder.java @@ -29,17 +29,16 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -public class SeparatedValuesLogStructureFinder implements LogStructureFinder { +public class DelimitedLogStructureFinder implements LogStructureFinder { private static final int MAX_LEVENSHTEIN_COMPARISONS = 100; private final List sampleMessages; private final LogStructure structure; - static SeparatedValuesLogStructureFinder makeSeparatedValuesLogStructureFinder(List explanation, String sample, - String charsetName, Boolean hasByteOrderMarker, - CsvPreference csvPreference, boolean trimFields) - throws IOException { + static DelimitedLogStructureFinder makeDelimitedLogStructureFinder(List explanation, String sample, String charsetName, + Boolean hasByteOrderMarker, CsvPreference csvPreference, + boolean trimFields) throws IOException { Tuple>, List> parsed = readRows(sample, csvPreference); List> rows = parsed.v1(); @@ -73,13 +72,14 @@ static SeparatedValuesLogStructureFinder makeSeparatedValuesLogStructureFinder(L String preamble = Pattern.compile("\n").splitAsStream(sample).limit(lineNumbers.get(1)).collect(Collectors.joining("\n", "", "\n")); char delimiter = (char) csvPreference.getDelimiterChar(); - LogStructure.Builder structureBuilder = new LogStructure.Builder(LogStructure.Format.fromSeparator(delimiter)) + LogStructure.Builder structureBuilder = new LogStructure.Builder(LogStructure.Format.DELIMITED) .setCharset(charsetName) .setHasByteOrderMarker(hasByteOrderMarker) .setSampleStart(preamble) .setNumLinesAnalyzed(lineNumbers.get(lineNumbers.size() - 1)) .setNumMessagesAnalyzed(sampleRecords.size()) .setHasHeaderRow(isHeaderInFile) + .setDelimiter(delimiter) .setInputFields(Arrays.stream(headerWithNamedBlanks).collect(Collectors.toList())); if (trimFields) { @@ -131,10 +131,10 @@ static SeparatedValuesLogStructureFinder makeSeparatedValuesLogStructureFinder(L .setExplanation(explanation) .build(); - return new SeparatedValuesLogStructureFinder(sampleMessages, structure); + return new DelimitedLogStructureFinder(sampleMessages, structure); } - private SeparatedValuesLogStructureFinder(List sampleMessages, LogStructure structure) { + private DelimitedLogStructureFinder(List sampleMessages, LogStructure structure) { this.sampleMessages = Collections.unmodifiableList(sampleMessages); this.structure = structure; } diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactory.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactory.java new file mode 100644 index 000000000000..3e4c3ea225cf --- /dev/null +++ b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.logstructurefinder; + +import org.supercsv.prefs.CsvPreference; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +public class DelimitedLogStructureFinderFactory implements LogStructureFinderFactory { + + private final CsvPreference csvPreference; + private final int minFieldsPerRow; + private final boolean trimFields; + + DelimitedLogStructureFinderFactory(char delimiter, int minFieldsPerRow, boolean trimFields) { + csvPreference = new CsvPreference.Builder('"', delimiter, "\n").build(); + this.minFieldsPerRow = minFieldsPerRow; + this.trimFields = trimFields; + } + + /** + * Rules are: + * - It must contain at least two complete records + * - There must be a minimum number of fields per record (otherwise files with no commas could be treated as CSV!) + * - Every record except the last must have the same number of fields + * The reason the last record is allowed to have fewer fields than the others is that + * it could have been truncated when the file was sampled. + */ + @Override + public boolean canCreateFromSample(List explanation, String sample) { + String formatName; + switch ((char) csvPreference.getDelimiterChar()) { + case ',': + formatName = "CSV"; + break; + case '\t': + formatName = "TSV"; + break; + default: + formatName = Character.getName(csvPreference.getDelimiterChar()).toLowerCase(Locale.ROOT) + " delimited values"; + break; + } + return DelimitedLogStructureFinder.canCreateFromSample(explanation, sample, minFieldsPerRow, csvPreference, formatName); + } + + @Override + public LogStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) + throws IOException { + return DelimitedLogStructureFinder.makeDelimitedLogStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, + csvPreference, trimFields); + } +} diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructure.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructure.java index 64a00d20899c..ea8fe37e62f9 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructure.java +++ b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructure.java @@ -27,37 +27,14 @@ public class LogStructure implements ToXContentObject { public enum Format { - JSON, XML, CSV, TSV, SEMI_COLON_SEPARATED_VALUES, PIPE_SEPARATED_VALUES, SEMI_STRUCTURED_TEXT; - - public Character separator() { - switch (this) { - case JSON: - case XML: - return null; - case CSV: - return ','; - case TSV: - return '\t'; - case SEMI_COLON_SEPARATED_VALUES: - return ';'; - case PIPE_SEPARATED_VALUES: - return '|'; - case SEMI_STRUCTURED_TEXT: - return null; - default: - throw new IllegalStateException("enum value [" + this + "] missing from switch."); - } - } + JSON, XML, DELIMITED, SEMI_STRUCTURED_TEXT; public boolean supportsNesting() { switch (this) { case JSON: case XML: return true; - case CSV: - case TSV: - case SEMI_COLON_SEPARATED_VALUES: - case PIPE_SEPARATED_VALUES: + case DELIMITED: case SEMI_STRUCTURED_TEXT: return false; default: @@ -69,10 +46,7 @@ public boolean isStructured() { switch (this) { case JSON: case XML: - case CSV: - case TSV: - case SEMI_COLON_SEPARATED_VALUES: - case PIPE_SEPARATED_VALUES: + case DELIMITED: return true; case SEMI_STRUCTURED_TEXT: return false; @@ -85,10 +59,7 @@ public boolean isSemiStructured() { switch (this) { case JSON: case XML: - case CSV: - case TSV: - case SEMI_COLON_SEPARATED_VALUES: - case PIPE_SEPARATED_VALUES: + case DELIMITED: return false; case SEMI_STRUCTURED_TEXT: return true; @@ -97,38 +68,6 @@ public boolean isSemiStructured() { } } - public boolean isSeparatedValues() { - switch (this) { - case JSON: - case XML: - return false; - case CSV: - case TSV: - case SEMI_COLON_SEPARATED_VALUES: - case PIPE_SEPARATED_VALUES: - return true; - case SEMI_STRUCTURED_TEXT: - return false; - default: - throw new IllegalStateException("enum value [" + this + "] missing from switch."); - } - } - - public static Format fromSeparator(char separator) { - switch (separator) { - case ',': - return CSV; - case '\t': - return TSV; - case ';': - return SEMI_COLON_SEPARATED_VALUES; - case '|': - return PIPE_SEPARATED_VALUES; - default: - throw new IllegalArgumentException("No known format has separator [" + separator + "]"); - } - } - public static Format fromString(String name) { return valueOf(name.trim().toUpperCase(Locale.ROOT)); } @@ -149,7 +88,7 @@ public String toString() { static final ParseField EXCLUDE_LINES_PATTERN = new ParseField("exclude_lines_pattern"); static final ParseField INPUT_FIELDS = new ParseField("input_fields"); static final ParseField HAS_HEADER_ROW = new ParseField("has_header_row"); - static final ParseField SEPARATOR = new ParseField("separator"); + static final ParseField DELIMITER = new ParseField("delimiter"); static final ParseField SHOULD_TRIM_FIELDS = new ParseField("should_trim_fields"); static final ParseField GROK_PATTERN = new ParseField("grok_pattern"); static final ParseField TIMESTAMP_FIELD = new ParseField("timestamp_field"); @@ -171,7 +110,7 @@ public String toString() { PARSER.declareString(Builder::setExcludeLinesPattern, EXCLUDE_LINES_PATTERN); PARSER.declareStringArray(Builder::setInputFields, INPUT_FIELDS); PARSER.declareBoolean(Builder::setHasHeaderRow, HAS_HEADER_ROW); - PARSER.declareString((p, c) -> p.setSeparator(c.charAt(0)), SEPARATOR); + PARSER.declareString((p, c) -> p.setDelimiter(c.charAt(0)), DELIMITER); PARSER.declareBoolean(Builder::setShouldTrimFields, SHOULD_TRIM_FIELDS); PARSER.declareString(Builder::setGrokPattern, GROK_PATTERN); PARSER.declareString(Builder::setTimestampField, TIMESTAMP_FIELD); @@ -191,7 +130,7 @@ public String toString() { private final String excludeLinesPattern; private final List inputFields; private final Boolean hasHeaderRow; - private final Character separator; + private final Character delimiter; private final Boolean shouldTrimFields; private final String grokPattern; private final List timestampFormats; @@ -202,7 +141,7 @@ public String toString() { public LogStructure(int numLinesAnalyzed, int numMessagesAnalyzed, String sampleStart, String charset, Boolean hasByteOrderMarker, Format format, String multilineStartPattern, String excludeLinesPattern, List inputFields, - Boolean hasHeaderRow, Character separator, Boolean shouldTrimFields, String grokPattern, String timestampField, + Boolean hasHeaderRow, Character delimiter, Boolean shouldTrimFields, String grokPattern, String timestampField, List timestampFormats, boolean needClientTimezone, Map mappings, List explanation) { @@ -216,7 +155,7 @@ public LogStructure(int numLinesAnalyzed, int numMessagesAnalyzed, String sample this.excludeLinesPattern = excludeLinesPattern; this.inputFields = (inputFields == null) ? null : Collections.unmodifiableList(new ArrayList<>(inputFields)); this.hasHeaderRow = hasHeaderRow; - this.separator = separator; + this.delimiter = delimiter; this.shouldTrimFields = shouldTrimFields; this.grokPattern = grokPattern; this.timestampField = timestampField; @@ -266,8 +205,8 @@ public Boolean getHasHeaderRow() { return hasHeaderRow; } - public Character getSeparator() { - return separator; + public Character getDelimiter() { + return delimiter; } public Boolean getShouldTrimFields() { @@ -322,8 +261,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (hasHeaderRow != null) { builder.field(HAS_HEADER_ROW.getPreferredName(), hasHeaderRow.booleanValue()); } - if (separator != null) { - builder.field(SEPARATOR.getPreferredName(), String.valueOf(separator)); + if (delimiter != null) { + builder.field(DELIMITER.getPreferredName(), String.valueOf(delimiter)); } if (shouldTrimFields != null) { builder.field(SHOULD_TRIM_FIELDS.getPreferredName(), shouldTrimFields.booleanValue()); @@ -349,7 +288,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public int hashCode() { return Objects.hash(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, inputFields, hasHeaderRow, separator, shouldTrimFields, grokPattern, timestampField, + multilineStartPattern, excludeLinesPattern, inputFields, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, timestampField, timestampFormats, needClientTimezone, mappings, explanation); } @@ -376,7 +315,7 @@ public boolean equals(Object other) { Objects.equals(this.excludeLinesPattern, that.excludeLinesPattern) && Objects.equals(this.inputFields, that.inputFields) && Objects.equals(this.hasHeaderRow, that.hasHeaderRow) && - Objects.equals(this.separator, that.separator) && + Objects.equals(this.delimiter, that.delimiter) && Objects.equals(this.shouldTrimFields, that.shouldTrimFields) && Objects.equals(this.grokPattern, that.grokPattern) && Objects.equals(this.timestampField, that.timestampField) && @@ -397,7 +336,7 @@ public static class Builder { private String excludeLinesPattern; private List inputFields; private Boolean hasHeaderRow; - private Character separator; + private Character delimiter; private Boolean shouldTrimFields; private String grokPattern; private String timestampField; @@ -441,7 +380,6 @@ public Builder setHasByteOrderMarker(Boolean hasByteOrderMarker) { public Builder setFormat(Format format) { this.format = Objects.requireNonNull(format); - this.separator = format.separator(); return this; } @@ -465,13 +403,13 @@ public Builder setHasHeaderRow(Boolean hasHeaderRow) { return this; } - public Builder setShouldTrimFields(Boolean shouldTrimFields) { - this.shouldTrimFields = shouldTrimFields; + public Builder setDelimiter(Character delimiter) { + this.delimiter = delimiter; return this; } - public Builder setSeparator(Character separator) { - this.separator = separator; + public Builder setShouldTrimFields(Boolean shouldTrimFields) { + this.shouldTrimFields = shouldTrimFields; return this; } @@ -542,28 +480,22 @@ public LogStructure build() { if (hasHeaderRow != null) { throw new IllegalArgumentException("Has header row may not be specified for [" + format + "] structures."); } - if (separator != null) { - throw new IllegalArgumentException("Separator may not be specified for [" + format + "] structures."); + if (delimiter != null) { + throw new IllegalArgumentException("Delimiter may not be specified for [" + format + "] structures."); } if (grokPattern != null) { throw new IllegalArgumentException("Grok pattern may not be specified for [" + format + "] structures."); } break; - case CSV: - case TSV: - case SEMI_COLON_SEPARATED_VALUES: - case PIPE_SEPARATED_VALUES: + case DELIMITED: if (inputFields == null || inputFields.isEmpty()) { throw new IllegalArgumentException("Input fields must be specified for [" + format + "] structures."); } if (hasHeaderRow == null) { throw new IllegalArgumentException("Has header row must be specified for [" + format + "] structures."); } - Character expectedSeparator = format.separator(); - assert expectedSeparator != null; - if (expectedSeparator.equals(separator) == false) { - throw new IllegalArgumentException("Separator must be [" + expectedSeparator + "] for [" + format + - "] structures."); + if (delimiter == null) { + throw new IllegalArgumentException("Delimiter must be specified for [" + format + "] structures."); } if (grokPattern != null) { throw new IllegalArgumentException("Grok pattern may not be specified for [" + format + "] structures."); @@ -576,8 +508,8 @@ public LogStructure build() { if (hasHeaderRow != null) { throw new IllegalArgumentException("Has header row may not be specified for [" + format + "] structures."); } - if (separator != null) { - throw new IllegalArgumentException("Separator may not be specified for [" + format + "] structures."); + if (delimiter != null) { + throw new IllegalArgumentException("Delimiter may not be specified for [" + format + "] structures."); } if (shouldTrimFields != null) { throw new IllegalArgumentException("Should trim fields may not be specified for [" + format + "] structures."); @@ -607,7 +539,7 @@ public LogStructure build() { } return new LogStructure(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, inputFields, hasHeaderRow, separator, shouldTrimFields, grokPattern, + multilineStartPattern, excludeLinesPattern, inputFields, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, timestampField, timestampFormats, needClientTimezone, mappings, explanation); } } diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java index a8fd9d7eb895..e747a588dfd8 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java +++ b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManager.java @@ -69,10 +69,10 @@ public final class LogStructureFinderManager { new JsonLogStructureFinderFactory(), new XmlLogStructureFinderFactory(), // ND-JSON will often also be valid (although utterly weird) CSV, so JSON must come before CSV - new CsvLogStructureFinderFactory(), - new TsvLogStructureFinderFactory(), - new SemiColonSeparatedValuesLogStructureFinderFactory(), - new PipeSeparatedValuesLogStructureFinderFactory(), + new DelimitedLogStructureFinderFactory(',', 2, false), + new DelimitedLogStructureFinderFactory('\t', 2, false), + new DelimitedLogStructureFinderFactory(';', 4, false), + new DelimitedLogStructureFinderFactory('|', 5, true), new TextLogStructureFinderFactory() )); diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureUtils.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureUtils.java index b1dfee22ee64..71a68c399910 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureUtils.java +++ b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureUtils.java @@ -21,12 +21,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -final class LogStructureUtils { +public final class LogStructureUtils { - static final String DEFAULT_TIMESTAMP_FIELD = "@timestamp"; - static final String MAPPING_TYPE_SETTING = "type"; - static final String MAPPING_FORMAT_SETTING = "format"; - static final String MAPPING_PROPERTIES_SETTING = "properties"; + public static final String DEFAULT_TIMESTAMP_FIELD = "@timestamp"; + public static final String MAPPING_TYPE_SETTING = "type"; + public static final String MAPPING_FORMAT_SETTING = "format"; + public static final String MAPPING_PROPERTIES_SETTING = "properties"; // NUMBER Grok pattern doesn't support scientific notation, so we extend it private static final Grok NUMBER_GROK = new Grok(Grok.getBuiltinPatterns(), "^%{NUMBER}(?:[eE][+-]?[0-3]?[0-9]{1,2})?$"); diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactory.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactory.java deleted file mode 100644 index 085599de847f..000000000000 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactory.java +++ /dev/null @@ -1,38 +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.ml.logstructurefinder; - -import org.supercsv.prefs.CsvPreference; - -import java.io.IOException; -import java.util.List; - -public class PipeSeparatedValuesLogStructureFinderFactory implements LogStructureFinderFactory { - - private static final CsvPreference PIPE_PREFERENCE = new CsvPreference.Builder('"', '|', "\n").build(); - - /** - * Rules are: - * - The file must be valid pipe (|) separated values - * - It must contain at least two complete records - * - There must be at least five fields per record (otherwise files with coincidental - * or no pipe characters could be treated as pipe separated) - * - Every pipe separated value record except the last must have the same number of fields - * The reason the last record is allowed to have fewer fields than the others is that - * it could have been truncated when the file was sampled. - */ - @Override - public boolean canCreateFromSample(List explanation, String sample) { - return SeparatedValuesLogStructureFinder.canCreateFromSample(explanation, sample, 5, PIPE_PREFERENCE, "pipe separated values"); - } - - @Override - public LogStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws IOException { - return SeparatedValuesLogStructureFinder.makeSeparatedValuesLogStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, - PIPE_PREFERENCE, true); - } -} diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactory.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactory.java deleted file mode 100644 index e0e80fa7465b..000000000000 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactory.java +++ /dev/null @@ -1,37 +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.ml.logstructurefinder; - -import org.supercsv.prefs.CsvPreference; - -import java.io.IOException; -import java.util.List; - -public class SemiColonSeparatedValuesLogStructureFinderFactory implements LogStructureFinderFactory { - - /** - * Rules are: - * - The file must be valid semi-colon separated values - * - It must contain at least two complete records - * - There must be at least four fields per record (otherwise files with coincidental - * or no semi-colons could be treated as semi-colon separated) - * - Every semi-colon separated value record except the last must have the same number of fields - * The reason the last record is allowed to have fewer fields than the others is that - * it could have been truncated when the file was sampled. - */ - @Override - public boolean canCreateFromSample(List explanation, String sample) { - return SeparatedValuesLogStructureFinder.canCreateFromSample(explanation, sample, 4, - CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE, "semi-colon separated values"); - } - - @Override - public LogStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws IOException { - return SeparatedValuesLogStructureFinder.makeSeparatedValuesLogStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, - CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE, false); - } -} diff --git a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactory.java b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactory.java index 733b32346fbe..1b53a33f31ee 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactory.java +++ b/x-pack/plugin/ml/log-structure-finder/src/main/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactory.java @@ -23,13 +23,13 @@ public class TsvLogStructureFinderFactory implements LogStructureFinderFactory { */ @Override public boolean canCreateFromSample(List explanation, String sample) { - return SeparatedValuesLogStructureFinder.canCreateFromSample(explanation, sample, 2, CsvPreference.TAB_PREFERENCE, "TSV"); + return DelimitedLogStructureFinder.canCreateFromSample(explanation, sample, 2, CsvPreference.TAB_PREFERENCE, "TSV"); } @Override public LogStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) throws IOException { - return SeparatedValuesLogStructureFinder.makeSeparatedValuesLogStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, + return DelimitedLogStructureFinder.makeDelimitedLogStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, CsvPreference.TAB_PREFERENCE, false); } } diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactoryTests.java deleted file mode 100644 index f53ee008d691..000000000000 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/CsvLogStructureFinderFactoryTests.java +++ /dev/null @@ -1,38 +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.ml.logstructurefinder; - -public class CsvLogStructureFinderFactoryTests extends LogStructureTestCase { - - private LogStructureFinderFactory factory = new CsvLogStructureFinderFactory(); - - // No need to check JSON or XML because they come earlier in the order we check formats - - public void testCanCreateFromSampleGivenCsv() { - - assertTrue(factory.canCreateFromSample(explanation, CSV_SAMPLE)); - } - - public void testCanCreateFromSampleGivenTsv() { - - assertFalse(factory.canCreateFromSample(explanation, TSV_SAMPLE)); - } - - public void testCanCreateFromSampleGivenSemiColonSeparatedValues() { - - assertFalse(factory.canCreateFromSample(explanation, SEMI_COLON_SEPARATED_VALUES_SAMPLE)); - } - - public void testCanCreateFromSampleGivenPipeSeparatedValues() { - - assertFalse(factory.canCreateFromSample(explanation, PIPE_SEPARATED_VALUES_SAMPLE)); - } - - public void testCanCreateFromSampleGivenText() { - - assertFalse(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); - } -} diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactoryTests.java new file mode 100644 index 000000000000..d9eadbc8f0fd --- /dev/null +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderFactoryTests.java @@ -0,0 +1,93 @@ +/* + * 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.logstructurefinder; + +public class DelimitedLogStructureFinderFactoryTests extends LogStructureTestCase { + + private LogStructureFinderFactory csvFactory = new DelimitedLogStructureFinderFactory(',', 2, false); + private LogStructureFinderFactory tsvFactory = new DelimitedLogStructureFinderFactory('\t', 2, false); + private LogStructureFinderFactory semiColonDelimitedfactory = new DelimitedLogStructureFinderFactory(';', 4, false); + private LogStructureFinderFactory pipeDelimitedFactory = new DelimitedLogStructureFinderFactory('|', 5, true); + + // CSV - no need to check JSON or XML because they come earlier in the order we check formats + + public void testCanCreateCsvFromSampleGivenCsv() { + + assertTrue(csvFactory.canCreateFromSample(explanation, CSV_SAMPLE)); + } + + public void testCanCreateCsvFromSampleGivenTsv() { + + assertFalse(csvFactory.canCreateFromSample(explanation, TSV_SAMPLE)); + } + + public void testCanCreateCsvFromSampleGivenSemiColonDelimited() { + + assertFalse(csvFactory.canCreateFromSample(explanation, SEMI_COLON_DELIMITED_SAMPLE)); + } + + public void testCanCreateCsvFromSampleGivenPipeDelimited() { + + assertFalse(csvFactory.canCreateFromSample(explanation, PIPE_DELIMITED_SAMPLE)); + } + + public void testCanCreateCsvFromSampleGivenText() { + + assertFalse(csvFactory.canCreateFromSample(explanation, TEXT_SAMPLE)); + } + + // TSV - no need to check JSON, XML or CSV because they come earlier in the order we check formats + + public void testCanCreateTsvFromSampleGivenTsv() { + + assertTrue(tsvFactory.canCreateFromSample(explanation, TSV_SAMPLE)); + } + + public void testCanCreateTsvFromSampleGivenSemiColonDelimited() { + + assertFalse(tsvFactory.canCreateFromSample(explanation, SEMI_COLON_DELIMITED_SAMPLE)); + } + + public void testCanCreateTsvFromSampleGivenPipeDelimited() { + + assertFalse(tsvFactory.canCreateFromSample(explanation, PIPE_DELIMITED_SAMPLE)); + } + + public void testCanCreateTsvFromSampleGivenText() { + + assertFalse(tsvFactory.canCreateFromSample(explanation, TEXT_SAMPLE)); + } + + // Semi-colon delimited - no need to check JSON, XML, CSV or TSV because they come earlier in the order we check formats + + public void testCanCreateSemiColonDelimitedFromSampleGivenSemiColonDelimited() { + + assertTrue(semiColonDelimitedfactory.canCreateFromSample(explanation, SEMI_COLON_DELIMITED_SAMPLE)); + } + + public void testCanCreateSemiColonDelimitedFromSampleGivenPipeDelimited() { + + assertFalse(semiColonDelimitedfactory.canCreateFromSample(explanation, PIPE_DELIMITED_SAMPLE)); + } + + public void testCanCreateSemiColonDelimitedFromSampleGivenText() { + + assertFalse(semiColonDelimitedfactory.canCreateFromSample(explanation, TEXT_SAMPLE)); + } + + // Pipe delimited - no need to check JSON, XML, CSV, TSV or semi-colon delimited + // values because they come earlier in the order we check formats + + public void testCanCreatePipeDelimitedFromSampleGivenPipeDelimited() { + + assertTrue(pipeDelimitedFactory.canCreateFromSample(explanation, PIPE_DELIMITED_SAMPLE)); + } + + public void testCanCreatePipeDelimitedFromSampleGivenText() { + + assertFalse(pipeDelimitedFactory.canCreateFromSample(explanation, TEXT_SAMPLE)); + } +} diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/SeparatedValuesLogStructureFinderTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderTests.java similarity index 65% rename from x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/SeparatedValuesLogStructureFinderTests.java rename to x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderTests.java index b62832a0a19c..57c297cf8d57 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/SeparatedValuesLogStructureFinderTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/DelimitedLogStructureFinderTests.java @@ -12,27 +12,27 @@ import java.util.Arrays; import java.util.Collections; -import static org.elasticsearch.xpack.ml.logstructurefinder.SeparatedValuesLogStructureFinder.levenshteinFieldwiseCompareRows; -import static org.elasticsearch.xpack.ml.logstructurefinder.SeparatedValuesLogStructureFinder.levenshteinDistance; +import static org.elasticsearch.xpack.ml.logstructurefinder.DelimitedLogStructureFinder.levenshteinFieldwiseCompareRows; +import static org.elasticsearch.xpack.ml.logstructurefinder.DelimitedLogStructureFinder.levenshteinDistance; import static org.hamcrest.Matchers.arrayContaining; -public class SeparatedValuesLogStructureFinderTests extends LogStructureTestCase { +public class DelimitedLogStructureFinderTests extends LogStructureTestCase { - private LogStructureFinderFactory factory = new CsvLogStructureFinderFactory(); + private LogStructureFinderFactory csvFactory = new DelimitedLogStructureFinderFactory(',', 2, false); public void testCreateConfigsGivenCompleteCsv() throws Exception { String sample = "time,message\n" + "2018-05-17T13:41:23,hello\n" + "2018-05-17T13:41:32,hello again\n"; - assertTrue(factory.canCreateFromSample(explanation, sample)); + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - LogStructureFinder structureFinder = factory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + LogStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); LogStructure structure = structureFinder.getStructure(); - assertEquals(LogStructure.Format.CSV, structure.getFormat()); + assertEquals(LogStructure.Format.DELIMITED, structure.getFormat()); assertEquals(charset, structure.getCharset()); if (hasByteOrderMarker == null) { assertNull(structure.getHasByteOrderMarker()); @@ -41,7 +41,7 @@ public void testCreateConfigsGivenCompleteCsv() throws Exception { } 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.getSeparator()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("time", "message"), structure.getInputFields()); @@ -55,15 +55,15 @@ public void testCreateConfigsGivenCsvWithIncompleteLastRecord() throws Exception "\"hello\n" + "world\",2018-05-17T13:41:23,1\n" + "\"hello again\n"; // note that this last record is truncated - assertTrue(factory.canCreateFromSample(explanation, sample)); + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - LogStructureFinder structureFinder = factory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + LogStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); LogStructure structure = structureFinder.getStructure(); - assertEquals(LogStructure.Format.CSV, structure.getFormat()); + assertEquals(LogStructure.Format.DELIMITED, structure.getFormat()); assertEquals(charset, structure.getCharset()); if (hasByteOrderMarker == null) { assertNull(structure.getHasByteOrderMarker()); @@ -72,7 +72,7 @@ public void testCreateConfigsGivenCsvWithIncompleteLastRecord() throws Exception } 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.getSeparator()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("message", "time", "count"), structure.getInputFields()); @@ -88,15 +88,15 @@ public void testCreateConfigsGivenCsvWithTrailingNulls() throws Exception { "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(factory.canCreateFromSample(explanation, sample)); + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - LogStructureFinder structureFinder = factory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + LogStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); LogStructure structure = structureFinder.getStructure(); - assertEquals(LogStructure.Format.CSV, structure.getFormat()); + assertEquals(LogStructure.Format.DELIMITED, structure.getFormat()); assertEquals(charset, structure.getCharset()); if (hasByteOrderMarker == null) { assertNull(structure.getHasByteOrderMarker()); @@ -108,7 +108,7 @@ public void testCreateConfigsGivenCsvWithTrailingNulls() throws Exception { "\"?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.getSeparator()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", @@ -126,15 +126,15 @@ public void testCreateConfigsGivenCsvWithTrailingNullsExceptHeader() throws Exce "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(factory.canCreateFromSample(explanation, sample)); + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - LogStructureFinder structureFinder = factory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + LogStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); LogStructure structure = structureFinder.getStructure(); - assertEquals(LogStructure.Format.CSV, structure.getFormat()); + assertEquals(LogStructure.Format.DELIMITED, structure.getFormat()); assertEquals(charset, structure.getCharset()); if (hasByteOrderMarker == null) { assertNull(structure.getHasByteOrderMarker()); @@ -146,7 +146,7 @@ public void testCreateConfigsGivenCsvWithTrailingNullsExceptHeader() throws Exce "\"?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.getSeparator()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", @@ -161,15 +161,15 @@ 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" + "\"2\",\"3\",\"4703.7815\",\"1527.4714\",\"359.9\",\"2017-01-19 16:19:05.741890\"\n"; - assertTrue(factory.canCreateFromSample(explanation, sample)); + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - LogStructureFinder structureFinder = factory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + LogStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); LogStructure structure = structureFinder.getStructure(); - assertEquals(LogStructure.Format.CSV, structure.getFormat()); + assertEquals(LogStructure.Format.DELIMITED, structure.getFormat()); assertEquals(charset, structure.getCharset()); if (hasByteOrderMarker == null) { assertNull(structure.getHasByteOrderMarker()); @@ -179,7 +179,7 @@ public void testCreateConfigsGivenCsvWithTimeLastColumn() throws Exception { assertEquals("^\"?pos_id\"?,\"?trip_id\"?,\"?latitude\"?,\"?longitude\"?,\"?altitude\"?,\"?timestamp\"?", structure.getExcludeLinesPattern()); assertNull(structure.getMultilineStartPattern()); - assertEquals(Character.valueOf(','), structure.getSeparator()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("pos_id", "trip_id", "latitude", "longitude", "altitude", "timestamp"), structure.getInputFields()); @@ -195,8 +195,8 @@ public void testFindHeaderFromSampleGivenHeaderInSample() throws IOException { "2014-06-23 00:00:01Z,JBU,877.5927,farequote\n" + "2014-06-23 00:00:01Z,KLM,1355.4812,farequote\n"; - Tuple header = SeparatedValuesLogStructureFinder.findHeaderFromSample(explanation, - SeparatedValuesLogStructureFinder.readRows(withHeader, CsvPreference.EXCEL_PREFERENCE).v1()); + Tuple header = DelimitedLogStructureFinder.findHeaderFromSample(explanation, + DelimitedLogStructureFinder.readRows(withHeader, CsvPreference.EXCEL_PREFERENCE).v1()); assertTrue(header.v1()); assertThat(header.v2(), arrayContaining("time", "airline", "responsetime", "sourcetype")); @@ -208,8 +208,8 @@ public void testFindHeaderFromSampleGivenHeaderNotInSample() throws IOException "2014-06-23 00:00:01Z,JBU,877.5927,farequote\n" + "2014-06-23 00:00:01Z,KLM,1355.4812,farequote\n"; - Tuple header = SeparatedValuesLogStructureFinder.findHeaderFromSample(explanation, - SeparatedValuesLogStructureFinder.readRows(withoutHeader, CsvPreference.EXCEL_PREFERENCE).v1()); + Tuple header = DelimitedLogStructureFinder.findHeaderFromSample(explanation, + DelimitedLogStructureFinder.readRows(withoutHeader, CsvPreference.EXCEL_PREFERENCE).v1()); assertFalse(header.v1()); assertThat(header.v2(), arrayContaining("column1", "column2", "column3", "column4")); @@ -251,43 +251,43 @@ public void testLevenshteinCompareRows() { public void testLineHasUnescapedQuote() { - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a,b,c", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a\",b,c", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a,b\",c", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a,b,c\"", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a,\"b\",c", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a,b,\"c\"", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a,\"b\"\"\",c", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a,b,\"c\"\"\"", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"\"\"a\",b,c", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a\"\"\",b,c", CsvPreference.EXCEL_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a,\"\"b\",c", CsvPreference.EXCEL_PREFERENCE)); - assertTrue(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("between\"words,b,c", CsvPreference.EXCEL_PREFERENCE)); - assertTrue(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("x and \"y\",b,c", CsvPreference.EXCEL_PREFERENCE)); - - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a\tb\tc", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a\"\tb\tc", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a\tb\"\tc", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a\tb\tc\"", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a\t\"b\"\tc", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a\tb\t\"c\"", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a\t\"b\"\"\"\tc", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("a\tb\t\"c\"\"\"", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"\"\"a\"\tb\tc", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a\"\"\"\tb\tc", CsvPreference.TAB_PREFERENCE)); - assertFalse(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("\"a\t\"\"b\"\tc", CsvPreference.TAB_PREFERENCE)); - assertTrue(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("between\"words\tb\tc", CsvPreference.TAB_PREFERENCE)); - assertTrue(SeparatedValuesLogStructureFinder.lineHasUnescapedQuote("x and \"y\"\tb\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a,b,c", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a\",b,c", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a,b\",c", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a,b,c\"", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a,\"b\",c", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a,b,\"c\"", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a,\"b\"\"\",c", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a,b,\"c\"\"\"", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"\"\"a\",b,c", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a\"\"\",b,c", CsvPreference.EXCEL_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a,\"\"b\",c", CsvPreference.EXCEL_PREFERENCE)); + assertTrue(DelimitedLogStructureFinder.lineHasUnescapedQuote("between\"words,b,c", CsvPreference.EXCEL_PREFERENCE)); + assertTrue(DelimitedLogStructureFinder.lineHasUnescapedQuote("x and \"y\",b,c", CsvPreference.EXCEL_PREFERENCE)); + + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a\tb\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a\"\tb\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a\tb\"\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a\tb\tc\"", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a\t\"b\"\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a\tb\t\"c\"", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a\t\"b\"\"\"\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("a\tb\t\"c\"\"\"", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"\"\"a\"\tb\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a\"\"\"\tb\tc", CsvPreference.TAB_PREFERENCE)); + assertFalse(DelimitedLogStructureFinder.lineHasUnescapedQuote("\"a\t\"\"b\"\tc", CsvPreference.TAB_PREFERENCE)); + assertTrue(DelimitedLogStructureFinder.lineHasUnescapedQuote("between\"words\tb\tc", CsvPreference.TAB_PREFERENCE)); + assertTrue(DelimitedLogStructureFinder.lineHasUnescapedQuote("x and \"y\"\tb\tc", CsvPreference.TAB_PREFERENCE)); } public void testRowContainsDuplicateNonEmptyValues() { - assertFalse(SeparatedValuesLogStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList("a"))); - assertFalse(SeparatedValuesLogStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList(""))); - assertFalse(SeparatedValuesLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "c"))); - assertTrue(SeparatedValuesLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "a"))); - assertTrue(SeparatedValuesLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "b"))); - assertFalse(SeparatedValuesLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "", ""))); - assertFalse(SeparatedValuesLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("", "a", ""))); + assertFalse(DelimitedLogStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList("a"))); + assertFalse(DelimitedLogStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList(""))); + assertFalse(DelimitedLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "c"))); + assertTrue(DelimitedLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "a"))); + assertTrue(DelimitedLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "b"))); + assertFalse(DelimitedLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "", ""))); + assertFalse(DelimitedLogStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("", "a", ""))); } } diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderFactoryTests.java index 39ef3b9eedbb..cdbffa8259e0 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderFactoryTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderFactoryTests.java @@ -29,14 +29,14 @@ public void testCanCreateFromSampleGivenTsv() { assertFalse(factory.canCreateFromSample(explanation, TSV_SAMPLE)); } - public void testCanCreateFromSampleGivenSemiColonSeparatedValues() { + public void testCanCreateFromSampleGivenSemiColonDelimited() { - assertFalse(factory.canCreateFromSample(explanation, SEMI_COLON_SEPARATED_VALUES_SAMPLE)); + assertFalse(factory.canCreateFromSample(explanation, SEMI_COLON_DELIMITED_SAMPLE)); } - public void testCanCreateFromSampleGivenPipeSeparatedValues() { + public void testCanCreateFromSampleGivenPipeDelimited() { - assertFalse(factory.canCreateFromSample(explanation, PIPE_SEPARATED_VALUES_SAMPLE)); + assertFalse(factory.canCreateFromSample(explanation, PIPE_DELIMITED_SAMPLE)); } public void testCanCreateFromSampleGivenText() { diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderTests.java index 2f727747bbff..917054919dd5 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/JsonLogStructureFinderTests.java @@ -29,7 +29,7 @@ public void testCreateConfigsGivenGoodJson() throws Exception { } assertNull(structure.getExcludeLinesPattern()); assertNull(structure.getMultilineStartPattern()); - assertNull(structure.getSeparator()); + assertNull(structure.getDelimiter()); assertNull(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertNull(structure.getGrokPattern()); diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManagerTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManagerTests.java index 1f8691de8cf6..520a55510c7a 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManagerTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureFinderManagerTests.java @@ -61,7 +61,7 @@ public void testMakeBestStructureGivenXml() throws Exception { 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(SeparatedValuesLogStructureFinder.class)); + instanceOf(DelimitedLogStructureFinder.class)); } public void testMakeBestStructureGivenText() throws Exception { diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTestCase.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTestCase.java index 5f9a87ef2a7f..6b718fef6c7e 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTestCase.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTestCase.java @@ -34,14 +34,14 @@ public abstract class LogStructureTestCase extends ESTestCase { "\"level\":\"INFO\",\"pid\":42,\"thread\":\"0x7fff7d2a8000\",\"message\":\"message 2\",\"class\":\"ml\"," + "\"method\":\"core::SomeNoiseMaker\",\"file\":\"Noisemaker.cc\",\"line\":333}\n"; - protected static final String PIPE_SEPARATED_VALUES_SAMPLE = "2018-01-06 16:56:14.295748|INFO |VirtualServer |1 |" + + protected static final String PIPE_DELIMITED_SAMPLE = "2018-01-06 16:56:14.295748|INFO |VirtualServer |1 |" + "listening on 0.0.0.0:9987, :::9987\n" + "2018-01-06 17:19:44.465252|INFO |VirtualServer |1 |client " + "'User1'(id:2) changed default admin channelgroup to 'Guest'(id:8)\n" + "2018-01-06 17:21:25.764368|INFO |VirtualServer |1 |client " + "'User1'(id:2) was added to channelgroup 'Channel Admin'(id:5) by client 'User1'(id:2) in channel 'Default Channel'(id:1)"; - protected static final String SEMI_COLON_SEPARATED_VALUES_SAMPLE = "\"pos_id\";\"trip_id\";\"latitude\";\"longitude\";\"altitude\";" + + protected static final String SEMI_COLON_DELIMITED_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" + "\"2\";\"3\";\"4703.7815\";\"1527.4714\";\"359.9\";\"2017-01-19 16:19:05.741890\"\n" + diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTests.java index 738928ed28a3..302946dcaa86 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/LogStructureTests.java @@ -43,14 +43,12 @@ protected LogStructure createTestInstance() { builder.setExcludeLinesPattern(randomAlphaOfLength(100)); } - if (format.isSeparatedValues() || (format.supportsNesting() && randomBoolean())) { + if (format == LogStructure.Format.DELIMITED || (format.supportsNesting() && randomBoolean())) { builder.setInputFields(Arrays.asList(generateRandomStringArray(10, 10, false, false))); } - if (format.isSeparatedValues()) { + if (format == LogStructure.Format.DELIMITED) { builder.setHasHeaderRow(randomBoolean()); - if (rarely()) { - builder.setSeparator(format.separator()); - } + builder.setDelimiter(randomFrom(',', '\t', ';', '|')); } if (format.isSemiStructured()) { builder.setGrokPattern(randomAlphaOfLength(100)); diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactoryTests.java deleted file mode 100644 index 3fd2fb7840ac..000000000000 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/PipeSeparatedValuesLogStructureFinderFactoryTests.java +++ /dev/null @@ -1,23 +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.ml.logstructurefinder; - -public class PipeSeparatedValuesLogStructureFinderFactoryTests extends LogStructureTestCase { - - private LogStructureFinderFactory factory = new PipeSeparatedValuesLogStructureFinderFactory(); - - // No need to check JSON, XML, CSV, TSV or semi-colon separated values because they come earlier in the order we check formats - - public void testCanCreateFromSampleGivenPipeSeparatedValues() { - - assertTrue(factory.canCreateFromSample(explanation, PIPE_SEPARATED_VALUES_SAMPLE)); - } - - public void testCanCreateFromSampleGivenText() { - - assertFalse(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); - } -} diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactoryTests.java deleted file mode 100644 index 64dad7e078cd..000000000000 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/SemiColonSeparatedValuesLogStructureFinderFactoryTests.java +++ /dev/null @@ -1,28 +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.ml.logstructurefinder; - -public class SemiColonSeparatedValuesLogStructureFinderFactoryTests extends LogStructureTestCase { - - private LogStructureFinderFactory factory = new SemiColonSeparatedValuesLogStructureFinderFactory(); - - // No need to check JSON, XML, CSV or TSV because they come earlier in the order we check formats - - public void testCanCreateFromSampleGivenSemiColonSeparatedValues() { - - assertTrue(factory.canCreateFromSample(explanation, SEMI_COLON_SEPARATED_VALUES_SAMPLE)); - } - - public void testCanCreateFromSampleGivenPipeSeparatedValues() { - - assertFalse(factory.canCreateFromSample(explanation, PIPE_SEPARATED_VALUES_SAMPLE)); - } - - public void testCanCreateFromSampleGivenText() { - - assertFalse(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); - } -} diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderFactoryTests.java index 267ce375d6e9..c1b30cc74961 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderFactoryTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderFactoryTests.java @@ -9,8 +9,8 @@ public class TextLogStructureFinderFactoryTests extends LogStructureTestCase { private LogStructureFinderFactory factory = new TextLogStructureFinderFactory(); - // No need to check JSON, XML, CSV, TSV, semi-colon separated values or pipe - // separated values because they come earlier in the order we check formats + // No need to check JSON, XML, CSV, TSV, semi-colon delimited values or pipe + // delimited values because they come earlier in the order we check formats public void testCanCreateFromSampleGivenText() { diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderTests.java index 7c6a58bb6838..c9e153a82c43 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TextLogStructureFinderTests.java @@ -34,7 +34,7 @@ public void testCreateConfigsGivenElasticsearchLog() throws Exception { } assertNull(structure.getExcludeLinesPattern()); assertEquals("^\\[\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); - assertNull(structure.getSeparator()); + assertNull(structure.getDelimiter()); assertNull(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals("\\[%{TIMESTAMP_ISO8601:timestamp}\\]\\[%{LOGLEVEL:loglevel} \\]\\[.*", structure.getGrokPattern()); diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactoryTests.java deleted file mode 100644 index 1c8acc14d328..000000000000 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/TsvLogStructureFinderFactoryTests.java +++ /dev/null @@ -1,33 +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.ml.logstructurefinder; - -public class TsvLogStructureFinderFactoryTests extends LogStructureTestCase { - - private LogStructureFinderFactory factory = new TsvLogStructureFinderFactory(); - - // No need to check JSON, XML or CSV because they come earlier in the order we check formats - - public void testCanCreateFromSampleGivenTsv() { - - assertTrue(factory.canCreateFromSample(explanation, TSV_SAMPLE)); - } - - public void testCanCreateFromSampleGivenSemiColonSeparatedValues() { - - assertFalse(factory.canCreateFromSample(explanation, SEMI_COLON_SEPARATED_VALUES_SAMPLE)); - } - - public void testCanCreateFromSampleGivenPipeSeparatedValues() { - - assertFalse(factory.canCreateFromSample(explanation, PIPE_SEPARATED_VALUES_SAMPLE)); - } - - public void testCanCreateFromSampleGivenText() { - - assertFalse(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); - } -} diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderFactoryTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderFactoryTests.java index 27eb4ede040b..b6dc3a56f1df 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderFactoryTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderFactoryTests.java @@ -26,14 +26,14 @@ public void testCanCreateFromSampleGivenTsv() { assertFalse(factory.canCreateFromSample(explanation, TSV_SAMPLE)); } - public void testCanCreateFromSampleGivenSemiColonSeparatedValues() { + public void testCanCreateFromSampleGivenSemiColonDelimited() { - assertFalse(factory.canCreateFromSample(explanation, SEMI_COLON_SEPARATED_VALUES_SAMPLE)); + assertFalse(factory.canCreateFromSample(explanation, SEMI_COLON_DELIMITED_SAMPLE)); } - public void testCanCreateFromSampleGivenPipeSeparatedValues() { + public void testCanCreateFromSampleGivenPipeDelimited() { - assertFalse(factory.canCreateFromSample(explanation, PIPE_SEPARATED_VALUES_SAMPLE)); + assertFalse(factory.canCreateFromSample(explanation, PIPE_DELIMITED_SAMPLE)); } public void testCanCreateFromSampleGivenText() { diff --git a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderTests.java b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderTests.java index 0d04df152ef0..de653d7bcd0c 100644 --- a/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderTests.java +++ b/x-pack/plugin/ml/log-structure-finder/src/test/java/org/elasticsearch/xpack/ml/logstructurefinder/XmlLogStructureFinderTests.java @@ -29,7 +29,7 @@ public void testCreateConfigsGivenGoodXml() throws Exception { } assertNull(structure.getExcludeLinesPattern()); assertEquals("^\\s* Date: Fri, 31 Aug 2018 13:08:32 +0300 Subject: [PATCH 257/283] Different handling for security specific errors in the CLI. Fix for https://github.com/elastic/elasticsearch/issues/33230 (#33255) --- docs/reference/sql/endpoints/cli.asciidoc | 9 +++++++++ .../main/java/org/elasticsearch/xpack/sql/cli/Cli.java | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/docs/reference/sql/endpoints/cli.asciidoc b/docs/reference/sql/endpoints/cli.asciidoc index 0908c2344bb1..eef2fbfbf596 100644 --- a/docs/reference/sql/endpoints/cli.asciidoc +++ b/docs/reference/sql/endpoints/cli.asciidoc @@ -22,6 +22,15 @@ the first parameter: $ ./bin/elasticsearch-sql-cli https://some.server:9200 -------------------------------------------------- +If security is enabled on your cluster, you can pass the username +and password in the form `username:password@host_name:port` +to the SQL CLI: + +[source,bash] +-------------------------------------------------- +$ ./bin/elasticsearch-sql-cli https://sql_user:strongpassword@some.server:9200 +-------------------------------------------------- + Once the CLI is running you can use any <> that Elasticsearch supports: diff --git a/x-pack/plugin/sql/sql-cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java b/x-pack/plugin/sql/sql-cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java index 357a4bcb5a77..6431f10a4921 100644 --- a/x-pack/plugin/sql/sql-cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java +++ b/x-pack/plugin/sql/sql-cli/src/main/java/org/elasticsearch/xpack/sql/cli/Cli.java @@ -27,6 +27,7 @@ import org.jline.terminal.TerminalBuilder; import java.io.IOException; import java.net.ConnectException; +import java.sql.SQLInvalidAuthorizationSpecException; import java.util.Arrays; import java.util.List; import java.util.logging.LogManager; @@ -139,6 +140,10 @@ private void checkConnection(CliSession cliSession, CliTerminal cliTerminal, Con // Most likely Elasticsearch is not running throw new UserException(ExitCodes.IO_ERROR, "Cannot connect to the server " + con.connectionString() + " - " + ex.getCause().getMessage()); + } else if (ex.getCause() != null && ex.getCause() instanceof SQLInvalidAuthorizationSpecException) { + throw new UserException(ExitCodes.NOPERM, + "Cannot establish a secure connection to the server " + + con.connectionString() + " - " + ex.getCause().getMessage()); } else { // Most likely we connected to something other than Elasticsearch throw new UserException(ExitCodes.DATA_ERROR, From a88f8789a0a9260de0774ee7b76446c2a792294f Mon Sep 17 00:00:00 2001 From: Pablo Musa Date: Fri, 31 Aug 2018 14:48:55 +0200 Subject: [PATCH 258/283] Highlight that index_phrases only works if no slop is used (#33303) Highlight that `index_phrases` only works if no slop is used at query time. --- docs/reference/mapping/types/text.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/mapping/types/text.asciidoc b/docs/reference/mapping/types/text.asciidoc index e2336bd5cb06..db64e87412e0 100644 --- a/docs/reference/mapping/types/text.asciidoc +++ b/docs/reference/mapping/types/text.asciidoc @@ -99,7 +99,7 @@ The following parameters are accepted by `text` fields: `index_phrases`:: If enabled, two-term word combinations ('shingles') are indexed into a separate - field. This allows exact phrase queries to run more efficiently, at the expense + field. This allows exact phrase queries (no slop) to run more efficiently, at the expense of a larger index. Note that this works best when stopwords are not removed, as phrases containing stopwords will not use the subsidiary field and will fall back to a standard phrase query. Accepts `true` or `false` (default). @@ -171,4 +171,4 @@ PUT my_index -------------------------------- // CONSOLE <1> `min_chars` must be greater than zero, defaults to 2 -<2> `max_chars` must be greater than or equal to `min_chars` and less than 20, defaults to 5 \ No newline at end of file +<2> `max_chars` must be greater than or equal to `min_chars` and less than 20, defaults to 5 From f6a570880c5c44adea4ad1f35e950c51335d8ac7 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 31 Aug 2018 16:01:54 +0300 Subject: [PATCH 259/283] Work around to be able to generate eclipse projects (#33295) * Work around to be able to generate eclipse projects https://github.com/gradle/gradle/issues/6582 --- build.gradle | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 36d3a543d89b..f8282ca5ae89 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ + import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin +import org.apache.tools.ant.taskdefs.condition.Os import org.elasticsearch.gradle.BuildPlugin import org.elasticsearch.gradle.LoggedExec import org.elasticsearch.gradle.Version @@ -24,14 +26,9 @@ import org.elasticsearch.gradle.VersionCollection import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.plugin.PluginBuildPlugin import org.gradle.plugins.ide.eclipse.model.SourceFolder -import org.gradle.util.GradleVersion -import org.gradle.util.DistributionLocator -import org.apache.tools.ant.taskdefs.condition.Os -import org.apache.tools.ant.filters.ReplaceTokens import java.nio.file.Files import java.nio.file.Path -import java.security.MessageDigest plugins { id 'com.gradle.build-scan' version '1.13.2' @@ -512,6 +509,16 @@ allprojects { tasks.cleanEclipse.dependsOn(wipeEclipseSettings) // otherwise the eclipse merging is *super confusing* tasks.eclipse.dependsOn(cleanEclipse, copyEclipseSettings) + + // work arround https://github.com/gradle/gradle/issues/6582 + tasks.eclipseProject.mustRunAfter tasks.cleanEclipseProject + tasks.matching { it.name == 'eclipseClasspath' }.all { + it.mustRunAfter { tasks.cleanEclipseClasspath } + } + tasks.matching { it.name == 'eclipseJdt' }.all { + it.mustRunAfter { tasks.cleanEclipseJdt } + } + tasks.copyEclipseSettings.mustRunAfter tasks.wipeEclipseSettings } allprojects { From 0c4b3162be7c2a6bd03a44ab15821ef49a776738 Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Fri, 31 Aug 2018 16:12:01 +0300 Subject: [PATCH 260/283] SQL: test coverage for JdbcResultSet (#32813) * Tests for JdbcResultSet * Added VARCHAR conversion for different types * Made error messages consistent: they now contain both the type that fails to be converted and the value itself --- .../xpack/sql/jdbc/jdbc/JdbcResultSet.java | 65 +- .../xpack/sql/jdbc/jdbc/TypeConverter.java | 87 +- .../jdbc/jdbc/JdbcPreparedStatementTests.java | 42 +- .../qa/sql/nosecurity/JdbcResultSetIT.java | 16 + .../xpack/qa/sql/jdbc/ResultSetTestCase.java | 1522 ++++++++++++++++- .../qa/sql/jdbc/SimpleExampleTestCase.java | 3 +- 6 files changed, 1624 insertions(+), 111 deletions(-) create mode 100644 x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/JdbcResultSetIT.java diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcResultSet.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcResultSet.java index 201ae251ca0d..ebdeaef15cae 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcResultSet.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcResultSet.java @@ -133,72 +133,37 @@ public String getString(int columnIndex) throws SQLException { @Override public boolean getBoolean(int columnIndex) throws SQLException { - Object val = column(columnIndex); - try { - return val != null ? (Boolean) val : false; - } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to a boolean", cce); - } + return column(columnIndex) != null ? getObject(columnIndex, Boolean.class) : false; } @Override public byte getByte(int columnIndex) throws SQLException { - Object val = column(columnIndex); - try { - return val != null ? ((Number) val).byteValue() : 0; - } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to a byte", cce); - } + return column(columnIndex) != null ? getObject(columnIndex, Byte.class) : 0; } @Override public short getShort(int columnIndex) throws SQLException { - Object val = column(columnIndex); - try { - return val != null ? ((Number) val).shortValue() : 0; - } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to a short", cce); - } + return column(columnIndex) != null ? getObject(columnIndex, Short.class) : 0; } @Override public int getInt(int columnIndex) throws SQLException { - Object val = column(columnIndex); - try { - return val != null ? ((Number) val).intValue() : 0; - } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to an int", cce); - } + return column(columnIndex) != null ? getObject(columnIndex, Integer.class) : 0; } @Override public long getLong(int columnIndex) throws SQLException { - Object val = column(columnIndex); - try { - return val != null ? ((Number) val).longValue() : 0; - } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to a long", cce); - } + return column(columnIndex) != null ? getObject(columnIndex, Long.class) : 0; } @Override public float getFloat(int columnIndex) throws SQLException { - Object val = column(columnIndex); - try { - return val != null ? ((Number) val).floatValue() : 0; - } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to a float", cce); - } + return column(columnIndex) != null ? getObject(columnIndex, Float.class) : 0; } @Override public double getDouble(int columnIndex) throws SQLException { - Object val = column(columnIndex); - try { - return val != null ? ((Number) val).doubleValue() : 0; - } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to a double", cce); - } + return column(columnIndex) != null ? getObject(columnIndex, Double.class) : 0; } @Override @@ -272,15 +237,29 @@ public byte[] getBytes(String columnLabel) throws SQLException { @Override public Date getDate(String columnLabel) throws SQLException { + // TODO: the error message in case the value in the column cannot be converted to a Date refers to a column index + // (for example - "unable to convert column 4 to a long") and not to the column name, which is a bit confusing. + // Should we reconsider this? Maybe by catching the exception here and rethrowing it with the columnLabel instead. return getDate(column(columnLabel)); } private Long dateTime(int columnIndex) throws SQLException { Object val = column(columnIndex); + JDBCType type = cursor.columns().get(columnIndex - 1).type; try { + // TODO: the B6 appendix of the jdbc spec does mention CHAR, VARCHAR, LONGVARCHAR, DATE, TIMESTAMP as supported + // jdbc types that should be handled by getDate and getTime methods. From all of those we support VARCHAR and + // TIMESTAMP. Should we consider the VARCHAR conversion as a later enhancement? + if (JDBCType.TIMESTAMP.equals(type)) { + // the cursor can return an Integer if the date-since-epoch is small enough, XContentParser (Jackson) will + // return the "smallest" data type for numbers when parsing + // TODO: this should probably be handled server side + return val == null ? null : ((Number) val).longValue(); + }; return val == null ? null : (Long) val; } catch (ClassCastException cce) { - throw new SQLException("unable to convert column " + columnIndex + " to a long", cce); + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Long", val, type.getName()), cce); } } diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java index 3b5180b71f7c..7b638d8bd094 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/TypeConverter.java @@ -10,7 +10,6 @@ import java.sql.Date; import java.sql.JDBCType; -import java.sql.SQLDataException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Time; @@ -56,9 +55,10 @@ private TypeConverter() { } - private static final long DAY_IN_MILLIS = 60 * 60 * 24; + private static final long DAY_IN_MILLIS = 60 * 60 * 24 * 1000; private static final Map, JDBCType> javaToJDBC; + static { Map, JDBCType> aMap = Arrays.stream(DataType.values()) .filter(dataType -> dataType.javaClass() != null @@ -120,6 +120,7 @@ private static T dateTimeConvert(Long millis, Calendar c, Function T convert(Object val, JDBCType columnType, Class type) throws SQLE return (T) convert(val, columnType); } - if (type.isInstance(val)) { + // converting a Long to a Timestamp shouldn't be possible according to the spec, + // it feels a little brittle to check this scenario here and I don't particularly like it + // TODO: can we do any better or should we go over the spec and allow getLong(date) to be valid? + if (!(type == Long.class && columnType == JDBCType.TIMESTAMP) && type.isInstance(val)) { try { return type.cast(val); } catch (ClassCastException cce) { - throw new SQLDataException("Unable to convert " + val.getClass().getName() + " to " + columnType, cce); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a %s", val, + columnType.getName(), type.getName()), cce); } } @@ -205,7 +210,8 @@ static T convert(Object val, JDBCType columnType, Class type) throws SQLE if (type == OffsetDateTime.class) { return (T) asOffsetDateTime(val, columnType); } - throw new SQLException("Conversion from type [" + columnType + "] to [" + type.getName() + "] not supported"); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a %s", val, + columnType.getName(), type.getName())); } /** @@ -336,8 +342,11 @@ private static Boolean asBoolean(Object val, JDBCType columnType) throws SQLExce case FLOAT: case DOUBLE: return Boolean.valueOf(Integer.signum(((Number) val).intValue()) != 0); + case VARCHAR: + return Boolean.valueOf((String) val); default: - throw new SQLException("Conversion from type [" + columnType + "] to [Boolean] not supported"); + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Boolean", val, columnType.getName())); } } @@ -355,10 +364,16 @@ private static Byte asByte(Object val, JDBCType columnType) throws SQLException case FLOAT: case DOUBLE: return safeToByte(safeToLong(((Number) val).doubleValue())); + case VARCHAR: + try { + return Byte.valueOf((String) val); + } catch (NumberFormatException e) { + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Byte", val), e); + } default: } - throw new SQLException("Conversion from type [" + columnType + "] to [Byte] not supported"); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Byte", val, columnType.getName())); } private static Short asShort(Object val, JDBCType columnType) throws SQLException { @@ -374,10 +389,16 @@ private static Short asShort(Object val, JDBCType columnType) throws SQLExceptio case FLOAT: case DOUBLE: return safeToShort(safeToLong(((Number) val).doubleValue())); + case VARCHAR: + try { + return Short.valueOf((String) val); + } catch (NumberFormatException e) { + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Short", val), e); + } default: } - throw new SQLException("Conversion from type [" + columnType + "] to [Short] not supported"); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Short", val, columnType.getName())); } private static Integer asInteger(Object val, JDBCType columnType) throws SQLException { @@ -393,10 +414,18 @@ private static Integer asInteger(Object val, JDBCType columnType) throws SQLExce case FLOAT: case DOUBLE: return safeToInt(safeToLong(((Number) val).doubleValue())); + case VARCHAR: + try { + return Integer.valueOf((String) val); + } catch (NumberFormatException e) { + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to an Integer", val), e); + } default: } - throw new SQLException("Conversion from type [" + columnType + "] to [Integer] not supported"); + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to an Integer", val, columnType.getName())); } private static Long asLong(Object val, JDBCType columnType) throws SQLException { @@ -412,12 +441,21 @@ private static Long asLong(Object val, JDBCType columnType) throws SQLException case FLOAT: case DOUBLE: return safeToLong(((Number) val).doubleValue()); - case TIMESTAMP: - return ((Number) val).longValue(); + //TODO: should we support conversion to TIMESTAMP? + //The spec says that getLong() should support the following types conversions: + //TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR + //case TIMESTAMP: + // return ((Number) val).longValue(); + case VARCHAR: + try { + return Long.valueOf((String) val); + } catch (NumberFormatException e) { + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Long", val), e); + } default: } - throw new SQLException("Conversion from type [" + columnType + "] to [Long] not supported"); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Long", val, columnType.getName())); } private static Float asFloat(Object val, JDBCType columnType) throws SQLException { @@ -433,10 +471,16 @@ private static Float asFloat(Object val, JDBCType columnType) throws SQLExceptio case FLOAT: case DOUBLE: return Float.valueOf((((float) ((Number) val).doubleValue()))); + case VARCHAR: + try { + return Float.valueOf((String) val); + } catch (NumberFormatException e) { + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Float", val), e); + } default: } - throw new SQLException("Conversion from type [" + columnType + "] to [Float] not supported"); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Float", val, columnType.getName())); } private static Double asDouble(Object val, JDBCType columnType) throws SQLException { @@ -451,32 +495,41 @@ private static Double asDouble(Object val, JDBCType columnType) throws SQLExcept case REAL: case FLOAT: case DOUBLE: + return Double.valueOf(((Number) val).doubleValue()); + case VARCHAR: + try { + return Double.valueOf((String) val); + } catch (NumberFormatException e) { + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Double", val), e); + } default: } - throw new SQLException("Conversion from type [" + columnType + "] to [Double] not supported"); + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Double", val, columnType.getName())); } private static Date asDate(Object val, JDBCType columnType) throws SQLException { if (columnType == JDBCType.TIMESTAMP) { return new Date(utcMillisRemoveTime(((Number) val).longValue())); } - throw new SQLException("Conversion from type [" + columnType + "] to [Date] not supported"); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Date", val, columnType.getName())); } private static Time asTime(Object val, JDBCType columnType) throws SQLException { if (columnType == JDBCType.TIMESTAMP) { return new Time(utcMillisRemoveDate(((Number) val).longValue())); } - throw new SQLException("Conversion from type [" + columnType + "] to [Time] not supported"); + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Time", val, columnType.getName())); } private static Timestamp asTimestamp(Object val, JDBCType columnType) throws SQLException { if (columnType == JDBCType.TIMESTAMP) { return new Timestamp(((Number) val).longValue()); } - throw new SQLException("Conversion from type [" + columnType + "] to [Timestamp] not supported"); + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Timestamp", val, columnType.getName())); } private static byte[] asByteArray(Object val, JDBCType columnType) { diff --git a/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java index 9da06f6537c0..35a3ec574874 100644 --- a/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java +++ b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcPreparedStatementTests.java @@ -25,6 +25,7 @@ import java.util.Locale; import java.util.Map; +import static java.lang.String.format; import static java.sql.JDBCType.BIGINT; import static java.sql.JDBCType.BOOLEAN; import static java.sql.JDBCType.DOUBLE; @@ -68,7 +69,7 @@ public void testThrownExceptionsWhenSettingBooleanValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, true, Types.TIMESTAMP)); - assertEquals("Conversion from type [BOOLEAN] to [Timestamp] not supported", sqle.getMessage()); + assertEquals("Unable to convert value [true] of type [BOOLEAN] to a Timestamp", sqle.getMessage()); } public void testSettingStringValues() throws SQLException { @@ -92,7 +93,7 @@ public void testThrownExceptionsWhenSettingStringValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, "foo bar", Types.INTEGER)); - assertEquals("Conversion from type [VARCHAR] to [Integer] not supported", sqle.getMessage()); + assertEquals("Unable to convert value [foo bar] of type [VARCHAR] to an Integer", sqle.getMessage()); } public void testSettingByteTypeValues() throws SQLException { @@ -128,7 +129,7 @@ public void testThrownExceptionsWhenSettingByteTypeValues() throws SQLException JdbcPreparedStatement jps = createJdbcPreparedStatement(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, (byte) 6, Types.TIMESTAMP)); - assertEquals("Conversion from type [TINYINT] to [Timestamp] not supported", sqle.getMessage()); + assertEquals("Unable to convert value [6] of type [TINYINT] to a Timestamp", sqle.getMessage()); } public void testSettingShortTypeValues() throws SQLException { @@ -161,7 +162,7 @@ public void testThrownExceptionsWhenSettingShortTypeValues() throws SQLException JdbcPreparedStatement jps = createJdbcPreparedStatement(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, (short) 6, Types.TIMESTAMP)); - assertEquals("Conversion from type [SMALLINT] to [Timestamp] not supported", sqle.getMessage()); + assertEquals("Unable to convert value [6] of type [SMALLINT] to a Timestamp", sqle.getMessage()); sqle = expectThrows(SQLException.class, () -> jps.setObject(1, 256, Types.TINYINT)); assertEquals("Numeric " + 256 + " out of range", sqle.getMessage()); @@ -195,7 +196,7 @@ public void testThrownExceptionsWhenSettingIntegerValues() throws SQLException { int someInt = randomInt(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someInt, Types.TIMESTAMP)); - assertEquals("Conversion from type [INTEGER] to [Timestamp] not supported", sqle.getMessage()); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [INTEGER] to a Timestamp", someInt), sqle.getMessage()); Integer randomIntNotShort = randomIntBetween(32768, Integer.MAX_VALUE); sqle = expectThrows(SQLException.class, () -> jps.setObject(1, randomIntNotShort, Types.SMALLINT)); @@ -236,7 +237,7 @@ public void testThrownExceptionsWhenSettingLongValues() throws SQLException { long someLong = randomLong(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someLong, Types.TIMESTAMP)); - assertEquals("Conversion from type [BIGINT] to [Timestamp] not supported", sqle.getMessage()); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [BIGINT] to a Timestamp", someLong), sqle.getMessage()); Long randomLongNotShort = randomLongBetween(Integer.MAX_VALUE + 1, Long.MAX_VALUE); sqle = expectThrows(SQLException.class, () -> jps.setObject(1, randomLongNotShort, Types.INTEGER)); @@ -277,7 +278,7 @@ public void testThrownExceptionsWhenSettingFloatValues() throws SQLException { float someFloat = randomFloat(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someFloat, Types.TIMESTAMP)); - assertEquals("Conversion from type [REAL] to [Timestamp] not supported", sqle.getMessage()); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [REAL] to a Timestamp", someFloat), sqle.getMessage()); Float floatNotInt = 5_155_000_000f; sqle = expectThrows(SQLException.class, () -> jps.setObject(1, floatNotInt, Types.INTEGER)); @@ -316,7 +317,8 @@ public void testThrownExceptionsWhenSettingDoubleValues() throws SQLException { double someDouble = randomDouble(); SQLException sqle = expectThrows(SQLException.class, () -> jps.setObject(1, someDouble, Types.TIMESTAMP)); - assertEquals("Conversion from type [DOUBLE] to [Timestamp] not supported", sqle.getMessage()); + assertEquals( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [DOUBLE] to a Timestamp", someDouble), sqle.getMessage()); Double doubleNotInt = 5_155_000_000d; sqle = expectThrows(SQLException.class, () -> jps.setObject(1, doubleNotInt, Types.INTEGER)); @@ -361,7 +363,7 @@ public Object[] getAttributes() throws SQLException { public void testSettingTimestampValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - Timestamp someTimestamp = new Timestamp(randomMillisSinceEpoch()); + Timestamp someTimestamp = new Timestamp(randomLong()); jps.setTimestamp(1, someTimestamp); assertEquals(someTimestamp.getTime(), ((Date)value(jps)).getTime()); assertEquals(TIMESTAMP, jdbcType(jps)); @@ -372,7 +374,7 @@ public void testSettingTimestampValues() throws SQLException { assertEquals(1456708675000L, convertFromUTCtoCalendar(((Date)value(jps)), nonDefaultCal)); assertEquals(TIMESTAMP, jdbcType(jps)); - long beforeEpochTime = -randomMillisSinceEpoch(); + long beforeEpochTime = randomLongBetween(Long.MIN_VALUE, 0); jps.setTimestamp(1, new Timestamp(beforeEpochTime), nonDefaultCal); assertEquals(beforeEpochTime, convertFromUTCtoCalendar(((Date)value(jps)), nonDefaultCal)); assertTrue(value(jps) instanceof java.util.Date); @@ -384,7 +386,7 @@ public void testSettingTimestampValues() throws SQLException { public void testThrownExceptionsWhenSettingTimestampValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - Timestamp someTimestamp = new Timestamp(randomMillisSinceEpoch()); + Timestamp someTimestamp = new Timestamp(randomLong()); SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, someTimestamp, Types.INTEGER)); assertEquals("Conversion from type java.sql.Timestamp to INTEGER not supported", sqle.getMessage()); @@ -416,12 +418,12 @@ public void testThrownExceptionsWhenSettingTimeValues() throws SQLException { public void testSettingSqlDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - java.sql.Date someSqlDate = new java.sql.Date(randomMillisSinceEpoch()); + java.sql.Date someSqlDate = new java.sql.Date(randomLong()); jps.setDate(1, someSqlDate); assertEquals(someSqlDate.getTime(), ((Date)value(jps)).getTime()); assertEquals(TIMESTAMP, jdbcType(jps)); - someSqlDate = new java.sql.Date(randomMillisSinceEpoch()); + someSqlDate = new java.sql.Date(randomLong()); Calendar nonDefaultCal = randomCalendar(); jps.setDate(1, someSqlDate, nonDefaultCal); assertEquals(someSqlDate.getTime(), convertFromUTCtoCalendar(((Date)value(jps)), nonDefaultCal)); @@ -435,17 +437,17 @@ public void testSettingSqlDateValues() throws SQLException { public void testThrownExceptionsWhenSettingSqlDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - java.sql.Date someSqlDate = new java.sql.Date(randomMillisSinceEpoch()); + java.sql.Date someSqlDate = new java.sql.Date(randomLong()); SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, - () -> jps.setObject(1, new java.sql.Date(randomMillisSinceEpoch()), Types.DOUBLE)); + () -> jps.setObject(1, new java.sql.Date(randomLong()), Types.DOUBLE)); assertEquals("Conversion from type " + someSqlDate.getClass().getName() + " to DOUBLE not supported", sqle.getMessage()); } public void testSettingCalendarValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); Calendar someCalendar = randomCalendar(); - someCalendar.setTimeInMillis(randomMillisSinceEpoch()); + someCalendar.setTimeInMillis(randomLong()); jps.setObject(1, someCalendar); assertEquals(someCalendar.getTime(), (Date) value(jps)); @@ -472,7 +474,7 @@ public void testThrownExceptionsWhenSettingCalendarValues() throws SQLException public void testSettingDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - Date someDate = new Date(randomMillisSinceEpoch()); + Date someDate = new Date(randomLong()); jps.setObject(1, someDate); assertEquals(someDate, (Date) value(jps)); @@ -486,7 +488,7 @@ public void testSettingDateValues() throws SQLException { public void testThrownExceptionsWhenSettingDateValues() throws SQLException { JdbcPreparedStatement jps = createJdbcPreparedStatement(); - Date someDate = new Date(randomMillisSinceEpoch()); + Date someDate = new Date(randomLong()); SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setObject(1, someDate, Types.BIGINT)); assertEquals("Conversion from type " + someDate.getClass().getName() + " to BIGINT not supported", sqle.getMessage()); @@ -549,10 +551,6 @@ public void testThrownExceptionsWhenSettingByteArrayValues() throws SQLException assertEquals("Conversion from type byte[] to DOUBLE not supported", sqle.getMessage()); } - private long randomMillisSinceEpoch() { - return randomLongBetween(0, System.currentTimeMillis()); - } - private JdbcPreparedStatement createJdbcPreparedStatement() throws SQLException { return new JdbcPreparedStatement(null, JdbcConfiguration.create("jdbc:es://l:1", null, 0), "?"); } diff --git a/x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/JdbcResultSetIT.java b/x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/JdbcResultSetIT.java new file mode 100644 index 000000000000..30756a11f62e --- /dev/null +++ b/x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/JdbcResultSetIT.java @@ -0,0 +1,16 @@ +/* + * 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.qa.sql.nosecurity; + +import org.elasticsearch.xpack.qa.sql.jdbc.ResultSetTestCase; + +/* + * Integration testing class for "no security" (cluster running without the Security plugin, + * or the Security is disbled) scenario. Runs all tests in the base class. + */ +public class JdbcResultSetIT extends ResultSetTestCase { +} diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/ResultSetTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/ResultSetTestCase.java index 861a6dccaba5..447fc4f17e18 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/ResultSetTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/ResultSetTestCase.java @@ -5,55 +5,1067 @@ */ package org.elasticsearch.xpack.qa.sql.jdbc; +import org.elasticsearch.client.Request; +import org.elasticsearch.common.CheckedBiFunction; +import org.elasticsearch.common.CheckedConsumer; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.sql.jdbc.jdbc.JdbcConfiguration; +import org.elasticsearch.xpack.sql.jdbc.jdbcx.JdbcDataSource; +import org.elasticsearch.xpack.sql.type.DataType; + import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.sql.Blob; +import java.sql.Clob; import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.JDBCType; +import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Timestamp; +import java.sql.Types; +import java.util.Arrays; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.TimeZone; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static java.util.Calendar.DAY_OF_MONTH; +import static java.util.Calendar.ERA; +import static java.util.Calendar.HOUR_OF_DAY; +import static java.util.Calendar.MILLISECOND; +import static java.util.Calendar.MINUTE; +import static java.util.Calendar.MONTH; +import static java.util.Calendar.SECOND; +import static java.util.Calendar.YEAR; public class ResultSetTestCase extends JdbcIntegrationTestCase { - public void testGettingTimestamp() throws Exception { - long randomMillis = randomLongBetween(0, System.currentTimeMillis()); + + static final Set fieldsNames = Stream.of("test_byte", "test_integer", "test_long", "test_short", "test_double", + "test_float", "test_keyword") + .collect(Collectors.toCollection(HashSet::new)); + static final Map,JDBCType> dateTimeTestingFields = new HashMap,JDBCType>(); + static final String SELECT_ALL_FIELDS = "SELECT test_boolean, test_byte, test_integer," + + "test_long, test_short, test_double, test_float, test_keyword, test_date FROM test"; + static final String SELECT_WILDCARD = "SELECT * FROM test"; + static { + dateTimeTestingFields.put(new Tuple("test_boolean", true), DataType.BOOLEAN.jdbcType); + dateTimeTestingFields.put(new Tuple("test_byte", 1), DataType.BYTE.jdbcType); + dateTimeTestingFields.put(new Tuple("test_integer", 1), DataType.INTEGER.jdbcType); + dateTimeTestingFields.put(new Tuple("test_long", 1L), DataType.LONG.jdbcType); + dateTimeTestingFields.put(new Tuple("test_short", 1), DataType.SHORT.jdbcType); + dateTimeTestingFields.put(new Tuple("test_double", 1d), DataType.DOUBLE.jdbcType); + dateTimeTestingFields.put(new Tuple("test_float", 1f), DataType.FLOAT.jdbcType); + dateTimeTestingFields.put(new Tuple("test_keyword", "true"), DataType.KEYWORD.jdbcType); + } + + // Byte values testing + public void testGettingValidByteWithoutCasting() throws Exception { + byte random1 = randomByte(); + byte random2 = randomValueOtherThan(random1, () -> randomByte()); + byte random3 = randomValueOtherThanMany(Arrays.asList(random1, random2)::contains, () -> randomByte()); + + createTestDataForByteValueTests(random1, random2, random3); + + doWithQuery("SELECT test_byte, test_null_byte, test_keyword FROM test", (results) -> { + ResultSetMetaData resultSetMetaData = results.getMetaData(); + + results.next(); + assertEquals(3, resultSetMetaData.getColumnCount()); + assertEquals(Types.TINYINT, resultSetMetaData.getColumnType(1)); + assertEquals(Types.TINYINT, resultSetMetaData.getColumnType(2)); + assertEquals(random1, results.getByte(1)); + assertEquals(random1, results.getByte("test_byte")); + assertEquals(random1, (byte) results.getObject("test_byte", Byte.class)); + assertTrue(results.getObject(1) instanceof Byte); + + assertEquals(0, results.getByte(2)); + assertTrue(results.wasNull()); + assertEquals(null, results.getObject("test_null_byte")); + assertTrue(results.wasNull()); + + assertTrue(results.next()); + assertEquals(random2, results.getByte(1)); + assertEquals(random2, results.getByte("test_byte")); + assertTrue(results.getObject(1) instanceof Byte); + assertEquals(random3, results.getByte("test_keyword")); + + assertFalse(results.next()); + }); + } + + public void testGettingValidByteWithCasting() throws Exception { + Map map = createTestDataForNumericValueTypes(() -> randomByte()); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + for(Entry e : map.entrySet()) { + byte actual = results.getObject(e.getKey(), Byte.class); + if (e.getValue() instanceof Double) { + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), results.getByte(e.getKey())); + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), actual); + } else if (e.getValue() instanceof Float) { + assertEquals("For field " + e.getKey(), Math.round(e.getValue().floatValue()), results.getByte(e.getKey())); + assertEquals("For field " + e.getKey(), Math.round(e.getValue().floatValue()), actual); + } else { + assertEquals("For field " + e.getKey(), e.getValue().byteValue(), results.getByte(e.getKey())); + assertEquals("For field " + e.getKey(), e.getValue().byteValue(), actual); + } + } + }); + } + + public void testGettingInvalidByte() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_keyword").field("type", "keyword").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + + int intNotByte = randomIntBetween(Byte.MAX_VALUE + 1, Integer.MAX_VALUE); + long longNotByte = randomLongBetween(Byte.MAX_VALUE + 1, Long.MAX_VALUE); + short shortNotByte = (short) randomIntBetween(Byte.MAX_VALUE + 1, Short.MAX_VALUE); + double doubleNotByte = randomDoubleBetween(Byte.MAX_VALUE + 1, Double.MAX_VALUE, true); + float floatNotByte = randomFloatBetween(Byte.MAX_VALUE + 1, Float.MAX_VALUE); + String randomString = randomUnicodeOfCodepointLengthBetween(128, 256); + long randomDate = randomLong(); + + String doubleErrorMessage = (doubleNotByte > Long.MAX_VALUE || doubleNotByte < Long.MIN_VALUE) ? + Double.toString(doubleNotByte) : Long.toString(Math.round(doubleNotByte)); + + index("test", "1", builder -> { + builder.field("test_integer", intNotByte); + builder.field("test_long", longNotByte); + builder.field("test_short", shortNotByte); + builder.field("test_double", doubleNotByte); + builder.field("test_float", floatNotByte); + builder.field("test_keyword", randomString); + builder.field("test_date", randomDate); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + + SQLException sqle = expectThrows(SQLException.class, () -> results.getByte("test_integer")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", intNotByte), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_integer", Byte.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", intNotByte), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getByte("test_short")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", shortNotByte), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_short", Byte.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", shortNotByte), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getByte("test_long")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Long.toString(longNotByte)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_long", Byte.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Long.toString(longNotByte)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getByte("test_double")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", doubleErrorMessage), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_double", Byte.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", doubleErrorMessage), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getByte("test_float")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotByte)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_float", Byte.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotByte)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getByte("test_keyword")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Byte", randomString), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_keyword", Byte.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Byte", randomString), + sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getByte("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Byte", randomDate), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Byte.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Byte", randomDate), + sqle.getMessage()); + }); + } + + // Short values testing + public void testGettingValidShortWithoutCasting() throws Exception { + short random1 = randomShort(); + short random2 = randomValueOtherThan(random1, () -> randomShort()); + short random3 = randomValueOtherThanMany(Arrays.asList(random1, random2)::contains, () -> randomShort()); + + createTestDataForShortValueTests(random1, random2, random3); + + doWithQuery("SELECT test_short, test_null_short, test_keyword FROM test", (results) -> { + ResultSetMetaData resultSetMetaData = results.getMetaData(); + + results.next(); + assertEquals(3, resultSetMetaData.getColumnCount()); + assertEquals(Types.SMALLINT, resultSetMetaData.getColumnType(1)); + assertEquals(Types.SMALLINT, resultSetMetaData.getColumnType(2)); + assertEquals(random1, results.getShort(1)); + assertEquals(random1, results.getShort("test_short")); + assertEquals(random1, results.getObject("test_short")); + assertTrue(results.getObject(1) instanceof Short); + + assertEquals(0, results.getShort(2)); + assertTrue(results.wasNull()); + assertEquals(null, results.getObject("test_null_short")); + assertTrue(results.wasNull()); + + assertTrue(results.next()); + assertEquals(random2, results.getShort(1)); + assertEquals(random2, results.getShort("test_short")); + assertTrue(results.getObject(1) instanceof Short); + assertEquals(random3, results.getShort("test_keyword")); + + assertFalse(results.next()); + }); + } + + public void testGettingValidShortWithCasting() throws Exception { + Map map = createTestDataForNumericValueTypes(() -> randomShort()); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + for(Entry e : map.entrySet()) { + short actual = (short) results.getObject(e.getKey(), Short.class); + if (e.getValue() instanceof Double) { + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), results.getShort(e.getKey())); + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), actual); + } else if (e.getValue() instanceof Float) { + assertEquals("For field " + e.getKey(), Math.round(e.getValue().floatValue()), results.getShort(e.getKey())); + assertEquals("For field " + e.getKey(), Math.round(e.getValue().floatValue()), actual); + } else { + assertEquals("For field " + e.getKey(), + e.getValue().shortValue(), results.getShort(e.getKey())); + assertEquals("For field " + e.getKey(), e.getValue().shortValue(), actual); + } + } + }); + } + + public void testGettingInvalidShort() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_keyword").field("type", "keyword").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + + int intNotShort = randomIntBetween(Short.MAX_VALUE + 1, Integer.MAX_VALUE); + long longNotShort = randomLongBetween(Short.MAX_VALUE + 1, Long.MAX_VALUE); + double doubleNotShort = randomDoubleBetween(Short.MAX_VALUE + 1, Double.MAX_VALUE, true); + float floatNotShort = randomFloatBetween(Short.MAX_VALUE + 1, Float.MAX_VALUE); + String randomString = randomUnicodeOfCodepointLengthBetween(128, 256); + long randomDate = randomLong(); + + String doubleErrorMessage = (doubleNotShort > Long.MAX_VALUE || doubleNotShort < Long.MIN_VALUE) ? + Double.toString(doubleNotShort) : Long.toString(Math.round(doubleNotShort)); + + index("test", "1", builder -> { + builder.field("test_integer", intNotShort); + builder.field("test_long", longNotShort); + builder.field("test_double", doubleNotShort); + builder.field("test_float", floatNotShort); + builder.field("test_keyword", randomString); + builder.field("test_date", randomDate); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + + SQLException sqle = expectThrows(SQLException.class, () -> results.getShort("test_integer")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", intNotShort), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_integer", Short.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", intNotShort), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getShort("test_long")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Long.toString(longNotShort)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_long", Short.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Long.toString(longNotShort)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getShort("test_double")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", doubleErrorMessage), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_double", Short.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", doubleErrorMessage), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getShort("test_float")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotShort)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_float", Short.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotShort)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getShort("test_keyword")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Short", randomString), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_keyword", Short.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Short", randomString), + sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getShort("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Short", randomDate), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Short.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Short", randomDate), + sqle.getMessage()); + }); + } + + // Integer values testing + public void testGettingValidIntegerWithoutCasting() throws Exception { + int random1 = randomInt(); + int random2 = randomValueOtherThan(random1, () -> randomInt()); + int random3 = randomValueOtherThanMany(Arrays.asList(random1, random2)::contains, () -> randomInt()); + + createTestDataForIntegerValueTests(random1, random2, random3); + + doWithQuery("SELECT test_integer,test_null_integer,test_keyword FROM test", (results) -> { + ResultSetMetaData resultSetMetaData = results.getMetaData(); + + results.next(); + assertEquals(3, resultSetMetaData.getColumnCount()); + assertEquals(Types.INTEGER, resultSetMetaData.getColumnType(1)); + assertEquals(Types.INTEGER, resultSetMetaData.getColumnType(2)); + assertEquals(random1, results.getInt(1)); + assertEquals(random1, results.getInt("test_integer")); + assertEquals(random1, (int) results.getObject("test_integer", Integer.class)); + assertTrue(results.getObject(1) instanceof Integer); + + assertEquals(0, results.getInt(2)); + assertTrue(results.wasNull()); + assertEquals(null, results.getObject("test_null_integer")); + assertTrue(results.wasNull()); + + assertTrue(results.next()); + assertEquals(random2, results.getInt(1)); + assertEquals(random2, results.getInt("test_integer")); + assertTrue(results.getObject(1) instanceof Integer); + assertEquals(random3, results.getInt("test_keyword")); + + assertFalse(results.next()); + }); + } + + public void testGettingValidIntegerWithCasting() throws Exception { + Map map = createTestDataForNumericValueTypes(() -> randomInt()); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + for(Entry e : map.entrySet()) { + int actual = results.getObject(e.getKey(), Integer.class); + if (e.getValue() instanceof Double) { + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), results.getInt(e.getKey())); + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), actual); + } else if (e.getValue() instanceof Float) { + assertEquals("For field " + e.getKey(), Math.round(e.getValue().floatValue()), results.getInt(e.getKey())); + assertEquals("For field " + e.getKey(), Math.round(e.getValue().floatValue()), actual); + } else { + assertEquals("For field " + e.getKey(), e.getValue().intValue(), results.getInt(e.getKey())); + assertEquals("For field " + e.getKey(), e.getValue().intValue(), actual); + } + } + }); + } + + public void testGettingInvalidInteger() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_keyword").field("type", "keyword").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + + long longNotInt = randomLongBetween(getMaxIntPlusOne(), Long.MAX_VALUE); + double doubleNotInt = randomDoubleBetween(getMaxIntPlusOne().doubleValue(), Double.MAX_VALUE, true); + float floatNotInt = randomFloatBetween(getMaxIntPlusOne().floatValue(), Float.MAX_VALUE); + String randomString = randomUnicodeOfCodepointLengthBetween(128, 256); + long randomDate = randomLong(); + + String doubleErrorMessage = (doubleNotInt > Long.MAX_VALUE || doubleNotInt < Long.MIN_VALUE) ? + Double.toString(doubleNotInt) : Long.toString(Math.round(doubleNotInt)); + + index("test", "1", builder -> { + builder.field("test_long", longNotInt); + builder.field("test_double", doubleNotInt); + builder.field("test_float", floatNotInt); + builder.field("test_keyword", randomString); + builder.field("test_date", randomDate); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + + SQLException sqle = expectThrows(SQLException.class, () -> results.getInt("test_long")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Long.toString(longNotInt)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_long", Integer.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Long.toString(longNotInt)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getInt("test_double")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", doubleErrorMessage), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_double", Integer.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", doubleErrorMessage), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getInt("test_float")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotInt)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_float", Integer.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotInt)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getInt("test_keyword")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to an Integer", randomString), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_keyword", Integer.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to an Integer", randomString), + sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getInt("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to an Integer", randomDate), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Integer.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to an Integer", randomDate), + sqle.getMessage()); + }); + } + + // Long values testing + public void testGettingValidLongWithoutCasting() throws Exception { + long random1 = randomLong(); + long random2 = randomValueOtherThan(random1, () -> randomLong()); + long random3 = randomValueOtherThanMany(Arrays.asList(random1, random2)::contains, () -> randomLong()); + + createTestDataForLongValueTests(random1, random2, random3); + + doWithQuery("SELECT test_long, test_null_long, test_keyword FROM test", (results) -> { + ResultSetMetaData resultSetMetaData = results.getMetaData(); + + results.next(); + assertEquals(3, resultSetMetaData.getColumnCount()); + assertEquals(Types.BIGINT, resultSetMetaData.getColumnType(1)); + assertEquals(Types.BIGINT, resultSetMetaData.getColumnType(2)); + assertEquals(random1, results.getLong(1)); + assertEquals(random1, results.getLong("test_long")); + assertEquals(random1, (long) results.getObject("test_long", Long.class)); + assertTrue(results.getObject(1) instanceof Long); + + assertEquals(0, results.getLong(2)); + assertTrue(results.wasNull()); + assertEquals(null, results.getObject("test_null_long")); + assertTrue(results.wasNull()); + + assertTrue(results.next()); + assertEquals(random2, results.getLong(1)); + assertEquals(random2, results.getLong("test_long")); + assertTrue(results.getObject(1) instanceof Long); + assertEquals(random3, results.getLong("test_keyword")); + + assertFalse(results.next()); + }); + } + + public void testGettingValidLongWithCasting() throws Exception { + Map map = createTestDataForNumericValueTypes(() -> randomLong()); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + for(Entry e : map.entrySet()) { + long actual = results.getObject(e.getKey(), Long.class); + if (e.getValue() instanceof Double || e.getValue() instanceof Float) { + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), results.getLong(e.getKey())); + assertEquals("For field " + e.getKey(), Math.round(e.getValue().doubleValue()), actual); + } else { + assertEquals("For field " + e.getKey(), e.getValue().longValue(), results.getLong(e.getKey())); + assertEquals("For field " + e.getKey(), e.getValue().longValue(), actual); + } + } + }); + } + + public void testGettingInvalidLong() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_keyword").field("type", "keyword").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + + double doubleNotLong = randomDoubleBetween(getMaxLongPlusOne().doubleValue(), Double.MAX_VALUE, true); + float floatNotLong = randomFloatBetween(getMaxLongPlusOne().floatValue(), Float.MAX_VALUE); + String randomString = randomUnicodeOfCodepointLengthBetween(128, 256); + long randomDate = randomLong(); + + index("test", "1", builder -> { + builder.field("test_double", doubleNotLong); + builder.field("test_float", floatNotLong); + builder.field("test_keyword", randomString); + builder.field("test_date", randomDate); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + + SQLException sqle = expectThrows(SQLException.class, () -> results.getLong("test_double")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(doubleNotLong)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_double", Long.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(doubleNotLong)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getLong("test_float")); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotLong)), sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_float", Long.class)); + assertEquals(format(Locale.ROOT, "Numeric %s out of range", Double.toString(floatNotLong)), sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getLong("test_keyword")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Long", randomString), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_keyword", Long.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Long", randomString), + sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getLong("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Long", randomDate), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Long.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Long", randomDate), + sqle.getMessage()); + }); + } + + // Double values testing + public void testGettingValidDoubleWithoutCasting() throws Exception { + double random1 = randomDouble(); + double random2 = randomValueOtherThan(random1, () -> randomDouble()); + double random3 = randomValueOtherThanMany(Arrays.asList(random1, random2)::contains, () -> randomDouble()); + + createTestDataForDoubleValueTests(random1, random2, random3); + + doWithQuery("SELECT test_double, test_null_double, test_keyword FROM test", (results) -> { + ResultSetMetaData resultSetMetaData = results.getMetaData(); + + results.next(); + assertEquals(3, resultSetMetaData.getColumnCount()); + assertEquals(Types.DOUBLE, resultSetMetaData.getColumnType(1)); + assertEquals(Types.DOUBLE, resultSetMetaData.getColumnType(2)); + assertEquals(random1, results.getDouble(1), 0.0d); + assertEquals(random1, results.getDouble("test_double"), 0.0d); + assertEquals(random1, results.getObject("test_double", Double.class), 0.0d); + assertTrue(results.getObject(1) instanceof Double); + + assertEquals(0, results.getDouble(2), 0.0d); + assertTrue(results.wasNull()); + assertEquals(null, results.getObject("test_null_double")); + assertTrue(results.wasNull()); + + assertTrue(results.next()); + assertEquals(random2, results.getDouble(1), 0.0d); + assertEquals(random2, results.getDouble("test_double"), 0.0d); + assertTrue(results.getObject(1) instanceof Double); + assertEquals(random3, results.getDouble("test_keyword"), 0.0d); + + assertFalse(results.next()); + }); + } + + public void testGettingValidDoubleWithCasting() throws Exception { + Map map = createTestDataForNumericValueTypes(() -> randomDouble()); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + for(Entry e : map.entrySet()) { + assertEquals("For field " + e.getKey(), e.getValue().doubleValue(), results.getDouble(e.getKey()), 0.0d); + assertEquals("For field " + e.getKey(), + e.getValue().doubleValue(), results.getObject(e.getKey(), Double.class), 0.0d); + } + }); + } + + public void testGettingInvalidDouble() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_keyword").field("type", "keyword").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + + String randomString = randomUnicodeOfCodepointLengthBetween(128, 256); + long randomDate = randomLong(); + + index("test", "1", builder -> { + builder.field("test_keyword", randomString); + builder.field("test_date", randomDate); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + + SQLException sqle = expectThrows(SQLException.class, () -> results.getDouble("test_keyword")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Double", randomString), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_keyword", Double.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Double", randomString), + sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getDouble("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Double", randomDate), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Double.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Double", randomDate), + sqle.getMessage()); + }); + } + + // Float values testing + public void testGettingValidFloatWithoutCasting() throws Exception { + float random1 = randomFloat(); + float random2 = randomValueOtherThan(random1, () -> randomFloat()); + float random3 = randomValueOtherThanMany(Arrays.asList(random1, random2)::contains, () -> randomFloat()); + + createTestDataForFloatValueTests(random1, random2, random3); + + doWithQuery("SELECT test_float, test_null_float, test_keyword FROM test", (results) -> { + ResultSetMetaData resultSetMetaData = results.getMetaData(); + + results.next(); + assertEquals(3, resultSetMetaData.getColumnCount()); + assertEquals(Types.REAL, resultSetMetaData.getColumnType(1)); + assertEquals(Types.REAL, resultSetMetaData.getColumnType(2)); + assertEquals(random1, results.getFloat(1), 0.0f); + assertEquals(random1, results.getFloat("test_float"), 0.0f); + assertEquals(random1, results.getObject("test_float", Float.class), 0.0f); + assertTrue(results.getObject(1) instanceof Float); + + assertEquals(0, results.getFloat(2), 0.0d); + assertTrue(results.wasNull()); + assertEquals(null, results.getObject("test_null_float")); + assertTrue(results.wasNull()); + + assertTrue(results.next()); + assertEquals(random2, results.getFloat(1), 0.0d); + assertEquals(random2, results.getFloat("test_float"), 0.0d); + assertTrue(results.getObject(1) instanceof Float); + assertEquals(random3, results.getFloat("test_keyword"), 0.0d); + + assertFalse(results.next()); + }); + } + + public void testGettingValidFloatWithCasting() throws Exception { + Map map = createTestDataForNumericValueTypes(() -> randomFloat()); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + for(Entry e : map.entrySet()) { + assertEquals("For field " + e.getKey(), e.getValue().floatValue(), results.getFloat(e.getKey()), 0.0f); + assertEquals("For field " + e.getKey(), + e.getValue().floatValue(), results.getObject(e.getKey(), Float.class), 0.0f); + } + }); + } + + public void testGettingInvalidFloat() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_keyword").field("type", "keyword").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + + String randomString = randomUnicodeOfCodepointLengthBetween(128, 256); + long randomDate = randomLong(); + + index("test", "1", builder -> { + builder.field("test_keyword", randomString); + builder.field("test_date", randomDate); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + + SQLException sqle = expectThrows(SQLException.class, () -> results.getFloat("test_keyword")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Float", randomString), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_keyword", Float.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [VARCHAR] to a Float", randomString), + sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getFloat("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Float", randomDate), + sqle.getMessage()); + sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Float.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Float", randomDate), + sqle.getMessage()); + }); + } + + public void testGettingBooleanValues() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_boolean").field("type", "boolean").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + long randomDate1 = randomLong(); + long randomDate2 = randomLong(); + + // true values + indexSimpleDocumentWithTrueValues(randomDate1); + + // false values + index("test", "2", builder -> { + builder.field("test_boolean", false); + builder.field("test_byte", 0); + builder.field("test_integer", 0); + builder.field("test_long", 0L); + builder.field("test_short", 0); + builder.field("test_double", 0d); + builder.field("test_float", 0f); + builder.field("test_keyword", "false"); + builder.field("test_date", randomDate2); + }); + + // other (non 0 = true) values + index("test", "3", builder -> { + builder.field("test_byte", randomValueOtherThan((byte) 0, () -> randomByte())); + builder.field("test_integer", randomValueOtherThan(0, () -> randomInt())); + builder.field("test_long", randomValueOtherThan(0L, () -> randomLong())); + builder.field("test_short", randomValueOtherThan((short) 0, () -> randomShort())); + builder.field("test_double", randomValueOtherThanMany(i -> i < 1.0d && i > -1.0d && i < Double.MAX_VALUE + && i > Double.MIN_VALUE, + () -> randomDouble() * randomInt())); + builder.field("test_float", randomValueOtherThanMany(i -> i < 1.0f && i > -1.0f && i < Float.MAX_VALUE && i > Float.MIN_VALUE, + () -> randomFloat() * randomInt())); + builder.field("test_keyword", "1"); + }); + + // other false values + index("test", "4", builder -> { + builder.field("test_keyword", "0"); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + assertEquals(true, results.getBoolean("test_boolean")); + for(String fld : fieldsNames) { + assertEquals("Expected: but was: for field " + fld, true, results.getBoolean(fld)); + assertEquals("Expected: but was: for field " + fld, true, results.getObject(fld, Boolean.class)); + } + SQLException sqle = expectThrows(SQLException.class, () -> results.getBoolean("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Boolean", randomDate1), + sqle.getMessage()); + + results.next(); + assertEquals(false, results.getBoolean("test_boolean")); + for(String fld : fieldsNames) { + assertEquals("Expected: but was: for field " + fld, false, results.getBoolean(fld)); + assertEquals("Expected: but was: for field " + fld, false, results.getObject(fld, Boolean.class)); + } + sqle = expectThrows(SQLException.class, () -> results.getBoolean("test_date")); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Boolean", randomDate2), + sqle.getMessage()); + + sqle = expectThrows(SQLException.class, () -> results.getObject("test_date", Boolean.class)); + assertEquals(format(Locale.ROOT, "Unable to convert value [%.128s] of type [TIMESTAMP] to a Boolean", randomDate2), + sqle.getMessage()); + + results.next(); + for(String fld : fieldsNames.stream() + .filter((f) -> !f.equals("test_keyword")).collect(Collectors.toCollection(HashSet::new))) { + assertEquals("Expected: but was: for field " + fld, true, results.getBoolean(fld)); + assertEquals("Expected: but was: for field " + fld, true, results.getObject(fld, Boolean.class)); + } + + results.next(); + assertEquals(false, results.getBoolean("test_keyword")); + assertEquals(false, results.getObject("test_keyword", Boolean.class)); + }); + } + + public void testGettingDateWithoutCalendar() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_boolean").field("type", "boolean").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + Long randomLongDate = randomLong(); + indexSimpleDocumentWithTrueValues(randomLongDate); + + String timeZoneId = randomKnownTimeZone(); + Calendar connCalendar = Calendar.getInstance(TimeZone.getTimeZone(timeZoneId), Locale.ROOT); + + doWithQueryAndTimezone(SELECT_ALL_FIELDS, timeZoneId, (results) -> { + results.next(); + connCalendar.setTimeInMillis(randomLongDate); + connCalendar.set(HOUR_OF_DAY, 0); + connCalendar.set(MINUTE, 0); + connCalendar.set(SECOND, 0); + connCalendar.set(MILLISECOND, 0); + + assertEquals(results.getDate("test_date"), new java.sql.Date(connCalendar.getTimeInMillis())); + assertEquals(results.getDate(9), new java.sql.Date(connCalendar.getTimeInMillis())); + assertEquals(results.getObject("test_date", java.sql.Date.class), + new java.sql.Date(randomLongDate - (randomLongDate % 86400000L))); + assertEquals(results.getObject(9, java.sql.Date.class), + new java.sql.Date(randomLongDate - (randomLongDate % 86400000L))); + + // bulk validation for all fields which are not of type date + validateErrorsForDateTimeTestsWithoutCalendar(results::getDate); + }); + } + + public void testGettingDateWithCalendar() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_boolean").field("type", "boolean").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + Long randomLongDate = randomLong(); + indexSimpleDocumentWithTrueValues(randomLongDate); + index("test", "2", builder -> { + builder.timeField("test_date", null); + }); + + String timeZoneId = randomKnownTimeZone(); + String anotherTZId = randomValueOtherThan(timeZoneId, () -> randomKnownTimeZone()); + Calendar c = Calendar.getInstance(TimeZone.getTimeZone(anotherTZId), Locale.ROOT); + + doWithQueryAndTimezone(SELECT_ALL_FIELDS, timeZoneId, (results) -> { + results.next(); + c.setTimeInMillis(randomLongDate); + c.set(HOUR_OF_DAY, 0); + c.set(MINUTE, 0); + c.set(SECOND, 0); + c.set(MILLISECOND, 0); + + assertEquals(results.getDate("test_date", c), new java.sql.Date(c.getTimeInMillis())); + assertEquals(results.getDate(9, c), new java.sql.Date(c.getTimeInMillis())); + + // bulk validation for all fields which are not of type date + validateErrorsForDateTimeTestsWithCalendar(c, results::getDate); + + results.next(); + assertNull(results.getDate("test_date")); + }); + } + + public void testGettingTimeWithoutCalendar() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_boolean").field("type", "boolean").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + Long randomLongDate = randomLong(); + indexSimpleDocumentWithTrueValues(randomLongDate); + + String timeZoneId = randomKnownTimeZone(); + Calendar c = Calendar.getInstance(TimeZone.getTimeZone(timeZoneId), Locale.ROOT); + + doWithQueryAndTimezone(SELECT_ALL_FIELDS, timeZoneId, (results) -> { + results.next(); + c.setTimeInMillis(randomLongDate); + c.set(ERA, GregorianCalendar.AD); + c.set(YEAR, 1970); + c.set(MONTH, 0); + c.set(DAY_OF_MONTH, 1); + + assertEquals(results.getTime("test_date"), new java.sql.Time(c.getTimeInMillis())); + assertEquals(results.getTime(9), new java.sql.Time(c.getTimeInMillis())); + assertEquals(results.getObject("test_date", java.sql.Time.class), + new java.sql.Time(randomLongDate % 86400000L)); + assertEquals(results.getObject(9, java.sql.Time.class), + new java.sql.Time(randomLongDate % 86400000L)); + + validateErrorsForDateTimeTestsWithoutCalendar(results::getTime); + }); + } + + public void testGettingTimeWithCalendar() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_boolean").field("type", "boolean").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + Long randomLongDate = randomLong(); + indexSimpleDocumentWithTrueValues(randomLongDate); + index("test", "2", builder -> { + builder.timeField("test_date", null); + }); + + String timeZoneId = randomKnownTimeZone(); + String anotherTZId = randomValueOtherThan(timeZoneId, () -> randomKnownTimeZone()); + Calendar c = Calendar.getInstance(TimeZone.getTimeZone(anotherTZId), Locale.ROOT); + + doWithQueryAndTimezone(SELECT_ALL_FIELDS, timeZoneId, (results) -> { + results.next(); + c.setTimeInMillis(randomLongDate); + c.set(ERA, GregorianCalendar.AD); + c.set(YEAR, 1970); + c.set(MONTH, 0); + c.set(DAY_OF_MONTH, 1); + + assertEquals(results.getTime("test_date", c), new java.sql.Time(c.getTimeInMillis())); + assertEquals(results.getTime(9, c), new java.sql.Time(c.getTimeInMillis())); + + validateErrorsForDateTimeTestsWithCalendar(c, results::getTime); + + results.next(); + assertNull(results.getTime("test_date")); + }); + } + + public void testGettingTimestampWithoutCalendar() throws Exception { + createIndex("library"); + updateMapping("library", builder -> { + builder.startObject("release_date").field("type", "date").endObject(); + builder.startObject("republish_date").field("type", "date").endObject(); + }); + long randomMillis = randomLong(); index("library", "1", builder -> { builder.field("name", "Don Quixote"); builder.field("page_count", 1072); - builder.timeField("release_date", new Date(randomMillis)); + builder.field("release_date", randomMillis); builder.timeField("republish_date", null); }); index("library", "2", builder -> { builder.field("name", "1984"); builder.field("page_count", 328); - builder.timeField("release_date", new Date(-649036800000L)); - builder.timeField("republish_date", new Date(599616000000L)); + builder.field("release_date", -649036800000L); + builder.field("republish_date", 599616000000L); }); - try (Connection connection = esJdbc()) { - try (PreparedStatement statement = connection.prepareStatement("SELECT name, release_date, republish_date FROM library")) { - try (ResultSet results = statement.executeQuery()) { - ResultSetMetaData resultSetMetaData = results.getMetaData(); - - results.next(); - assertEquals(3, resultSetMetaData.getColumnCount()); - assertEquals(randomMillis, results.getTimestamp("release_date").getTime()); - assertEquals(randomMillis, results.getTimestamp(2).getTime()); - assertTrue(results.getObject(2) instanceof Timestamp); - assertEquals(randomMillis, ((Timestamp) results.getObject("release_date")).getTime()); - - assertNull(results.getTimestamp(3)); - assertNull(results.getObject("republish_date")); - - assertTrue(results.next()); - assertEquals(599616000000L, results.getTimestamp("republish_date").getTime()); - assertEquals(-649036800000L, ((Timestamp) results.getObject(2)).getTime()); - - assertFalse(results.next()); - } - } - } + doWithQuery("SELECT name, release_date, republish_date FROM library", (results) -> { + ResultSetMetaData resultSetMetaData = results.getMetaData(); + + results.next(); + assertEquals(3, resultSetMetaData.getColumnCount()); + assertEquals(randomMillis, results.getTimestamp("release_date").getTime()); + assertEquals(randomMillis, results.getTimestamp(2).getTime()); + assertTrue(results.getObject(2) instanceof Timestamp); + assertEquals(randomMillis, ((Timestamp) results.getObject("release_date")).getTime()); + + assertNull(results.getTimestamp(3)); + assertNull(results.getObject("republish_date")); + + assertTrue(results.next()); + assertEquals(599616000000L, results.getTimestamp("republish_date").getTime()); + assertEquals(-649036800000L, ((Timestamp) results.getObject(2)).getTime()); + + assertFalse(results.next()); + }); + } + + public void testGettingTimestampWithCalendar() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_boolean").field("type", "boolean").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + Long randomLongDate = randomLong(); + indexSimpleDocumentWithTrueValues(randomLongDate); + index("test", "2", builder -> { + builder.timeField("test_date", null); + }); + + String timeZoneId = randomKnownTimeZone(); + String anotherTZId = randomValueOtherThan(timeZoneId, () -> randomKnownTimeZone()); + Calendar c = Calendar.getInstance(TimeZone.getTimeZone(anotherTZId), Locale.ROOT); + + doWithQueryAndTimezone(SELECT_ALL_FIELDS, timeZoneId, (results) -> { + results.next(); + c.setTimeInMillis(randomLongDate); + + assertEquals(results.getTimestamp("test_date", c), new java.sql.Timestamp(c.getTimeInMillis())); + assertEquals(results.getTimestamp(9, c), new java.sql.Timestamp(c.getTimeInMillis())); + + validateErrorsForDateTimeTestsWithCalendar(c, results::getTimestamp); + + results.next(); + assertNull(results.getTimestamp("test_date")); + }); + } + + public void testValidGetObjectCalls() throws Exception { + createIndex("test"); + updateMappingForNumericValuesTests("test"); + updateMapping("test", builder -> { + builder.startObject("test_boolean").field("type", "boolean").endObject(); + builder.startObject("test_date").field("type", "date").endObject(); + }); + + byte b = randomByte(); + int i = randomInt(); + long l = randomLong(); + short s = (short) randomIntBetween(Short.MIN_VALUE, Short.MAX_VALUE); + double d = randomDouble(); + float f = randomFloat(); + boolean randomBool = randomBoolean(); + Long randomLongDate = randomLong(); + String randomString = randomUnicodeOfCodepointLengthBetween(128, 256); + + index("test", "1", builder -> { + builder.field("test_byte", b); + builder.field("test_integer", i); + builder.field("test_long", l); + builder.field("test_short", s); + builder.field("test_double", d); + builder.field("test_float", f); + builder.field("test_keyword", randomString); + builder.field("test_date", randomLongDate); + builder.field("test_boolean", randomBool); + }); + + doWithQuery(SELECT_WILDCARD, (results) -> { + results.next(); + + assertEquals(b, results.getObject("test_byte")); + assertTrue(results.getObject("test_byte") instanceof Byte); + + assertEquals(i, results.getObject("test_integer")); + assertTrue(results.getObject("test_integer") instanceof Integer); + + assertEquals(l, results.getObject("test_long")); + assertTrue(results.getObject("test_long") instanceof Long); + + assertEquals(s, results.getObject("test_short")); + assertTrue(results.getObject("test_short") instanceof Short); + + assertEquals(d, results.getObject("test_double")); + assertTrue(results.getObject("test_double") instanceof Double); + + assertEquals(f, results.getObject("test_float")); + assertTrue(results.getObject("test_float") instanceof Float); + + assertEquals(randomString, results.getObject("test_keyword")); + assertTrue(results.getObject("test_keyword") instanceof String); + + assertEquals(new Date(randomLongDate), results.getObject("test_date")); + assertTrue(results.getObject("test_date") instanceof Timestamp); + + assertEquals(randomBool, results.getObject("test_boolean")); + assertTrue(results.getObject("test_boolean") instanceof Boolean); + }); } /* @@ -79,4 +1091,458 @@ public void testNoInfiniteRecursiveGetObjectCalls() throws SQLException, IOExcep fail("Infinite recursive call on getObject() method"); } } + + public void testUnsupportedGetMethods() throws IOException, SQLException { + index("test", "1", builder -> { + builder.field("test", "test"); + }); + Connection conn = esJdbc(); + PreparedStatement statement = conn.prepareStatement("SELECT * FROM test"); + ResultSet r = statement.executeQuery(); + + r.next(); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getAsciiStream("test"), "AsciiStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getAsciiStream(1), "AsciiStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getArray("test"), "Array not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getArray(1), "Array not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getBigDecimal("test"), "BigDecimal not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getBigDecimal("test"), "BigDecimal not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getBinaryStream("test"), "BinaryStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getBinaryStream(1), "BinaryStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getBlob("test"), "Blob not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getBlob(1), "Blob not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getCharacterStream("test"), "CharacterStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getCharacterStream(1), "CharacterStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getClob("test"), "Clob not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getClob(1), "Clob not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getNCharacterStream("test"), "NCharacterStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getNCharacterStream(1), "NCharacterStream not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getNClob("test"), "NClob not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getNClob(1), "NClob not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getNString("test"), "NString not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getNString(1), "NString not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getRef("test"), "Ref not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getRef(1), "Ref not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getRowId("test"), "RowId not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getRowId(1), "RowId not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getSQLXML("test"), "SQLXML not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getSQLXML(1), "SQLXML not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getURL("test"), "URL not supported"); + assertThrowsUnsupportedAndExpectErrorMessage(() -> r.getURL(1), "URL not supported"); + } + + public void testUnsupportedUpdateMethods() throws IOException, SQLException { + index("test", "1", builder -> { + builder.field("test", "test"); + }); + Connection conn = esJdbc(); + PreparedStatement statement = conn.prepareStatement("SELECT * FROM test"); + ResultSet r = statement.executeQuery(); + + r.next(); + Blob b = null; + InputStream i = null; + Clob c = null; + NClob nc = null; + Reader rd = null; + + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBytes(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBytes("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateArray(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateArray("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateAsciiStream(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateAsciiStream("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateAsciiStream(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateAsciiStream(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateAsciiStream("", null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateAsciiStream("", null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBigDecimal(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBigDecimal("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBinaryStream(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBinaryStream("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBinaryStream(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBinaryStream(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBinaryStream("", null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBinaryStream("", null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBlob(1, b)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBlob(1, i)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBlob("", b)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBlob("", i)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBlob(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBlob("", null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBoolean(1, false)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateBoolean("", false)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateByte(1, (byte) 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateByte("", (byte) 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateCharacterStream(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateCharacterStream("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateCharacterStream(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateCharacterStream(1, null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateCharacterStream("", null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateCharacterStream("", null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateClob(1, c)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateClob(1, rd)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateClob("", c)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateClob("", rd)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateClob(1, null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateClob("", null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateDate(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateDate("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateDouble(1, 0d)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateDouble("", 0d)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateFloat(1, 0f)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateFloat("", 0f)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateInt(1, 0)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateInt("", 0)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateLong(1, 0L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateLong("", 0L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNCharacterStream(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNCharacterStream("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNCharacterStream(1, null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNCharacterStream("", null, 1L)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNClob(1, nc)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNClob(1, rd)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNClob("", nc)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNClob("", rd)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNClob(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNClob("", null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNString(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNString("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNull(1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateNull("")); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateObject(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateObject("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateObject(1, null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateObject("", null, 1)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateRef(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateRef("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateRow()); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateRowId(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateRowId("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateSQLXML(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateSQLXML("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateShort(1, (short) 0)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateShort("", (short) 0)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateString(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateString("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateTime(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateTime("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateTimestamp(1, null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateTimestamp("", null)); + assertThrowsWritesUnsupportedForUpdate(() -> r.insertRow()); + assertThrowsWritesUnsupportedForUpdate(() -> r.updateRow()); + assertThrowsWritesUnsupportedForUpdate(() -> r.deleteRow()); + assertThrowsWritesUnsupportedForUpdate(() -> r.cancelRowUpdates()); + assertThrowsWritesUnsupportedForUpdate(() -> r.moveToInsertRow()); + assertThrowsWritesUnsupportedForUpdate(() -> r.refreshRow()); + assertThrowsWritesUnsupportedForUpdate(() -> r.moveToCurrentRow()); + assertThrowsWritesUnsupportedForUpdate(() -> r.rowUpdated()); + assertThrowsWritesUnsupportedForUpdate(() -> r.rowInserted()); + assertThrowsWritesUnsupportedForUpdate(() -> r.rowDeleted()); + } + + private void doWithQuery(String query, CheckedConsumer consumer) throws SQLException { + try (Connection connection = esJdbc()) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + try (ResultSet results = statement.executeQuery()) { + consumer.accept(results); + } + } + } + } + + private void doWithQueryAndTimezone(String query, String tz, CheckedConsumer consumer) throws SQLException { + try (Connection connection = esJdbc(tz)) { + try (PreparedStatement statement = connection.prepareStatement(query)) { + try (ResultSet results = statement.executeQuery()) { + consumer.accept(results); + } + } + } + } + + private void createIndex(String index) throws Exception { + Request request = new Request("PUT", "/" + index); + XContentBuilder createIndex = JsonXContent.contentBuilder().startObject(); + createIndex.startObject("settings"); + { + createIndex.field("number_of_shards", 1); + createIndex.field("number_of_replicas", 1); + } + createIndex.endObject(); + createIndex.startObject("mappings"); + { + createIndex.startObject("doc"); + { + createIndex.startObject("properties"); + {} + createIndex.endObject(); + } + createIndex.endObject(); + } + createIndex.endObject().endObject(); + request.setJsonEntity(Strings.toString(createIndex)); + client().performRequest(request); + } + + private void updateMapping(String index, CheckedConsumer body) throws Exception { + Request request = new Request("PUT", "/" + index + "/_mapping/doc"); + XContentBuilder updateMapping = JsonXContent.contentBuilder().startObject(); + updateMapping.startObject("properties"); + { + body.accept(updateMapping); + } + updateMapping.endObject().endObject(); + + request.setJsonEntity(Strings.toString(updateMapping)); + client().performRequest(request); + } + + private void createTestDataForByteValueTests(byte random1, byte random2, byte random3) throws Exception, IOException { + createIndex("test"); + updateMapping("test", builder -> { + builder.startObject("test_byte").field("type", "byte").endObject(); + builder.startObject("test_null_byte").field("type", "byte").endObject(); + builder.startObject("test_keyword").field("type", "keyword").endObject(); + }); + + index("test", "1", builder -> { + builder.field("test_byte", random1); + builder.field("test_null_byte", (Byte) null); + }); + index("test", "2", builder -> { + builder.field("test_byte", random2); + builder.field("test_keyword", random3); + }); + } + + private void createTestDataForShortValueTests(short random1, short random2, short random3) throws Exception, IOException { + createIndex("test"); + updateMapping("test", builder -> { + builder.startObject("test_short").field("type", "short").endObject(); + builder.startObject("test_null_short").field("type", "short").endObject(); + builder.startObject("test_keyword").field("type", "keyword").endObject(); + }); + + index("test", "1", builder -> { + builder.field("test_short", random1); + builder.field("test_null_short", (Short) null); + }); + index("test", "2", builder -> { + builder.field("test_short", random2); + builder.field("test_keyword", random3); + }); + } + + private void createTestDataForIntegerValueTests(int random1, int random2, int random3) throws Exception, IOException { + createIndex("test"); + updateMapping("test", builder -> { + builder.startObject("test_integer").field("type", "integer").endObject(); + builder.startObject("test_null_integer").field("type", "integer").endObject(); + builder.startObject("test_keyword").field("type", "keyword").endObject(); + }); + + index("test", "1", builder -> { + builder.field("test_integer", random1); + builder.field("test_null_integer", (Integer) null); + }); + index("test", "2", builder -> { + builder.field("test_integer", random2); + builder.field("test_keyword", random3); + }); + } + + private void createTestDataForLongValueTests(long random1, long random2, long random3) throws Exception, IOException { + createIndex("test"); + updateMapping("test", builder -> { + builder.startObject("test_long").field("type", "long").endObject(); + builder.startObject("test_null_long").field("type", "long").endObject(); + builder.startObject("test_keyword").field("type", "keyword").endObject(); + }); + + index("test", "1", builder -> { + builder.field("test_long", random1); + builder.field("test_null_long", (Long) null); + }); + index("test", "2", builder -> { + builder.field("test_long", random2); + builder.field("test_keyword", random3); + }); + } + + private void createTestDataForDoubleValueTests(double random1, double random2, double random3) throws Exception, IOException { + createIndex("test"); + updateMapping("test", builder -> { + builder.startObject("test_double").field("type", "double").endObject(); + builder.startObject("test_null_double").field("type", "double").endObject(); + builder.startObject("test_keyword").field("type", "keyword").endObject(); + }); + + index("test", "1", builder -> { + builder.field("test_double", random1); + builder.field("test_null_double", (Double) null); + }); + index("test", "2", builder -> { + builder.field("test_double", random2); + builder.field("test_keyword", random3); + }); + } + + private void createTestDataForFloatValueTests(float random1, float random2, float random3) throws Exception, IOException { + createIndex("test"); + updateMapping("test", builder -> { + builder.startObject("test_float").field("type", "float").endObject(); + builder.startObject("test_null_float").field("type", "float").endObject(); + builder.startObject("test_keyword").field("type", "keyword").endObject(); + }); + + index("test", "1", builder -> { + builder.field("test_float", random1); + builder.field("test_null_float", (Double) null); + }); + index("test", "2", builder -> { + builder.field("test_float", random2); + builder.field("test_keyword", random3); + }); + } + + private void indexSimpleDocumentWithTrueValues(Long randomLongDate) throws IOException { + index("test", "1", builder -> { + builder.field("test_boolean", true); + builder.field("test_byte", 1); + builder.field("test_integer", 1); + builder.field("test_long", 1L); + builder.field("test_short", 1); + builder.field("test_double", 1d); + builder.field("test_float", 1f); + builder.field("test_keyword", "true"); + builder.field("test_date", randomLongDate); + }); + } + + /** + * Creates test data for all numeric get* methods. All values random and different from the other numeric fields already generated. + * It returns a map containing the field name and its randomly generated value to be later used in checking the returned values. + */ + private Map createTestDataForNumericValueTypes(Supplier randomGenerator) throws Exception, IOException { + Map map = new HashMap(); + createIndex("test"); + updateMappingForNumericValuesTests("test"); + + index("test", "1", builder -> { + // random Byte + byte test_byte = randomValueOtherThanMany(map::containsValue, randomGenerator).byteValue(); + builder.field("test_byte", test_byte); + map.put("test_byte", test_byte); + + // random Integer + int test_integer = randomValueOtherThanMany(map::containsValue, randomGenerator).intValue(); + builder.field("test_integer", test_integer); + map.put("test_integer", test_integer); + + // random Short + int test_short = randomValueOtherThanMany(map::containsValue, randomGenerator).shortValue(); + builder.field("test_short", test_short); + map.put("test_short", test_short); + + // random Long + long test_long = randomValueOtherThanMany(map::containsValue, randomGenerator).longValue(); + builder.field("test_long", test_long); + map.put("test_long", test_long); + + // random Double + double test_double = randomValueOtherThanMany(map::containsValue, randomGenerator).doubleValue(); + builder.field("test_double", test_double); + map.put("test_double", test_double); + + // random Float + float test_float = randomValueOtherThanMany(map::containsValue, randomGenerator).floatValue(); + builder.field("test_float", test_float); + map.put("test_float", test_float); + }); + return map; + } + + private void updateMappingForNumericValuesTests(String indexName) throws Exception { + updateMapping(indexName, builder -> { + for(String field : fieldsNames) { + builder.startObject(field).field("type", field.substring(5)).endObject(); + } + }); + } + + private void assertThrowsUnsupportedAndExpectErrorMessage(ThrowingRunnable runnable, String message) { + SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, runnable); + assertEquals(message, sqle.getMessage()); + } + + private void assertThrowsWritesUnsupportedForUpdate(ThrowingRunnable r) { + assertThrowsUnsupportedAndExpectErrorMessage(r, "Writes not supported"); + } + + private void validateErrorsForDateTimeTestsWithoutCalendar(CheckedFunction method) { + SQLException sqle; + for(Entry,JDBCType> field : dateTimeTestingFields.entrySet()) { + sqle = expectThrows(SQLException.class, () -> method.apply(field.getKey().v1())); + assertEquals( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Long", + field.getKey().v2(), field.getValue()), sqle.getMessage()); + } + } + + private void validateErrorsForDateTimeTestsWithCalendar(Calendar c, CheckedBiFunction method) { + SQLException sqle; + for(Entry,JDBCType> field : dateTimeTestingFields.entrySet()) { + sqle = expectThrows(SQLException.class, () -> method.apply(field.getKey().v1(), c)); + assertEquals( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Long", + field.getKey().v2(), field.getValue()), sqle.getMessage()); + } + } + + private float randomFloatBetween(float start, float end) { + float result = 0.0f; + while (result < start || result > end || Float.isNaN(result)) { + result = start + randomFloat() * (end - start); + } + + return result; + } + + private Long getMaxIntPlusOne() { + return Long.valueOf(Integer.MAX_VALUE) + 1L; + } + + private Double getMaxLongPlusOne() { + return Double.valueOf(Long.MAX_VALUE) + 1d; + } + + private Connection esJdbc(String timeZoneId) throws SQLException { + return randomBoolean() ? useDriverManager(timeZoneId) : useDataSource(timeZoneId); + } + + private Connection useDriverManager(String timeZoneId) throws SQLException { + String elasticsearchAddress = getProtocol() + "://" + elasticsearchAddress(); + String address = "jdbc:es://" + elasticsearchAddress; + Properties connectionProperties = connectionProperties(); + connectionProperties.put(JdbcConfiguration.TIME_ZONE, timeZoneId); + Connection connection = DriverManager.getConnection(address, connectionProperties); + + assertNotNull("The timezone should be specified", connectionProperties.getProperty(JdbcConfiguration.TIME_ZONE)); + return connection; + } + + private Connection useDataSource(String timeZoneId) throws SQLException { + String elasticsearchAddress = getProtocol() + "://" + elasticsearchAddress(); + JdbcDataSource dataSource = new JdbcDataSource(); + String address = "jdbc:es://" + elasticsearchAddress; + dataSource.setUrl(address); + Properties connectionProperties = connectionProperties(); + connectionProperties.put(JdbcConfiguration.TIME_ZONE, timeZoneId); + dataSource.setProperties(connectionProperties); + Connection connection = dataSource.getConnection(); + + assertNotNull("The timezone should be specified", connectionProperties.getProperty(JdbcConfiguration.TIME_ZONE)); + return connection; + } } diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SimpleExampleTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SimpleExampleTestCase.java index 7621743481a4..f5d559d9bf0b 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SimpleExampleTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/jdbc/SimpleExampleTestCase.java @@ -25,7 +25,8 @@ public void testSimpleExample() throws Exception { assertEquals("Don Quixote", results.getString(1)); assertEquals(1072, results.getInt(2)); SQLException e = expectThrows(SQLException.class, () -> results.getInt(1)); - assertTrue(e.getMessage(), e.getMessage().contains("unable to convert column 1 to an int")); + assertTrue(e.getMessage(), + e.getMessage().contains("Unable to convert value [Don Quixote] of type [VARCHAR] to an Integer")); assertFalse(results.next()); } // end::simple_example From a84a20844be0bbb39c7e063e33691f8d3e0a8380 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 31 Aug 2018 16:36:57 +0300 Subject: [PATCH 261/283] Lazy evaluate java9home (#33301) --- .../groovy/org/elasticsearch/gradle/BuildPlugin.groovy | 2 +- .../gradle/precommit/ForbiddenApisCliTask.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 6a9d4076eef8..75b5676cc34a 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -211,7 +211,7 @@ class BuildPlugin implements Plugin { project.rootProject.ext.minimumRuntimeVersion = minimumRuntimeVersion project.rootProject.ext.inFipsJvm = inFipsJvm project.rootProject.ext.gradleJavaVersion = JavaVersion.toVersion(gradleJavaVersion) - project.rootProject.ext.java9Home = findJavaHome("9") + project.rootProject.ext.java9Home = "${-> findJavaHome("9")}" } project.targetCompatibility = project.rootProject.ext.minimumRuntimeVersion diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java index 46e5d84a2f28..aaa9564b0dc0 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/ForbiddenApisCliTask.java @@ -51,7 +51,8 @@ public class ForbiddenApisCliTask extends DefaultTask { private JavaVersion targetCompatibility; private FileCollection classesDirs; private SourceSet sourceSet; - private String javaHome; + // This needs to be an object so it can hold Groovy GStrings + private Object javaHome; @Input public JavaVersion getTargetCompatibility() { @@ -142,11 +143,11 @@ public Configuration getForbiddenAPIsConfiguration() { } @Input - public String getJavaHome() { + public Object getJavaHome() { return javaHome; } - public void setJavaHome(String javaHome) { + public void setJavaHome(Object javaHome) { this.javaHome = javaHome; } From db3d32ce91b02d0de76085a3c47d57e4d21f0e92 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 31 Aug 2018 16:48:00 +0300 Subject: [PATCH 262/283] Fix pom for build-tools (#33300) Looks like `java-gradle-plugin` reconfigures the pom. Stop using it since we don't publish to Gradle plugin portal. --- buildSrc/build.gradle | 9 --------- .../elasticsearch.clusterformation.properties | 1 + 2 files changed, 1 insertion(+), 9 deletions(-) create mode 100644 buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.clusterformation.properties diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index dce14b10fcb8..da8ad788164d 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -24,15 +24,6 @@ plugins { id 'groovy' } -gradlePlugin { - plugins { - simplePlugin { - id = 'elasticsearch.clusterformation' - implementationClass = 'org.elasticsearch.gradle.clusterformation.ClusterformationPlugin' - } - } -} - group = 'org.elasticsearch.gradle' String minimumGradleVersion = file('src/main/resources/minimumGradleVersion').text.trim() diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.clusterformation.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.clusterformation.properties new file mode 100644 index 000000000000..dfd6cd9956a5 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.clusterformation.properties @@ -0,0 +1 @@ +implementation-class=org.elasticsearch.gradle.clusterformation.ClusterformationPlugin From 66b164c2a6176b5cbb9e44e650749dbae7bec8d0 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 31 Aug 2018 21:16:06 +0700 Subject: [PATCH 263/283] [CCR] Removed custom follow and unfollow api's reponse classes with AcknowledgedResponse (#33260) These response classes did not add any value and in that case just AcknowledgedResponse should be used. I also changed the formatting of methods to take one line per parameter in FollowIndexAction.java and UnfollowIndexAction.java files to make reviewing diffs in the future easier. --- .../xpack/ccr/action/FollowIndexAction.java | 69 ++++++++++++------- .../xpack/ccr/action/UnfollowIndexAction.java | 32 ++++----- .../elasticsearch/xpack/ccr/CcrLicenseIT.java | 5 +- 3 files changed, 60 insertions(+), 46 deletions(-) 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 index 179c4f1c4389..17b7bbe674b3 100644 --- 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 @@ -62,7 +62,7 @@ import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.stream.Collectors; -public class FollowIndexAction extends Action { +public class FollowIndexAction extends Action { public static final FollowIndexAction INSTANCE = new FollowIndexAction(); public static final String NAME = "cluster:admin/xpack/ccr/follow_index"; @@ -72,8 +72,8 @@ private FollowIndexAction() { } @Override - public Response newResponse() { - return new Response(); + public AcknowledgedResponse newResponse() { + return new AcknowledgedResponse(); } public static class Request extends ActionRequest implements ToXContentObject { @@ -129,9 +129,17 @@ public static Request fromXContent(XContentParser parser, String followerIndex) 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) { + 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"); } @@ -271,22 +279,21 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(leaderIndex, followerIndex, maxBatchOperationCount, maxConcurrentReadBatches, maxOperationSizeInBytes, - maxConcurrentWriteBatches, maxWriteBufferSize, retryTimeout, idleShardRetryDelay); - } - } - - public static class Response extends AcknowledgedResponse { - - Response() { - } - - Response(boolean acknowledged) { - super(acknowledged); + return Objects.hash( + leaderIndex, + followerIndex, + maxBatchOperationCount, + maxConcurrentReadBatches, + maxOperationSizeInBytes, + maxConcurrentWriteBatches, + maxWriteBufferSize, + retryTimeout, + idleShardRetryDelay + ); } } - public static class TransportAction extends HandledTransportAction { + public static class TransportAction extends HandledTransportAction { private final Client client; private final ThreadPool threadPool; @@ -318,7 +325,9 @@ public TransportAction( } @Override - protected void doExecute(final Task task, final Request request, final ActionListener listener) { + protected void doExecute(final Task task, + final Request request, + final ActionListener listener) { if (ccrLicenseChecker.isCcrAllowed()) { final String[] indices = new String[]{request.leaderIndex}; final Map> remoteClusterIndices = remoteClusterService.groupClusterIndices(indices, s -> false); @@ -337,7 +346,8 @@ protected void doExecute(final Task task, final Request request, final ActionLis } } - private void followLocalIndex(final Request request, final ActionListener 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 @@ -353,7 +363,7 @@ private void followRemoteIndex( final Request request, final String clusterAlias, final String leaderIndex, - final ActionListener listener) { + final ActionListener listener) { final ClusterState state = clusterService.state(); final IndexMetaData followerIndexMetadata = state.getMetaData().index(request.getFollowerIndex()); ccrLicenseChecker.checkRemoteClusterLicenseAndFetchLeaderIndexMetadata( @@ -380,8 +390,13 @@ private void followRemoteIndex( *
  • 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 { + 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(); @@ -429,7 +444,7 @@ void finalizeResponse() { if (error == null) { // include task ids? - handler.onResponse(new Response(true)); + handler.onResponse(new AcknowledgedResponse(true)); } else { // TODO: cancel all started tasks handler.onFailure(error); @@ -493,7 +508,9 @@ void finalizeResponse() { WHITELISTED_SETTINGS = Collections.unmodifiableSet(whiteListedSettings); } - static void validate(Request request, IndexMetaData leaderIndex, IndexMetaData followIndex, MapperService followerMapperService) { + static void validate(Request request, + IndexMetaData leaderIndex, + IndexMetaData followIndex, MapperService followerMapperService) { if (leaderIndex == null) { throw new IllegalArgumentException("leader index [" + request.leaderIndex + "] does not exist"); } 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 index f671d59cfe46..93b2bcc3e409 100644 --- 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 @@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReferenceArray; -public class UnfollowIndexAction extends Action { +public class UnfollowIndexAction extends Action { public static final UnfollowIndexAction INSTANCE = new UnfollowIndexAction(); public static final String NAME = "cluster:admin/xpack/ccr/unfollow_index"; @@ -38,8 +38,8 @@ private UnfollowIndexAction() { } @Override - public Response newResponse() { - return new Response(); + public AcknowledgedResponse newResponse() { + return new AcknowledgedResponse(); } public static class Request extends ActionRequest { @@ -72,31 +72,27 @@ public void writeTo(StreamOutput out) throws IOException { } } - public static class Response extends AcknowledgedResponse { - - Response(boolean acknowledged) { - super(acknowledged); - } - - Response() { - } - } - - public static class TransportAction extends HandledTransportAction { + 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) { + 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) { + 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) { @@ -140,7 +136,7 @@ void finalizeResponse() { if (error == null) { // include task ids? - listener.onResponse(new Response(true)); + listener.onResponse(new AcknowledgedResponse(true)); } else { // TODO: cancel all started tasks listener.onFailure(error); 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 87772d0c1507..675758903bf2 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 @@ -8,6 +8,7 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -36,9 +37,9 @@ public void testThatFollowingIndexIsUnavailableWithIncompatibleLicense() throws client().execute( FollowIndexAction.INSTANCE, followRequest, - new ActionListener() { + new ActionListener() { @Override - public void onResponse(final FollowIndexAction.Response response) { + public void onResponse(final AcknowledgedResponse response) { fail(); } From 436d5c4eeec6185e8c745095a68227ea6e46d4b6 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Fri, 31 Aug 2018 17:47:05 +0100 Subject: [PATCH 264/283] Fixes SecurityIntegTestCase so it always adds at least one alias (#33296) * Fixes SecurityIntegTestCase so it always adds at least one alias `SecurityIntegTestCase.createIndicesWithRandomAliases` could randomly fail because its not gauranteed that the randomness of which aliases to add to the `IndicesAliasesRequestBuilder` would always select at least one alias to add. This change fixes the problem by keeping track of whether we have added an alias to teh request and forcing the last alias to be added if no other aliases have been added so far. Closes #30098 Closes #33123e * Addresses review comments --- .../org/elasticsearch/test/SecurityIntegTestCase.java | 8 ++++++-- .../xpack/security/authz/ReadActionsTests.java | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java index 9bb0e44eb664..7143182c1621 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecurityIntegTestCase.java @@ -7,6 +7,7 @@ import io.netty.util.ThreadDeathWatcher; import io.netty.util.concurrent.GlobalEventExecutor; + import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; @@ -44,7 +45,6 @@ import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.client.SecurityClient; import org.elasticsearch.xpack.security.LocalStateSecurity; - import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.AfterClass; import org.junit.Before; @@ -420,14 +420,18 @@ protected void createIndicesWithRandomAliases(String... indices) { createIndex(indices); if (frequently()) { + boolean aliasAdded = false; IndicesAliasesRequestBuilder builder = client().admin().indices().prepareAliases(); for (String index : indices) { if (frequently()) { //one alias per index with prefix "alias-" builder.addAlias(index, "alias-" + index); + aliasAdded = true; } } - if (randomBoolean()) { + // If we get to this point and we haven't added an alias to the request we need to add one + // or the request will fail so use noAliasAdded to force adding the alias in this case + if (aliasAdded == false || randomBoolean()) { //one alias pointing to all indices for (String index : indices) { builder.addAlias(index, "alias"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java index a88dafece325..76568d3d48b5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/ReadActionsTests.java @@ -102,7 +102,6 @@ public void testEmptyAuthorizedIndicesSearchForAll() { assertNoSearchHits(client().prepareSearch().get()); } - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/33123") public void testEmptyAuthorizedIndicesSearchForAllDisallowNoIndices() { createIndicesWithRandomAliases("index1", "index2"); IndexNotFoundException e = expectThrows(IndexNotFoundException.class, () -> client().prepareSearch() From 8703d875c0d14c7a723e9a02467bff65714b2f8c Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 31 Aug 2018 12:54:12 -0400 Subject: [PATCH 265/283] TEST: Disable soft-deletes in ParentChildTestCase Tracked at #33318 --- .../java/org/elasticsearch/join/query/ParentChildTestCase.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/ParentChildTestCase.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/ParentChildTestCase.java index 87b16bc448ef..5ea0d8312ad0 100644 --- a/modules/parent-join/src/test/java/org/elasticsearch/join/query/ParentChildTestCase.java +++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/ParentChildTestCase.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.IndexModule; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.join.ParentJoinPlugin; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; @@ -58,6 +59,8 @@ protected Collection> transportClientPlugins() { @Override public Settings indexSettings() { Settings.Builder builder = Settings.builder().put(super.indexSettings()) + // AwaitsFix: https://github.com/elastic/elasticsearch/issues/33318 + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false) // aggressive filter caching so that we can assert on the filter cache size .put(IndexModule.INDEX_QUERY_CACHE_ENABLED_SETTING.getKey(), true) .put(IndexModule.INDEX_QUERY_CACHE_EVERYTHING_SETTING.getKey(), true); From 08d0527e25b94eabf402c6f39c587b00ce76a1d1 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 31 Aug 2018 10:11:58 -0700 Subject: [PATCH 266/283] [DOCS] Rename X-Pack Commands section (#33005) --- docs/reference/commands/index.asciidoc | 8 ++++---- docs/reference/redirects.asciidoc | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/reference/commands/index.asciidoc b/docs/reference/commands/index.asciidoc index 164d2fc0e84f..134ac1edbd01 100644 --- a/docs/reference/commands/index.asciidoc +++ b/docs/reference/commands/index.asciidoc @@ -1,11 +1,11 @@ -[role="xpack"] -[[xpack-commands]] -= {xpack} Commands +[[commands]] += Command line tools [partintro] -- -{xpack} includes commands that help you configure security: +{es} provides the following tools for configuring security and performing other +tasks from the command line: * <> * <> diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index b88c7bf4547b..f0e055380537 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -544,3 +544,8 @@ You can use the following APIs to add, remove, and retrieve role mappings: === Privilege APIs See <>. + +[role="exclude",id="xpack-commands"] +=== X-Pack commands + +See <>. From cdeadfc585fbbe9c9c01da590eae89869b823291 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 31 Aug 2018 10:50:43 -0700 Subject: [PATCH 267/283] [DOCS] Move rollup APIs to docs (#31450) --- docs/build.gradle | 267 ++++++++++++++++++ docs/reference/index.asciidoc | 2 +- docs/reference/rest-api/index.asciidoc | 2 +- .../reference}/rollup/api-quickref.asciidoc | 2 + .../rollup/apis}/delete-job.asciidoc | 1 + .../reference/rollup/apis}/get-job.asciidoc | 1 + .../reference/rollup/apis}/put-job.asciidoc | 1 + .../rollup/apis}/rollup-caps.asciidoc | 1 + .../rollup/apis}/rollup-index-caps.asciidoc | 0 .../rollup/apis}/rollup-job-config.asciidoc | 1 + .../rollup/apis}/rollup-search.asciidoc | 1 + .../reference/rollup/apis}/start-job.asciidoc | 1 + .../reference/rollup/apis}/stop-job.asciidoc | 1 + .../reference}/rollup/index.asciidoc | 2 + .../reference}/rollup/overview.asciidoc | 2 + .../rollup/rollup-agg-limitations.asciidoc | 2 + .../reference/rollup}/rollup-api.asciidoc | 19 +- .../rollup/rollup-getting-started.asciidoc | 2 + .../rollup/rollup-search-limitations.asciidoc | 2 + .../rollup/understanding-groups.asciidoc | 2 + 20 files changed, 301 insertions(+), 11 deletions(-) rename {x-pack/docs/en => docs/reference}/rollup/api-quickref.asciidoc (97%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/delete-job.asciidoc (99%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/get-job.asciidoc (99%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/put-job.asciidoc (99%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/rollup-caps.asciidoc (99%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/rollup-index-caps.asciidoc (100%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/rollup-job-config.asciidoc (99%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/rollup-search.asciidoc (99%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/start-job.asciidoc (99%) rename {x-pack/docs/en/rest-api/rollup => docs/reference/rollup/apis}/stop-job.asciidoc (99%) rename {x-pack/docs/en => docs/reference}/rollup/index.asciidoc (97%) rename {x-pack/docs/en => docs/reference}/rollup/overview.asciidoc (99%) rename {x-pack/docs/en => docs/reference}/rollup/rollup-agg-limitations.asciidoc (94%) rename {x-pack/docs/en/rest-api => docs/reference/rollup}/rollup-api.asciidoc (61%) rename {x-pack/docs/en => docs/reference}/rollup/rollup-getting-started.asciidoc (99%) rename {x-pack/docs/en => docs/reference}/rollup/rollup-search-limitations.asciidoc (99%) rename {x-pack/docs/en => docs/reference}/rollup/understanding-groups.asciidoc (99%) diff --git a/docs/build.gradle b/docs/build.gradle index 980c99baf832..8a23654f2e20 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -74,6 +74,17 @@ buildRestTests.docs = fileTree(projectDir) { exclude 'build' // Just syntax examples exclude 'README.asciidoc' + // Broken code snippet tests + exclude 'reference/rollup/rollup-getting-started.asciidoc' + exclude 'reference/rollup/apis/rollup-job-config.asciidoc' + exclude 'reference/rollup/apis/rollup-index-caps.asciidoc' + exclude 'reference/rollup/apis/put-job.asciidoc' + exclude 'reference/rollup/apis/stop-job.asciidoc' + exclude 'reference/rollup/apis/start-job.asciidoc' + exclude 'reference/rollup/apis/rollup-search.asciidoc' + exclude 'reference/rollup/apis/delete-job.asciidoc' + exclude 'reference/rollup/apis/get-job.asciidoc' + exclude 'reference/rollup/apis/rollup-caps.asciidoc' } listSnippets.docs = buildRestTests.docs @@ -594,3 +605,259 @@ buildRestTests.setups['library'] = ''' {"name": "The Moon is a Harsh Mistress", "author": "Robert A. Heinlein", "release_date": "1966-04-01", "page_count": 288} ''' +buildRestTests.setups['sensor_rollup_job'] = ''' + - do: + indices.create: + index: sensor-1 + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + _doc: + properties: + timestamp: + type: date + temperature: + type: long + voltage: + type: float + node: + type: keyword + - do: + xpack.rollup.put_job: + id: "sensor" + body: > + { + "index_pattern": "sensor-*", + "rollup_index": "sensor_rollup", + "cron": "*/30 * * * * ?", + "page_size" :1000, + "groups" : { + "date_histogram": { + "field": "timestamp", + "interval": "1h", + "delay": "7d" + }, + "terms": { + "fields": ["node"] + } + }, + "metrics": [ + { + "field": "temperature", + "metrics": ["min", "max", "sum"] + }, + { + "field": "voltage", + "metrics": ["avg"] + } + ] + } +''' +buildRestTests.setups['sensor_started_rollup_job'] = ''' + - do: + indices.create: + index: sensor-1 + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + _doc: + properties: + timestamp: + type: date + temperature: + type: long + voltage: + type: float + node: + type: keyword + + - do: + bulk: + index: sensor-1 + type: _doc + refresh: true + body: | + {"index":{}} + {"timestamp": 1516729294000, "temperature": 200, "voltage": 5.2, "node": "a"} + {"index":{}} + {"timestamp": 1516642894000, "temperature": 201, "voltage": 5.8, "node": "b"} + {"index":{}} + {"timestamp": 1516556494000, "temperature": 202, "voltage": 5.1, "node": "a"} + {"index":{}} + {"timestamp": 1516470094000, "temperature": 198, "voltage": 5.6, "node": "b"} + {"index":{}} + {"timestamp": 1516383694000, "temperature": 200, "voltage": 4.2, "node": "c"} + {"index":{}} + {"timestamp": 1516297294000, "temperature": 202, "voltage": 4.0, "node": "c"} + + - do: + xpack.rollup.put_job: + id: "sensor" + body: > + { + "index_pattern": "sensor-*", + "rollup_index": "sensor_rollup", + "cron": "* * * * * ?", + "page_size" :1000, + "groups" : { + "date_histogram": { + "field": "timestamp", + "interval": "1h", + "delay": "7d" + }, + "terms": { + "fields": ["node"] + } + }, + "metrics": [ + { + "field": "temperature", + "metrics": ["min", "max", "sum"] + }, + { + "field": "voltage", + "metrics": ["avg"] + } + ] + } + - do: + xpack.rollup.start_job: + id: "sensor" +''' + +buildRestTests.setups['sensor_index'] = ''' + - do: + indices.create: + index: sensor-1 + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + _doc: + properties: + timestamp: + type: date + temperature: + type: long + voltage: + type: float + node: + type: keyword + load: + type: double + net_in: + type: long + net_out: + type: long + hostname: + type: keyword + datacenter: + type: keyword +''' + +buildRestTests.setups['sensor_prefab_data'] = ''' + - do: + indices.create: + index: sensor-1 + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + _doc: + properties: + timestamp: + type: date + temperature: + type: long + voltage: + type: float + node: + type: keyword + - do: + indices.create: + index: sensor_rollup + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + _doc: + properties: + node.terms.value: + type: keyword + temperature.sum.value: + type: double + temperature.max.value: + type: double + temperature.min.value: + type: double + timestamp.date_histogram.time_zone: + type: keyword + timestamp.date_histogram.interval: + type: keyword + timestamp.date_histogram.timestamp: + type: date + timestamp.date_histogram._count: + type: long + voltage.avg.value: + type: double + voltage.avg._count: + type: long + _rollup.id: + type: keyword + _rollup.version: + type: long + _meta: + _rollup: + sensor: + cron: "* * * * * ?" + rollup_index: "sensor_rollup" + index_pattern: "sensor-*" + timeout: "20s" + page_size: 1000 + groups: + date_histogram: + delay: "7d" + field: "timestamp" + interval: "1h" + time_zone: "UTC" + terms: + fields: + - "node" + id: sensor + metrics: + - field: "temperature" + metrics: + - min + - max + - sum + - field: "voltage" + metrics: + - avg + + - do: + bulk: + index: sensor_rollup + type: _doc + refresh: true + body: | + {"index":{}} + {"node.terms.value":"b","temperature.sum.value":201.0,"temperature.max.value":201.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":201.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":5.800000190734863,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516640400000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} + {"index":{}} + {"node.terms.value":"c","temperature.sum.value":200.0,"temperature.max.value":200.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":200.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":4.199999809265137,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516381200000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} + {"index":{}} + {"node.terms.value":"a","temperature.sum.value":202.0,"temperature.max.value":202.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":202.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":5.099999904632568,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516554000000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} + {"index":{}} + {"node.terms.value":"a","temperature.sum.value":200.0,"temperature.max.value":200.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":200.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":5.199999809265137,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516726800000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} + {"index":{}} + {"node.terms.value":"b","temperature.sum.value":198.0,"temperature.max.value":198.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":198.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":5.599999904632568,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516467600000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} + {"index":{}} + {"node.terms.value":"c","temperature.sum.value":202.0,"temperature.max.value":202.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":202.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":4.0,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516294800000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} + +''' diff --git a/docs/reference/index.asciidoc b/docs/reference/index.asciidoc index 7d51e4aa5126..4df3ad48fb6b 100644 --- a/docs/reference/index.asciidoc +++ b/docs/reference/index.asciidoc @@ -61,7 +61,7 @@ include::sql/index.asciidoc[] include::monitoring/index.asciidoc[] -include::{xes-repo-dir}/rollup/index.asciidoc[] +include::rollup/index.asciidoc[] include::rest-api/index.asciidoc[] diff --git a/docs/reference/rest-api/index.asciidoc b/docs/reference/rest-api/index.asciidoc index 9ec57940dd29..e1d607948e1e 100644 --- a/docs/reference/rest-api/index.asciidoc +++ b/docs/reference/rest-api/index.asciidoc @@ -23,7 +23,7 @@ include::{xes-repo-dir}/rest-api/graph/explore.asciidoc[] include::{es-repo-dir}/licensing/index.asciidoc[] include::{es-repo-dir}/migration/migration.asciidoc[] include::{xes-repo-dir}/rest-api/ml-api.asciidoc[] -include::{xes-repo-dir}/rest-api/rollup-api.asciidoc[] +include::{es-repo-dir}/rollup/rollup-api.asciidoc[] include::{xes-repo-dir}/rest-api/security.asciidoc[] include::{xes-repo-dir}/rest-api/watcher.asciidoc[] include::{xes-repo-dir}/rest-api/defs.asciidoc[] diff --git a/x-pack/docs/en/rollup/api-quickref.asciidoc b/docs/reference/rollup/api-quickref.asciidoc similarity index 97% rename from x-pack/docs/en/rollup/api-quickref.asciidoc rename to docs/reference/rollup/api-quickref.asciidoc index 5e99f1c69841..1d372a03ddcf 100644 --- a/x-pack/docs/en/rollup/api-quickref.asciidoc +++ b/docs/reference/rollup/api-quickref.asciidoc @@ -1,3 +1,5 @@ +[role="xpack"] +[testenv="basic"] [[rollup-api-quickref]] == API Quick Reference diff --git a/x-pack/docs/en/rest-api/rollup/delete-job.asciidoc b/docs/reference/rollup/apis/delete-job.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/delete-job.asciidoc rename to docs/reference/rollup/apis/delete-job.asciidoc index b795e0b28c76..37774560848c 100644 --- a/x-pack/docs/en/rest-api/rollup/delete-job.asciidoc +++ b/docs/reference/rollup/apis/delete-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-delete-job]] === Delete Job API ++++ diff --git a/x-pack/docs/en/rest-api/rollup/get-job.asciidoc b/docs/reference/rollup/apis/get-job.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/get-job.asciidoc rename to docs/reference/rollup/apis/get-job.asciidoc index 96053dbfea64..794d72480121 100644 --- a/x-pack/docs/en/rest-api/rollup/get-job.asciidoc +++ b/docs/reference/rollup/apis/get-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-get-job]] === Get Rollup Jobs API ++++ diff --git a/x-pack/docs/en/rest-api/rollup/put-job.asciidoc b/docs/reference/rollup/apis/put-job.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/put-job.asciidoc rename to docs/reference/rollup/apis/put-job.asciidoc index 27889d985b8c..79e30ae8dc99 100644 --- a/x-pack/docs/en/rest-api/rollup/put-job.asciidoc +++ b/docs/reference/rollup/apis/put-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-put-job]] === Create Job API ++++ diff --git a/x-pack/docs/en/rest-api/rollup/rollup-caps.asciidoc b/docs/reference/rollup/apis/rollup-caps.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/rollup-caps.asciidoc rename to docs/reference/rollup/apis/rollup-caps.asciidoc index 1f233f195a09..907efb94c177 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-caps.asciidoc +++ b/docs/reference/rollup/apis/rollup-caps.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-get-rollup-caps]] === Get Rollup Job Capabilities ++++ diff --git a/x-pack/docs/en/rest-api/rollup/rollup-index-caps.asciidoc b/docs/reference/rollup/apis/rollup-index-caps.asciidoc similarity index 100% rename from x-pack/docs/en/rest-api/rollup/rollup-index-caps.asciidoc rename to docs/reference/rollup/apis/rollup-index-caps.asciidoc diff --git a/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc b/docs/reference/rollup/apis/rollup-job-config.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc rename to docs/reference/rollup/apis/rollup-job-config.asciidoc index f937f28601a2..3a917fb59f21 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-job-config.asciidoc +++ b/docs/reference/rollup/apis/rollup-job-config.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-job-config]] === Rollup Job Configuration diff --git a/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc b/docs/reference/rollup/apis/rollup-search.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc rename to docs/reference/rollup/apis/rollup-search.asciidoc index 115ef8fb0438..8e7fc69a00a6 100644 --- a/x-pack/docs/en/rest-api/rollup/rollup-search.asciidoc +++ b/docs/reference/rollup/apis/rollup-search.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-search]] === Rollup Search ++++ diff --git a/x-pack/docs/en/rest-api/rollup/start-job.asciidoc b/docs/reference/rollup/apis/start-job.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/start-job.asciidoc rename to docs/reference/rollup/apis/start-job.asciidoc index 9a0a0a7e4f01..cf44883895c4 100644 --- a/x-pack/docs/en/rest-api/rollup/start-job.asciidoc +++ b/docs/reference/rollup/apis/start-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-start-job]] === Start Job API ++++ diff --git a/x-pack/docs/en/rest-api/rollup/stop-job.asciidoc b/docs/reference/rollup/apis/stop-job.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/rollup/stop-job.asciidoc rename to docs/reference/rollup/apis/stop-job.asciidoc index 605074027050..5912b2d688b7 100644 --- a/x-pack/docs/en/rest-api/rollup/stop-job.asciidoc +++ b/docs/reference/rollup/apis/stop-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-stop-job]] === Stop Job API ++++ diff --git a/x-pack/docs/en/rollup/index.asciidoc b/docs/reference/rollup/index.asciidoc similarity index 97% rename from x-pack/docs/en/rollup/index.asciidoc rename to docs/reference/rollup/index.asciidoc index 9ac89341bfe9..64dc233f82f6 100644 --- a/x-pack/docs/en/rollup/index.asciidoc +++ b/docs/reference/rollup/index.asciidoc @@ -1,3 +1,5 @@ +[role="xpack"] +[testenv="basic"] [[xpack-rollup]] = Rolling up historical data diff --git a/x-pack/docs/en/rollup/overview.asciidoc b/docs/reference/rollup/overview.asciidoc similarity index 99% rename from x-pack/docs/en/rollup/overview.asciidoc rename to docs/reference/rollup/overview.asciidoc index a9a983fbecc1..b2570f647e72 100644 --- a/x-pack/docs/en/rollup/overview.asciidoc +++ b/docs/reference/rollup/overview.asciidoc @@ -1,3 +1,5 @@ +[role="xpack"] +[testenv="basic"] [[rollup-overview]] == Overview diff --git a/x-pack/docs/en/rollup/rollup-agg-limitations.asciidoc b/docs/reference/rollup/rollup-agg-limitations.asciidoc similarity index 94% rename from x-pack/docs/en/rollup/rollup-agg-limitations.asciidoc rename to docs/reference/rollup/rollup-agg-limitations.asciidoc index cd20622d93c8..9f8b6f66adee 100644 --- a/x-pack/docs/en/rollup/rollup-agg-limitations.asciidoc +++ b/docs/reference/rollup/rollup-agg-limitations.asciidoc @@ -1,3 +1,5 @@ +[role="xpack"] +[testenv="basic"] [[rollup-agg-limitations]] == Rollup Aggregation Limitations diff --git a/x-pack/docs/en/rest-api/rollup-api.asciidoc b/docs/reference/rollup/rollup-api.asciidoc similarity index 61% rename from x-pack/docs/en/rest-api/rollup-api.asciidoc rename to docs/reference/rollup/rollup-api.asciidoc index 9a8ec00d77a0..099686fb4329 100644 --- a/x-pack/docs/en/rest-api/rollup-api.asciidoc +++ b/docs/reference/rollup/rollup-api.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="basic"] [[rollup-apis]] == Rollup APIs @@ -26,12 +27,12 @@ -include::rollup/delete-job.asciidoc[] -include::rollup/get-job.asciidoc[] -include::rollup/put-job.asciidoc[] -include::rollup/start-job.asciidoc[] -include::rollup/stop-job.asciidoc[] -include::rollup/rollup-caps.asciidoc[] -include::rollup/rollup-index-caps.asciidoc[] -include::rollup/rollup-search.asciidoc[] -include::rollup/rollup-job-config.asciidoc[] \ No newline at end of file +include::apis/delete-job.asciidoc[] +include::apis/get-job.asciidoc[] +include::apis/put-job.asciidoc[] +include::apis/start-job.asciidoc[] +include::apis/stop-job.asciidoc[] +include::apis/rollup-caps.asciidoc[] +include::apis/rollup-index-caps.asciidoc[] +include::apis/rollup-search.asciidoc[] +include::apis/rollup-job-config.asciidoc[] diff --git a/x-pack/docs/en/rollup/rollup-getting-started.asciidoc b/docs/reference/rollup/rollup-getting-started.asciidoc similarity index 99% rename from x-pack/docs/en/rollup/rollup-getting-started.asciidoc rename to docs/reference/rollup/rollup-getting-started.asciidoc index b6c913d7d34a..8f99bc2c010c 100644 --- a/x-pack/docs/en/rollup/rollup-getting-started.asciidoc +++ b/docs/reference/rollup/rollup-getting-started.asciidoc @@ -1,3 +1,5 @@ +[role="xpack"] +[testenv="basic"] [[rollup-getting-started]] == Getting Started diff --git a/x-pack/docs/en/rollup/rollup-search-limitations.asciidoc b/docs/reference/rollup/rollup-search-limitations.asciidoc similarity index 99% rename from x-pack/docs/en/rollup/rollup-search-limitations.asciidoc rename to docs/reference/rollup/rollup-search-limitations.asciidoc index 99f19a179ede..43feeab9a2ee 100644 --- a/x-pack/docs/en/rollup/rollup-search-limitations.asciidoc +++ b/docs/reference/rollup/rollup-search-limitations.asciidoc @@ -1,3 +1,5 @@ +[role="xpack"] +[testenv="basic"] [[rollup-search-limitations]] == Rollup Search Limitations diff --git a/x-pack/docs/en/rollup/understanding-groups.asciidoc b/docs/reference/rollup/understanding-groups.asciidoc similarity index 99% rename from x-pack/docs/en/rollup/understanding-groups.asciidoc rename to docs/reference/rollup/understanding-groups.asciidoc index 803555b2d73f..6321ab9b00f5 100644 --- a/x-pack/docs/en/rollup/understanding-groups.asciidoc +++ b/docs/reference/rollup/understanding-groups.asciidoc @@ -1,3 +1,5 @@ +[role="xpack"] +[testenv="basic"] [[rollup-understanding-groups]] == Understanding Groups From 874ebcb6d48b86dbcf539a5f34b91b4d1b38b6ea Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 31 Aug 2018 11:56:26 -0700 Subject: [PATCH 268/283] [DOCS] Moves ml folder from x-pack/docs to docs (#33248) --- docs/build.gradle | 6 ++ .../reference}/ml/aggregations.asciidoc | 4 +- .../reference}/ml/categories.asciidoc | 3 + .../reference}/ml/configuring.asciidoc | 12 +-- .../reference}/ml/customurl.asciidoc | 2 +- .../ml/detector-custom-rules.asciidoc | 7 +- .../reference}/ml/functions.asciidoc | 0 .../reference}/ml/functions/count.asciidoc | 7 ++ .../reference}/ml/functions/geo.asciidoc | 3 +- .../reference}/ml/functions/info.asciidoc | 0 .../reference}/ml/functions/metric.asciidoc | 0 .../reference}/ml/functions/rare.asciidoc | 0 .../reference}/ml/functions/sum.asciidoc | 0 .../reference}/ml/functions/time.asciidoc | 0 .../ml/images/ml-category-advanced.jpg | Bin .../ml/images/ml-category-anomalies.jpg | Bin .../reference}/ml/images/ml-categoryterms.jpg | Bin .../reference}/ml/images/ml-create-job.jpg | Bin .../reference}/ml/images/ml-create-jobs.jpg | Bin .../ml/images/ml-customurl-detail.jpg | Bin .../ml/images/ml-customurl-discover.jpg | Bin .../ml/images/ml-customurl-edit.jpg | Bin .../reference}/ml/images/ml-customurl.jpg | Bin .../reference}/ml/images/ml-data-dates.jpg | Bin .../reference}/ml/images/ml-data-keywords.jpg | Bin .../reference}/ml/images/ml-data-metrics.jpg | Bin .../ml/images/ml-data-topmetrics.jpg | Bin .../ml/images/ml-data-visualizer.jpg | Bin .../reference}/ml/images/ml-edit-job.jpg | Bin .../ml/images/ml-population-anomaly.jpg | Bin .../ml/images/ml-population-job.jpg | Bin .../ml/images/ml-population-results.jpg | Bin .../reference}/ml/images/ml-scriptfields.jpg | Bin .../reference}/ml/images/ml-start-feed.jpg | Bin .../reference}/ml/images/ml-stop-feed.jpg | Bin .../en => docs/reference}/ml/images/ml.jpg | Bin .../reference}/ml/populations.asciidoc | 5 +- .../reference}/ml/stopping-ml.asciidoc | 6 +- .../reference}/ml/transforms.asciidoc | 33 +++--- x-pack/docs/en/ml/api-quickref.asciidoc | 102 ------------------ 40 files changed, 50 insertions(+), 140 deletions(-) rename {x-pack/docs/en => docs/reference}/ml/aggregations.asciidoc (99%) rename {x-pack/docs/en => docs/reference}/ml/categories.asciidoc (99%) rename {x-pack/docs/en => docs/reference}/ml/configuring.asciidoc (88%) rename {x-pack/docs/en => docs/reference}/ml/customurl.asciidoc (99%) rename {x-pack/docs/en => docs/reference}/ml/detector-custom-rules.asciidoc (97%) rename {x-pack/docs/en => docs/reference}/ml/functions.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/ml/functions/count.asciidoc (97%) rename {x-pack/docs/en => docs/reference}/ml/functions/geo.asciidoc (98%) rename {x-pack/docs/en => docs/reference}/ml/functions/info.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/ml/functions/metric.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/ml/functions/rare.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/ml/functions/sum.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/ml/functions/time.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-category-advanced.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-category-anomalies.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-categoryterms.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-create-job.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-create-jobs.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-customurl-detail.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-customurl-discover.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-customurl-edit.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-customurl.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-data-dates.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-data-keywords.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-data-metrics.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-data-topmetrics.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-data-visualizer.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-edit-job.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-population-anomaly.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-population-job.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-population-results.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-scriptfields.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-start-feed.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml-stop-feed.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/images/ml.jpg (100%) rename {x-pack/docs/en => docs/reference}/ml/populations.asciidoc (94%) rename {x-pack/docs/en => docs/reference}/ml/stopping-ml.asciidoc (94%) rename {x-pack/docs/en => docs/reference}/ml/transforms.asciidoc (97%) delete mode 100644 x-pack/docs/en/ml/api-quickref.asciidoc diff --git a/docs/build.gradle b/docs/build.gradle index 8a23654f2e20..88bccfef4a3e 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -19,6 +19,12 @@ apply plugin: 'elasticsearch.docs-test' +/* List of files that have snippets that require a gold or platinum licence +and therefore cannot be tested yet... */ +buildRestTests.expectedUnconvertedCandidates = [ + 'reference/ml/transforms.asciidoc', +] + integTestCluster { /* Enable regexes in painless so our tests don't complain about example * snippets that use them. */ diff --git a/x-pack/docs/en/ml/aggregations.asciidoc b/docs/reference/ml/aggregations.asciidoc similarity index 99% rename from x-pack/docs/en/ml/aggregations.asciidoc rename to docs/reference/ml/aggregations.asciidoc index 07f465015696..4b873ea790b1 100644 --- a/x-pack/docs/en/ml/aggregations.asciidoc +++ b/docs/reference/ml/aggregations.asciidoc @@ -41,7 +41,7 @@ PUT _xpack/ml/anomaly_detectors/farequote } ---------------------------------- // CONSOLE -// TEST[setup:farequote_data] +// TEST[skip:setup:farequote_data] In this example, the `airline`, `responsetime`, and `time` fields are aggregations. @@ -90,7 +90,7 @@ PUT _xpack/ml/datafeeds/datafeed-farequote } ---------------------------------- // CONSOLE -// TEST[setup:farequote_job] +// TEST[skip:setup:farequote_job] In this example, the aggregations have names that match the fields that they operate on. That is to say, the `max` aggregation is named `time` and its diff --git a/x-pack/docs/en/ml/categories.asciidoc b/docs/reference/ml/categories.asciidoc similarity index 99% rename from x-pack/docs/en/ml/categories.asciidoc rename to docs/reference/ml/categories.asciidoc index 21f71b871cbb..03ebc8af76ee 100644 --- a/x-pack/docs/en/ml/categories.asciidoc +++ b/docs/reference/ml/categories.asciidoc @@ -44,6 +44,7 @@ PUT _xpack/ml/anomaly_detectors/it_ops_new_logs } ---------------------------------- //CONSOLE +// TEST[skip:needs-licence] <1> The `categorization_field_name` property indicates which field will be categorized. <2> The resulting categories are used in a detector by setting `by_field_name`, @@ -127,6 +128,7 @@ PUT _xpack/ml/anomaly_detectors/it_ops_new_logs2 } ---------------------------------- //CONSOLE +// TEST[skip:needs-licence] <1> The {ref}/analysis-pattern-replace-charfilter.html[`pattern_replace` character filter] here achieves exactly the same as the `categorization_filters` in the first @@ -193,6 +195,7 @@ PUT _xpack/ml/anomaly_detectors/it_ops_new_logs3 } ---------------------------------- //CONSOLE +// TEST[skip:needs-licence] <1> Tokens basically consist of hyphens, digits, letters, underscores and dots. <2> By default, categorization ignores tokens that begin with a digit. <3> By default, categorization also ignores tokens that are hexadecimal numbers. diff --git a/x-pack/docs/en/ml/configuring.asciidoc b/docs/reference/ml/configuring.asciidoc similarity index 88% rename from x-pack/docs/en/ml/configuring.asciidoc rename to docs/reference/ml/configuring.asciidoc index e35f046a82bd..9b6149d662aa 100644 --- a/x-pack/docs/en/ml/configuring.asciidoc +++ b/docs/reference/ml/configuring.asciidoc @@ -36,20 +36,20 @@ The scenarios in this section describe some best practices for generating useful * <> * <> -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/customurl.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/ml/customurl.asciidoc include::customurl.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/aggregations.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/ml/aggregations.asciidoc include::aggregations.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/categories.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/ml/categories.asciidoc include::categories.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/populations.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/ml/populations.asciidoc include::populations.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/transforms.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/ml/transforms.asciidoc include::transforms.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/detector-custom-rules.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/ml/detector-custom-rules.asciidoc include::detector-custom-rules.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/ml/customurl.asciidoc b/docs/reference/ml/customurl.asciidoc similarity index 99% rename from x-pack/docs/en/ml/customurl.asciidoc rename to docs/reference/ml/customurl.asciidoc index 7c197084c0e5..95f4f5f938f0 100644 --- a/x-pack/docs/en/ml/customurl.asciidoc +++ b/docs/reference/ml/customurl.asciidoc @@ -106,7 +106,7 @@ POST _xpack/ml/anomaly_detectors/sample_job/_update } ---------------------------------- //CONSOLE -//TEST[setup:sample_job] +//TEST[skip:setup:sample_job] When you click this custom URL in the anomalies table in {kib}, it opens up the *Discover* page and displays source data for the period one hour before and diff --git a/x-pack/docs/en/ml/detector-custom-rules.asciidoc b/docs/reference/ml/detector-custom-rules.asciidoc similarity index 97% rename from x-pack/docs/en/ml/detector-custom-rules.asciidoc rename to docs/reference/ml/detector-custom-rules.asciidoc index 8513c7e4d256..02881f4cc431 100644 --- a/x-pack/docs/en/ml/detector-custom-rules.asciidoc +++ b/docs/reference/ml/detector-custom-rules.asciidoc @@ -39,6 +39,7 @@ PUT _xpack/ml/filters/safe_domains } ---------------------------------- // CONSOLE +// TEST[skip:needs-licence] Now, we can create our job specifying a scope that uses the `safe_domains` filter for the `highest_registered_domain` field: @@ -70,6 +71,7 @@ PUT _xpack/ml/anomaly_detectors/dns_exfiltration_with_rule } ---------------------------------- // CONSOLE +// TEST[skip:needs-licence] As time advances and we see more data and more results, we might encounter new domains that we want to add in the filter. We can do that by using the @@ -83,7 +85,7 @@ POST _xpack/ml/filters/safe_domains/_update } ---------------------------------- // CONSOLE -// TEST[setup:ml_filter_safe_domains] +// TEST[skip:setup:ml_filter_safe_domains] Note that we can use any of the `partition_field_name`, `over_field_name`, or `by_field_name` fields in the `scope`. @@ -123,6 +125,7 @@ PUT _xpack/ml/anomaly_detectors/scoping_multiple_fields } ---------------------------------- // CONSOLE +// TEST[skip:needs-licence] Such a detector will skip results when the values of all 3 scoped fields are included in the referenced filters. @@ -166,6 +169,7 @@ PUT _xpack/ml/anomaly_detectors/cpu_with_rule } ---------------------------------- // CONSOLE +// TEST[skip:needs-licence] When there are multiple conditions they are combined with a logical `and`. This is useful when we want the rule to apply to a range. We simply create @@ -205,6 +209,7 @@ PUT _xpack/ml/anomaly_detectors/rule_with_range } ---------------------------------- // CONSOLE +// TEST[skip:needs-licence] ==== Custom rules in the life-cycle of a job diff --git a/x-pack/docs/en/ml/functions.asciidoc b/docs/reference/ml/functions.asciidoc similarity index 100% rename from x-pack/docs/en/ml/functions.asciidoc rename to docs/reference/ml/functions.asciidoc diff --git a/x-pack/docs/en/ml/functions/count.asciidoc b/docs/reference/ml/functions/count.asciidoc similarity index 97% rename from x-pack/docs/en/ml/functions/count.asciidoc rename to docs/reference/ml/functions/count.asciidoc index a2dc5645b61a..abbbd118ffeb 100644 --- a/x-pack/docs/en/ml/functions/count.asciidoc +++ b/docs/reference/ml/functions/count.asciidoc @@ -59,6 +59,7 @@ PUT _xpack/ml/anomaly_detectors/example1 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] This example is probably the simplest possible analysis. It identifies time buckets during which the overall count of events is higher or lower than @@ -86,6 +87,7 @@ PUT _xpack/ml/anomaly_detectors/example2 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] If you use this `high_count` function in a detector in your job, it models the event rate for each error code. It detects users that generate an @@ -110,6 +112,7 @@ PUT _xpack/ml/anomaly_detectors/example3 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] In this example, the function detects when the count of events for a status code is lower than usual. @@ -136,6 +139,7 @@ PUT _xpack/ml/anomaly_detectors/example4 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] If you are analyzing an aggregated `events_per_min` field, do not use a sum function (for example, `sum(events_per_min)`). Instead, use the count function @@ -200,6 +204,7 @@ PUT _xpack/ml/anomaly_detectors/example5 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] If you use this `high_non_zero_count` function in a detector in your job, it models the count of events for the `signaturename` field. It ignores any buckets @@ -253,6 +258,7 @@ PUT _xpack/ml/anomaly_detectors/example6 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] This `distinct_count` function detects when a system has an unusual number of logged in users. When you use this function in a detector in your job, it @@ -278,6 +284,7 @@ PUT _xpack/ml/anomaly_detectors/example7 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] This example detects instances of port scanning. When you use this function in a detector in your job, it models the distinct count of ports. It also detects the diff --git a/x-pack/docs/en/ml/functions/geo.asciidoc b/docs/reference/ml/functions/geo.asciidoc similarity index 98% rename from x-pack/docs/en/ml/functions/geo.asciidoc rename to docs/reference/ml/functions/geo.asciidoc index 5bcf6c339455..461ab825ff5b 100644 --- a/x-pack/docs/en/ml/functions/geo.asciidoc +++ b/docs/reference/ml/functions/geo.asciidoc @@ -47,6 +47,7 @@ PUT _xpack/ml/anomaly_detectors/example1 } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] If you use this `lat_long` function in a detector in your job, it detects anomalies where the geographic location of a credit card transaction is @@ -98,6 +99,6 @@ PUT _xpack/ml/datafeeds/datafeed-test2 } -------------------------------------------------- // CONSOLE -// TEST[setup:farequote_job] +// TEST[skip:setup:farequote_job] For more information, see <>. diff --git a/x-pack/docs/en/ml/functions/info.asciidoc b/docs/reference/ml/functions/info.asciidoc similarity index 100% rename from x-pack/docs/en/ml/functions/info.asciidoc rename to docs/reference/ml/functions/info.asciidoc diff --git a/x-pack/docs/en/ml/functions/metric.asciidoc b/docs/reference/ml/functions/metric.asciidoc similarity index 100% rename from x-pack/docs/en/ml/functions/metric.asciidoc rename to docs/reference/ml/functions/metric.asciidoc diff --git a/x-pack/docs/en/ml/functions/rare.asciidoc b/docs/reference/ml/functions/rare.asciidoc similarity index 100% rename from x-pack/docs/en/ml/functions/rare.asciidoc rename to docs/reference/ml/functions/rare.asciidoc diff --git a/x-pack/docs/en/ml/functions/sum.asciidoc b/docs/reference/ml/functions/sum.asciidoc similarity index 100% rename from x-pack/docs/en/ml/functions/sum.asciidoc rename to docs/reference/ml/functions/sum.asciidoc diff --git a/x-pack/docs/en/ml/functions/time.asciidoc b/docs/reference/ml/functions/time.asciidoc similarity index 100% rename from x-pack/docs/en/ml/functions/time.asciidoc rename to docs/reference/ml/functions/time.asciidoc diff --git a/x-pack/docs/en/ml/images/ml-category-advanced.jpg b/docs/reference/ml/images/ml-category-advanced.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-category-advanced.jpg rename to docs/reference/ml/images/ml-category-advanced.jpg diff --git a/x-pack/docs/en/ml/images/ml-category-anomalies.jpg b/docs/reference/ml/images/ml-category-anomalies.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-category-anomalies.jpg rename to docs/reference/ml/images/ml-category-anomalies.jpg diff --git a/x-pack/docs/en/ml/images/ml-categoryterms.jpg b/docs/reference/ml/images/ml-categoryterms.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-categoryterms.jpg rename to docs/reference/ml/images/ml-categoryterms.jpg diff --git a/x-pack/docs/en/ml/images/ml-create-job.jpg b/docs/reference/ml/images/ml-create-job.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-create-job.jpg rename to docs/reference/ml/images/ml-create-job.jpg diff --git a/x-pack/docs/en/ml/images/ml-create-jobs.jpg b/docs/reference/ml/images/ml-create-jobs.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-create-jobs.jpg rename to docs/reference/ml/images/ml-create-jobs.jpg diff --git a/x-pack/docs/en/ml/images/ml-customurl-detail.jpg b/docs/reference/ml/images/ml-customurl-detail.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-customurl-detail.jpg rename to docs/reference/ml/images/ml-customurl-detail.jpg diff --git a/x-pack/docs/en/ml/images/ml-customurl-discover.jpg b/docs/reference/ml/images/ml-customurl-discover.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-customurl-discover.jpg rename to docs/reference/ml/images/ml-customurl-discover.jpg diff --git a/x-pack/docs/en/ml/images/ml-customurl-edit.jpg b/docs/reference/ml/images/ml-customurl-edit.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-customurl-edit.jpg rename to docs/reference/ml/images/ml-customurl-edit.jpg diff --git a/x-pack/docs/en/ml/images/ml-customurl.jpg b/docs/reference/ml/images/ml-customurl.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-customurl.jpg rename to docs/reference/ml/images/ml-customurl.jpg diff --git a/x-pack/docs/en/ml/images/ml-data-dates.jpg b/docs/reference/ml/images/ml-data-dates.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-data-dates.jpg rename to docs/reference/ml/images/ml-data-dates.jpg diff --git a/x-pack/docs/en/ml/images/ml-data-keywords.jpg b/docs/reference/ml/images/ml-data-keywords.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-data-keywords.jpg rename to docs/reference/ml/images/ml-data-keywords.jpg diff --git a/x-pack/docs/en/ml/images/ml-data-metrics.jpg b/docs/reference/ml/images/ml-data-metrics.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-data-metrics.jpg rename to docs/reference/ml/images/ml-data-metrics.jpg diff --git a/x-pack/docs/en/ml/images/ml-data-topmetrics.jpg b/docs/reference/ml/images/ml-data-topmetrics.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-data-topmetrics.jpg rename to docs/reference/ml/images/ml-data-topmetrics.jpg diff --git a/x-pack/docs/en/ml/images/ml-data-visualizer.jpg b/docs/reference/ml/images/ml-data-visualizer.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-data-visualizer.jpg rename to docs/reference/ml/images/ml-data-visualizer.jpg diff --git a/x-pack/docs/en/ml/images/ml-edit-job.jpg b/docs/reference/ml/images/ml-edit-job.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-edit-job.jpg rename to docs/reference/ml/images/ml-edit-job.jpg diff --git a/x-pack/docs/en/ml/images/ml-population-anomaly.jpg b/docs/reference/ml/images/ml-population-anomaly.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-population-anomaly.jpg rename to docs/reference/ml/images/ml-population-anomaly.jpg diff --git a/x-pack/docs/en/ml/images/ml-population-job.jpg b/docs/reference/ml/images/ml-population-job.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-population-job.jpg rename to docs/reference/ml/images/ml-population-job.jpg diff --git a/x-pack/docs/en/ml/images/ml-population-results.jpg b/docs/reference/ml/images/ml-population-results.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-population-results.jpg rename to docs/reference/ml/images/ml-population-results.jpg diff --git a/x-pack/docs/en/ml/images/ml-scriptfields.jpg b/docs/reference/ml/images/ml-scriptfields.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-scriptfields.jpg rename to docs/reference/ml/images/ml-scriptfields.jpg diff --git a/x-pack/docs/en/ml/images/ml-start-feed.jpg b/docs/reference/ml/images/ml-start-feed.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-start-feed.jpg rename to docs/reference/ml/images/ml-start-feed.jpg diff --git a/x-pack/docs/en/ml/images/ml-stop-feed.jpg b/docs/reference/ml/images/ml-stop-feed.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml-stop-feed.jpg rename to docs/reference/ml/images/ml-stop-feed.jpg diff --git a/x-pack/docs/en/ml/images/ml.jpg b/docs/reference/ml/images/ml.jpg similarity index 100% rename from x-pack/docs/en/ml/images/ml.jpg rename to docs/reference/ml/images/ml.jpg diff --git a/x-pack/docs/en/ml/populations.asciidoc b/docs/reference/ml/populations.asciidoc similarity index 94% rename from x-pack/docs/en/ml/populations.asciidoc rename to docs/reference/ml/populations.asciidoc index bf0dd2ad7d7b..ed58c117f17d 100644 --- a/x-pack/docs/en/ml/populations.asciidoc +++ b/docs/reference/ml/populations.asciidoc @@ -51,14 +51,11 @@ PUT _xpack/ml/anomaly_detectors/population } ---------------------------------- //CONSOLE +// TEST[skip:needs-licence] <1> This `over_field_name` property indicates that the metrics for each user ( as identified by their `username` value) are analyzed relative to other users in each bucket. -//TO-DO: Per sophiec20 "Perhaps add the datafeed config and add a query filter to -//include only workstations as servers and printers would behave differently -//from the population - If your data is stored in {es}, you can use the population job wizard in {kib} to create a job with these same properties. For example, the population job wizard provides the following job settings: diff --git a/x-pack/docs/en/ml/stopping-ml.asciidoc b/docs/reference/ml/stopping-ml.asciidoc similarity index 94% rename from x-pack/docs/en/ml/stopping-ml.asciidoc rename to docs/reference/ml/stopping-ml.asciidoc index c0be2d947cdc..17505a02d152 100644 --- a/x-pack/docs/en/ml/stopping-ml.asciidoc +++ b/docs/reference/ml/stopping-ml.asciidoc @@ -28,7 +28,7 @@ request stops the `feed1` {dfeed}: POST _xpack/ml/datafeeds/datafeed-total-requests/_stop -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_startdf] +// TEST[skip:setup:server_metrics_startdf] NOTE: You must have `manage_ml`, or `manage` cluster privileges to stop {dfeeds}. For more information, see <>. @@ -49,6 +49,7 @@ If you are upgrading your cluster, you can use the following request to stop all POST _xpack/ml/datafeeds/_all/_stop ---------------------------------- // CONSOLE +// TEST[skip:needs-licence] [float] [[closing-ml-jobs]] @@ -67,7 +68,7 @@ example, the following request closes the `job1` job: POST _xpack/ml/anomaly_detectors/total-requests/_close -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_openjob] +// TEST[skip:setup:server_metrics_openjob] NOTE: You must have `manage_ml`, or `manage` cluster privileges to stop {dfeeds}. For more information, see <>. @@ -86,3 +87,4 @@ all open jobs on the cluster: POST _xpack/ml/anomaly_detectors/_all/_close ---------------------------------- // CONSOLE +// TEST[skip:needs-licence] diff --git a/x-pack/docs/en/ml/transforms.asciidoc b/docs/reference/ml/transforms.asciidoc similarity index 97% rename from x-pack/docs/en/ml/transforms.asciidoc rename to docs/reference/ml/transforms.asciidoc index c4b4d5602974..a2276895fc9e 100644 --- a/x-pack/docs/en/ml/transforms.asciidoc +++ b/docs/reference/ml/transforms.asciidoc @@ -95,7 +95,7 @@ PUT /my_index/my_type/1 } ---------------------------------- // CONSOLE -// TESTSETUP +// TEST[skip:SETUP] <1> In this example, string fields are mapped as `keyword` fields to support aggregation. If you want both a full text (`text`) and a keyword (`keyword`) version of the same field, use multi-fields. For more information, see @@ -144,7 +144,7 @@ PUT _xpack/ml/datafeeds/datafeed-test1 } ---------------------------------- // CONSOLE -// TEST[skip:broken] +// TEST[skip:needs-licence] <1> A script field named `total_error_count` is referenced in the detector within the job. <2> The script field is defined in the {dfeed}. @@ -163,7 +163,7 @@ You can preview the contents of the {dfeed} by using the following API: GET _xpack/ml/datafeeds/datafeed-test1/_preview ---------------------------------- // CONSOLE -// TEST[continued] +// TEST[skip:continued] In this example, the API returns the following results, which contain a sum of the `error_count` and `aborted_count` values: @@ -177,8 +177,6 @@ the `error_count` and `aborted_count` values: } ] ---------------------------------- -// TESTRESPONSE - NOTE: This example demonstrates how to use script fields, but it contains insufficient data to generate meaningful results. For a full demonstration of @@ -254,7 +252,7 @@ PUT _xpack/ml/datafeeds/datafeed-test2 GET _xpack/ml/datafeeds/datafeed-test2/_preview -------------------------------------------------- // CONSOLE -// TEST[skip:broken] +// TEST[skip:needs-licence] <1> The script field has a rather generic name in this case, since it will be used for various tests in the subsequent examples. <2> The script field uses the plus (+) operator to concatenate strings. @@ -271,7 +269,6 @@ and "SMITH " have been concatenated and an underscore was added: } ] ---------------------------------- -// TESTRESPONSE [[ml-configuring-transform3]] .Example 3: Trimming strings @@ -292,7 +289,7 @@ POST _xpack/ml/datafeeds/datafeed-test2/_update GET _xpack/ml/datafeeds/datafeed-test2/_preview -------------------------------------------------- // CONSOLE -// TEST[continued] +// TEST[skip:continued] <1> This script field uses the `trim()` function to trim extra white space from a string. @@ -308,7 +305,6 @@ has been trimmed to "SMITH": } ] ---------------------------------- -// TESTRESPONSE [[ml-configuring-transform4]] .Example 4: Converting strings to lowercase @@ -329,7 +325,7 @@ POST _xpack/ml/datafeeds/datafeed-test2/_update GET _xpack/ml/datafeeds/datafeed-test2/_preview -------------------------------------------------- // CONSOLE -// TEST[continued] +// TEST[skip:continued] <1> This script field uses the `toLowerCase` function to convert a string to all lowercase letters. Likewise, you can use the `toUpperCase{}` function to convert a string to uppercase letters. @@ -346,7 +342,6 @@ has been converted to "joe": } ] ---------------------------------- -// TESTRESPONSE [[ml-configuring-transform5]] .Example 5: Converting strings to mixed case formats @@ -367,7 +362,7 @@ POST _xpack/ml/datafeeds/datafeed-test2/_update GET _xpack/ml/datafeeds/datafeed-test2/_preview -------------------------------------------------- // CONSOLE -// TEST[continued] +// TEST[skip:continued] <1> This script field is a more complicated example of case manipulation. It uses the `subString()` function to capitalize the first letter of a string and converts the remaining characters to lowercase. @@ -384,7 +379,6 @@ has been converted to "Joe": } ] ---------------------------------- -// TESTRESPONSE [[ml-configuring-transform6]] .Example 6: Replacing tokens @@ -405,7 +399,7 @@ POST _xpack/ml/datafeeds/datafeed-test2/_update GET _xpack/ml/datafeeds/datafeed-test2/_preview -------------------------------------------------- // CONSOLE -// TEST[continued] +// TEST[skip:continued] <1> This script field uses regular expressions to replace white space with underscores. @@ -421,7 +415,6 @@ The preview {dfeed} API returns the following results, which show that } ] ---------------------------------- -// TESTRESPONSE [[ml-configuring-transform7]] .Example 7: Regular expression matching and concatenation @@ -442,7 +435,7 @@ POST _xpack/ml/datafeeds/datafeed-test2/_update GET _xpack/ml/datafeeds/datafeed-test2/_preview -------------------------------------------------- // CONSOLE -// TEST[continued] +// TEST[skip:continued] <1> This script field looks for a specific regular expression pattern and emits the matched groups as a concatenated string. If no match is found, it emits an empty string. @@ -459,7 +452,6 @@ The preview {dfeed} API returns the following results, which show that } ] ---------------------------------- -// TESTRESPONSE [[ml-configuring-transform8]] .Example 8: Splitting strings by domain name @@ -509,7 +501,7 @@ PUT _xpack/ml/datafeeds/datafeed-test3 GET _xpack/ml/datafeeds/datafeed-test3/_preview -------------------------------------------------- // CONSOLE -// TEST[skip:broken] +// TEST[skip:needs-licence] If you have a single field that contains a well-formed DNS domain name, you can use the `domainSplit()` function to split the string into its highest registered @@ -537,7 +529,6 @@ The preview {dfeed} API returns the following results, which show that } ] ---------------------------------- -// TESTRESPONSE [[ml-configuring-transform9]] .Example 9: Transforming geo_point data @@ -583,7 +574,7 @@ PUT _xpack/ml/datafeeds/datafeed-test4 GET _xpack/ml/datafeeds/datafeed-test4/_preview -------------------------------------------------- // CONSOLE -// TEST[skip:broken] +// TEST[skip:needs-licence] In {es}, location data can be stored in `geo_point` fields but this data type is not supported natively in {xpackml} analytics. This example of a script field @@ -602,4 +593,4 @@ The preview {dfeed} API returns the following results, which show that } ] ---------------------------------- -// TESTRESPONSE + diff --git a/x-pack/docs/en/ml/api-quickref.asciidoc b/x-pack/docs/en/ml/api-quickref.asciidoc deleted file mode 100644 index be74167862e1..000000000000 --- a/x-pack/docs/en/ml/api-quickref.asciidoc +++ /dev/null @@ -1,102 +0,0 @@ -[role="xpack"] -[[ml-api-quickref]] -== API quick reference - -All {ml} endpoints have the following base: - -[source,js] ----- -/_xpack/ml/ ----- -// NOTCONSOLE - -The main {ml} resources can be accessed with a variety of endpoints: - -* <>: Create and manage {ml} jobs -* <>: Select data from {es} to be analyzed -* <>: Access the results of a {ml} job -* <>: Manage model snapshots -//* <>: Validate subsections of job configurations - -[float] -[[ml-api-jobs]] -=== /anomaly_detectors/ - -* {ref}/ml-put-job.html[PUT /anomaly_detectors/+++]: Create a job -* {ref}/ml-open-job.html[POST /anomaly_detectors//_open]: Open a job -* {ref}/ml-post-data.html[POST /anomaly_detectors//_data]: Send data to a job -* {ref}/ml-get-job.html[GET /anomaly_detectors]: List jobs -* {ref}/ml-get-job.html[GET /anomaly_detectors/+++]: Get job details -* {ref}/ml-get-job-stats.html[GET /anomaly_detectors//_stats]: Get job statistics -* {ref}/ml-update-job.html[POST /anomaly_detectors//_update]: Update certain properties of the job configuration -* {ref}/ml-flush-job.html[POST anomaly_detectors//_flush]: Force a job to analyze buffered data -* {ref}/ml-forecast.html[POST anomaly_detectors//_forecast]: Forecast future job behavior -* {ref}/ml-close-job.html[POST /anomaly_detectors//_close]: Close a job -* {ref}/ml-delete-job.html[DELETE /anomaly_detectors/+++]: Delete a job - -[float] -[[ml-api-calendars]] -=== /calendars/ - -* {ref}/ml-put-calendar.html[PUT /calendars/+++]: Create a calendar -* {ref}/ml-post-calendar-event.html[POST /calendars/+++/events]: Add a scheduled event to a calendar -* {ref}/ml-put-calendar-job.html[PUT /calendars/+++/jobs/+++]: Associate a job with a calendar -* {ref}/ml-get-calendar.html[GET /calendars/+++]: Get calendar details -* {ref}/ml-get-calendar-event.html[GET /calendars/+++/events]: Get scheduled event details -* {ref}/ml-delete-calendar-event.html[DELETE /calendars/+++/events/+++]: Remove a scheduled event from a calendar -* {ref}/ml-delete-calendar-job.html[DELETE /calendars/+++/jobs/+++]: Disassociate a job from a calendar -* {ref}/ml-delete-calendar.html[DELETE /calendars/+++]: Delete a calendar - -[float] -[[ml-api-filters]] -=== /filters/ - -* {ref}/ml-put-filter.html[PUT /filters/+++]: Create a filter -* {ref}/ml-update-filter.html[POST /filters/+++/_update]: Update a filter -* {ref}/ml-get-filter.html[GET /filters/+++]: List filters -* {ref}/ml-delete-filter.html[DELETE /filter/+++]: Delete a filter - -[float] -[[ml-api-datafeeds]] -=== /datafeeds/ - -* {ref}/ml-put-datafeed.html[PUT /datafeeds/+++]: Create a {dfeed} -* {ref}/ml-start-datafeed.html[POST /datafeeds//_start]: Start a {dfeed} -* {ref}/ml-get-datafeed.html[GET /datafeeds]: List {dfeeds} -* {ref}/ml-get-datafeed.html[GET /datafeeds/+++]: Get {dfeed} details -* {ref}/ml-get-datafeed-stats.html[GET /datafeeds//_stats]: Get statistical information for {dfeeds} -* {ref}/ml-preview-datafeed.html[GET /datafeeds//_preview]: Get a preview of a {dfeed} -* {ref}/ml-update-datafeed.html[POST /datafeeds//_update]: Update certain settings for a {dfeed} -* {ref}/ml-stop-datafeed.html[POST /datafeeds//_stop]: Stop a {dfeed} -* {ref}/ml-delete-datafeed.html[DELETE /datafeeds/+++]: Delete {dfeed} - -[float] -[[ml-api-results]] -=== /results/ - -* {ref}/ml-get-bucket.html[GET /results/buckets]: List the buckets in the results -* {ref}/ml-get-bucket.html[GET /results/buckets/+++]: Get bucket details -* {ref}/ml-get-overall-buckets.html[GET /results/overall_buckets]: Get overall bucket results for multiple jobs -* {ref}/ml-get-category.html[GET /results/categories]: List the categories in the results -* {ref}/ml-get-category.html[GET /results/categories/+++]: Get category details -* {ref}/ml-get-influencer.html[GET /results/influencers]: Get influencer details -* {ref}/ml-get-record.html[GET /results/records]: Get records from the results - -[float] -[[ml-api-snapshots]] -=== /model_snapshots/ - -* {ref}/ml-get-snapshot.html[GET /model_snapshots]: List model snapshots -* {ref}/ml-get-snapshot.html[GET /model_snapshots/+++]: Get model snapshot details -* {ref}/ml-revert-snapshot.html[POST /model_snapshots//_revert]: Revert a model snapshot -* {ref}/ml-update-snapshot.html[POST /model_snapshots//_update]: Update certain settings for a model snapshot -* {ref}/ml-delete-snapshot.html[DELETE /model_snapshots/+++]: Delete a model snapshot - -//// -[float] -[[ml-api-validate]] -=== /validate/ - -* {ref}/ml-valid-detector.html[POST /anomaly_detectors/_validate/detector]: Validate a detector -* {ref}/ml-valid-job.html[POST /anomaly_detectors/_validate]: Validate a job -//// From d4f2b5be7de735ff4a5fb360c00ffc1741f3ddc4 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Fri, 31 Aug 2018 12:03:49 -0700 Subject: [PATCH 269/283] tracked at https://github.com/elastic/elasticsearch/issues/33320 and https://github.com/elastic/elasticsearch/issues/30777 --- .../elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java index 538d54416bf6..71dd17f0684b 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java @@ -135,6 +135,7 @@ protected Settings restAdminSettings() { } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33320") public void testSearchInputHasPermissions() throws Exception { try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); @@ -242,6 +243,7 @@ public void testSearchTransformInsufficientPermissions() throws Exception { assertThat(response.getStatusLine().getStatusCode(), is(404)); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/30777") public void testIndexActionHasPermissions() throws Exception { try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); From 3d82a30fadd035228f29ccc00d6c5bab71e9adf6 Mon Sep 17 00:00:00 2001 From: Vladimir Dolzhenko Date: Fri, 31 Aug 2018 21:29:06 +0200 Subject: [PATCH 270/283] drop `index.shard.check_on_startup: fix` (#32279) drop `index.shard.check_on_startup: fix` Relates #31389 --- docs/reference/index-modules.asciidoc | 4 +- .../elasticsearch/index/shard/IndexShard.java | 22 +-- .../org/elasticsearch/index/store/Store.java | 15 +- .../index/shard/IndexShardTests.java | 157 +++++++++++++++++- .../index/shard/IndexShardTestCase.java | 38 ++++- 5 files changed, 197 insertions(+), 39 deletions(-) diff --git a/docs/reference/index-modules.asciidoc b/docs/reference/index-modules.asciidoc index 54c0c1c1b157..214d77541779 100644 --- a/docs/reference/index-modules.asciidoc +++ b/docs/reference/index-modules.asciidoc @@ -65,9 +65,7 @@ corruption is detected, it will prevent the shard from being opened. Accepts: `fix`:: - Check for both physical and logical corruption. Segments that were reported - as corrupted will be automatically removed. This option *may result in data loss*. - Use with extreme caution! + The same as `false`. This option is deprecated and will be completely removed in 7.0. WARNING: Expert only. Checking shards may take a lot of time on large indices. -- 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 ef5f9ab0ef3e..24c78c72033f 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -301,6 +301,10 @@ public IndexShard( logger.debug("state: [CREATED]"); this.checkIndexOnStartup = indexSettings.getValue(IndexSettings.INDEX_CHECK_ON_STARTUP); + if ("fix".equals(checkIndexOnStartup)) { + deprecationLogger.deprecated("Setting [index.shard.check_on_startup] is set to deprecated value [fix], " + + "which has no effect and will not be accepted in future"); + } 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); @@ -1325,7 +1329,7 @@ private void innerOpenEngineAndTranslog() throws IOException { } recoveryState.setStage(RecoveryState.Stage.VERIFY_INDEX); // also check here, before we apply the translog - if (Booleans.isTrue(checkIndexOnStartup)) { + if (Booleans.isTrue(checkIndexOnStartup) || "checksum".equals(checkIndexOnStartup)) { try { checkIndex(); } catch (IOException ex) { @@ -1933,6 +1937,9 @@ void checkIndex() throws IOException { if (store.tryIncRef()) { try { doCheckIndex(); + } catch (IOException e) { + store.markStoreCorrupted(e); + throw e; } finally { store.decRef(); } @@ -1976,18 +1983,7 @@ private void doCheckIndex() throws IOException { return; } logger.warn("check index [failure]\n{}", os.bytes().utf8ToString()); - if ("fix".equals(checkIndexOnStartup)) { - if (logger.isDebugEnabled()) { - logger.debug("fixing index, writing new segments file ..."); - } - store.exorciseIndex(status); - if (logger.isDebugEnabled()) { - logger.debug("index fixed, wrote new segments file \"{}\"", status.segmentsFileName); - } - } else { - // only throw a failure if we are not going to fix the index - throw new IllegalStateException("index check failure but can't fix it"); - } + throw new IOException("index check failure"); } } 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 85975bc68c85..a00d779ca771 100644 --- a/server/src/main/java/org/elasticsearch/index/store/Store.java +++ b/server/src/main/java/org/elasticsearch/index/store/Store.java @@ -134,7 +134,8 @@ public class Store extends AbstractIndexShardComponent implements Closeable, Ref static final int VERSION_STACK_TRACE = 1; // we write the stack trace too since 1.4.0 static final int VERSION_START = 0; static final int VERSION = VERSION_WRITE_THROWABLE; - static final String CORRUPTED = "corrupted_"; + // public is for test purposes + public static final String CORRUPTED = "corrupted_"; public static final Setting INDEX_STORE_STATS_REFRESH_INTERVAL_SETTING = Setting.timeSetting("index.store.stats_refresh_interval", TimeValue.timeValueSeconds(10), Property.IndexScope); @@ -360,18 +361,6 @@ public CheckIndex.Status checkIndex(PrintStream out) throws IOException { } } - /** - * Repairs the index using the previous returned status from {@link #checkIndex(PrintStream)}. - */ - public void exorciseIndex(CheckIndex.Status status) throws IOException { - metadataLock.writeLock().lock(); - try (CheckIndex checkIndex = new CheckIndex(directory)) { - checkIndex.exorciseIndex(status); - } finally { - metadataLock.writeLock().unlock(); - } - } - public StoreStats stats() throws IOException { ensureOpen(); return new StoreStats(directory.estimateSize()); 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 50f95bf4d473..fdf7f5438d05 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -23,6 +23,7 @@ import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexCommit; import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TermQuery; @@ -118,6 +119,7 @@ import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.snapshots.SnapshotShardFailure; +import org.elasticsearch.test.CorruptionUtils; import org.elasticsearch.test.DummyShardLock; import org.elasticsearch.test.FieldMaskingReader; import org.elasticsearch.test.VersionUtils; @@ -126,7 +128,11 @@ import java.io.IOException; import java.nio.charset.Charset; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -1239,7 +1245,7 @@ public String[] listAll() throws IOException { }; try (Store store = createStore(shardId, new IndexSettings(metaData, Settings.EMPTY), directory)) { - IndexShard shard = newShard(shardRouting, shardPath, metaData, store, + IndexShard shard = newShard(shardRouting, shardPath, metaData, i -> store, null, new InternalEngineFactory(), () -> { }, EMPTY_EVENT_LISTENER); AtomicBoolean failureCallbackTriggered = new AtomicBoolean(false); @@ -2590,6 +2596,143 @@ public void testReadSnapshotConcurrently() throws IOException, InterruptedExcept closeShards(newShard); } + public void testIndexCheckOnStartup() throws Exception { + final IndexShard indexShard = newStartedShard(true); + + final long numDocs = between(10, 100); + for (long i = 0; i < numDocs; i++) { + indexDoc(indexShard, "_doc", Long.toString(i), "{}"); + } + indexShard.flush(new FlushRequest()); + closeShards(indexShard); + + final ShardPath shardPath = indexShard.shardPath(); + + final Path indexPath = corruptIndexFile(shardPath); + + final AtomicInteger corruptedMarkerCount = new AtomicInteger(); + final SimpleFileVisitor corruptedVisitor = new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (Files.isRegularFile(file) && file.getFileName().toString().startsWith(Store.CORRUPTED)) { + corruptedMarkerCount.incrementAndGet(); + } + return FileVisitResult.CONTINUE; + } + }; + Files.walkFileTree(indexPath, corruptedVisitor); + + assertThat("corruption marker should not be there", corruptedMarkerCount.get(), equalTo(0)); + + final ShardRouting shardRouting = ShardRoutingHelper.initWithSameId(indexShard.routingEntry(), + RecoverySource.StoreRecoverySource.EXISTING_STORE_INSTANCE + ); + // start shard and perform index check on startup. It enforce shard to fail due to corrupted index files + final IndexMetaData indexMetaData = IndexMetaData.builder(indexShard.indexSettings().getIndexMetaData()) + .settings(Settings.builder() + .put(indexShard.indexSettings.getSettings()) + .put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), randomFrom("true", "checksum"))) + .build(); + + IndexShard corruptedShard = newShard(shardRouting, shardPath, indexMetaData, + null, null, indexShard.engineFactory, + indexShard.getGlobalCheckpointSyncer(), EMPTY_EVENT_LISTENER); + + final IndexShardRecoveryException indexShardRecoveryException = + expectThrows(IndexShardRecoveryException.class, () -> newStartedShard(p -> corruptedShard, true)); + assertThat(indexShardRecoveryException.getMessage(), equalTo("failed recovery")); + + // check that corrupt marker is there + Files.walkFileTree(indexPath, corruptedVisitor); + assertThat("store has to be marked as corrupted", corruptedMarkerCount.get(), equalTo(1)); + + try { + closeShards(corruptedShard); + } catch (RuntimeException e) { + assertThat(e.getMessage(), equalTo("CheckIndex failed")); + } + } + + public void testShardDoesNotStartIfCorruptedMarkerIsPresent() throws Exception { + final IndexShard indexShard = newStartedShard(true); + + final long numDocs = between(10, 100); + for (long i = 0; i < numDocs; i++) { + indexDoc(indexShard, "_doc", Long.toString(i), "{}"); + } + indexShard.flush(new FlushRequest()); + closeShards(indexShard); + + final ShardPath shardPath = indexShard.shardPath(); + + final ShardRouting shardRouting = ShardRoutingHelper.initWithSameId(indexShard.routingEntry(), + RecoverySource.StoreRecoverySource.EXISTING_STORE_INSTANCE + ); + final IndexMetaData indexMetaData = indexShard.indexSettings().getIndexMetaData(); + + final Path indexPath = shardPath.getDataPath().resolve(ShardPath.INDEX_FOLDER_NAME); + + // create corrupted marker + final String corruptionMessage = "fake ioexception"; + try(Store store = createStore(indexShard.indexSettings(), shardPath)) { + store.markStoreCorrupted(new IOException(corruptionMessage)); + } + + // try to start shard on corrupted files + final IndexShard corruptedShard = newShard(shardRouting, shardPath, indexMetaData, + null, null, indexShard.engineFactory, + indexShard.getGlobalCheckpointSyncer(), EMPTY_EVENT_LISTENER); + + final IndexShardRecoveryException exception1 = expectThrows(IndexShardRecoveryException.class, + () -> newStartedShard(p -> corruptedShard, true)); + assertThat(exception1.getCause().getMessage(), equalTo(corruptionMessage + " (resource=preexisting_corruption)")); + closeShards(corruptedShard); + + final AtomicInteger corruptedMarkerCount = new AtomicInteger(); + final SimpleFileVisitor corruptedVisitor = new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (Files.isRegularFile(file) && file.getFileName().toString().startsWith(Store.CORRUPTED)) { + corruptedMarkerCount.incrementAndGet(); + } + return FileVisitResult.CONTINUE; + } + }; + Files.walkFileTree(indexPath, corruptedVisitor); + assertThat("store has to be marked as corrupted", corruptedMarkerCount.get(), equalTo(1)); + + // try to start another time shard on corrupted files + final IndexShard corruptedShard2 = newShard(shardRouting, shardPath, indexMetaData, + null, null, indexShard.engineFactory, + indexShard.getGlobalCheckpointSyncer(), EMPTY_EVENT_LISTENER); + + final IndexShardRecoveryException exception2 = expectThrows(IndexShardRecoveryException.class, + () -> newStartedShard(p -> corruptedShard2, true)); + assertThat(exception2.getCause().getMessage(), equalTo(corruptionMessage + " (resource=preexisting_corruption)")); + closeShards(corruptedShard2); + + // check that corrupt marker is there + corruptedMarkerCount.set(0); + Files.walkFileTree(indexPath, corruptedVisitor); + assertThat("store still has a single corrupt marker", corruptedMarkerCount.get(), equalTo(1)); + } + + private Path corruptIndexFile(ShardPath shardPath) throws IOException { + final Path indexPath = shardPath.getDataPath().resolve(ShardPath.INDEX_FOLDER_NAME); + final Path[] filesToCorrupt = + Files.walk(indexPath) + .filter(p -> { + final String name = p.getFileName().toString(); + return Files.isRegularFile(p) + && name.startsWith("extra") == false // Skip files added by Lucene's ExtrasFS + && IndexWriter.WRITE_LOCK_NAME.equals(name) == false + && name.startsWith("segments_") == false && name.endsWith(".si") == false; + }) + .toArray(Path[]::new); + CorruptionUtils.corruptFile(random(), filesToCorrupt); + return indexPath; + } + /** * Simulates a scenario that happens when we are async fetching snapshot metadata from GatewayService * and checking index concurrently. This should always be possible without any exception. @@ -2613,7 +2756,7 @@ public void testReadSnapshotAndCheckIndexConcurrently() throws Exception { final IndexMetaData indexMetaData = IndexMetaData.builder(indexShard.indexSettings().getIndexMetaData()) .settings(Settings.builder() .put(indexShard.indexSettings.getSettings()) - .put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), randomFrom("false", "true", "checksum", "fix"))) + .put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), randomFrom("false", "true", "checksum"))) .build(); final IndexShard newShard = newShard(shardRouting, indexShard.shardPath(), indexMetaData, null, null, indexShard.engineFactory, indexShard.getGlobalCheckpointSyncer(), EMPTY_EVENT_LISTENER); @@ -2655,6 +2798,16 @@ public void testReadSnapshotAndCheckIndexConcurrently() throws Exception { closeShards(newShard); } + public void testCheckOnStartupDeprecatedValue() throws Exception { + final Settings settings = Settings.builder().put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), "fix").build(); + + final IndexShard newShard = newShard(true, settings); + closeShards(newShard); + + assertWarnings("Setting [index.shard.check_on_startup] is set to deprecated value [fix], " + + "which has no effect and will not be accepted in future"); + } + class Result { private final int localCheckpoint; private final int maxSeqNo; 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 2f4a3dfd6c12..375e74f6cca3 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 @@ -32,6 +32,7 @@ import org.elasticsearch.cluster.routing.ShardRoutingHelper; import org.elasticsearch.cluster.routing.ShardRoutingState; import org.elasticsearch.cluster.routing.TestShardRouting; +import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.lucene.uid.Versions; @@ -156,7 +157,6 @@ public Settings threadPoolSettings() { return Settings.EMPTY; } - protected Store createStore(IndexSettings indexSettings, ShardPath shardPath) throws IOException { return createStore(shardPath.getShardId(), indexSettings, newFSDirectory(shardPath.resolveIndex())); } @@ -169,7 +169,6 @@ public Directory newDirectory() throws IOException { } }; return new Store(shardId, indexSettings, directoryService, new DummyShardLock(shardId)); - } /** @@ -179,7 +178,17 @@ public Directory newDirectory() throws IOException { * another shard) */ protected IndexShard newShard(boolean primary) throws IOException { - return newShard(primary, Settings.EMPTY, new InternalEngineFactory()); + return newShard(primary, Settings.EMPTY); + } + + /** + * Creates a new initializing shard. The shard will have its own unique data path. + * + * @param primary indicates whether to a primary shard (ready to recover from an empty store) or a replica (ready to recover from + * another shard) + */ + protected IndexShard newShard(final boolean primary, final Settings settings) throws IOException { + return newShard(primary, settings, new InternalEngineFactory()); } /** @@ -318,23 +327,25 @@ protected IndexShard newShard(ShardRouting routing, IndexMetaData indexMetaData, * @param routing shard routing to use * @param shardPath path to use for shard data * @param indexMetaData indexMetaData for the shard, including any mapping - * @param store an optional custom store to use. If null a default file based store will be created + * @param storeProvider an optional custom store provider to use. If null a default file based store will be created * @param indexSearcherWrapper an optional wrapper to be used during searchers * @param globalCheckpointSyncer callback for syncing global checkpoints * @param indexEventListener index event listener * @param listeners an optional set of listeners to add to the shard */ protected IndexShard newShard(ShardRouting routing, ShardPath shardPath, IndexMetaData indexMetaData, - @Nullable Store store, @Nullable IndexSearcherWrapper indexSearcherWrapper, + @Nullable CheckedFunction storeProvider, + @Nullable IndexSearcherWrapper indexSearcherWrapper, @Nullable EngineFactory engineFactory, Runnable globalCheckpointSyncer, IndexEventListener indexEventListener, IndexingOperationListener... listeners) throws IOException { final Settings nodeSettings = Settings.builder().put("node.name", routing.currentNodeId()).build(); final IndexSettings indexSettings = new IndexSettings(indexMetaData, nodeSettings); final IndexShard indexShard; - if (store == null) { - store = createStore(indexSettings, shardPath); + if (storeProvider == null) { + storeProvider = is -> createStore(is, shardPath); } + final Store store = storeProvider.apply(indexSettings); boolean success = false; try { IndexCache indexCache = new IndexCache(indexSettings, new DisabledQueryCache(indexSettings), null); @@ -424,7 +435,18 @@ protected IndexShard newStartedShard(final boolean primary) throws IOException { */ protected IndexShard newStartedShard( final boolean primary, final Settings settings, final EngineFactory engineFactory) throws IOException { - IndexShard shard = newShard(primary, settings, engineFactory); + return newStartedShard(p -> newShard(p, settings, engineFactory), primary); + } + + /** + * creates a new empty shard and starts it. + * + * @param shardFunction shard factory function + * @param primary controls whether the shard will be a primary or a replica. + */ + protected IndexShard newStartedShard(CheckedFunction shardFunction, + boolean primary) throws IOException { + IndexShard shard = shardFunction.apply(primary); if (primary) { recoverShardFromStore(shard); } else { From 4f1ffb5cb1b5163be02aba9bb54a5f872507a17c Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Fri, 31 Aug 2018 13:33:14 -0600 Subject: [PATCH 271/283] Mute SmokeTestWatcherWithSecurityIT testsi Tests from the SmokeTestWatcherWithSecurityIT suite have been failing occasionally. This commit mutes all the tests. This is tracked in --- .../elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java index 71dd17f0684b..b4d60d3708ec 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java @@ -186,6 +186,7 @@ public void testSearchInputWithInsufficientPrivileges() throws Exception { assertThat(conditionMet, is(false)); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/29893") public void testSearchTransformHasPermissions() throws Exception { try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); @@ -268,6 +269,7 @@ public void testIndexActionHasPermissions() throws Exception { assertThat(spam, is("eggs")); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/30777") public void testIndexActionInsufficientPrivileges() throws Exception { try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); From 6a77cb4211498006b681f46b20feb6b957f8e87d Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Fri, 31 Aug 2018 13:37:22 -0600 Subject: [PATCH 272/283] Fix AwaitsFix issue number In the previous commit where SmokeTestWatcherWithSecurityIT tests were muted, I added the incorrect issue numbers. This commit fixes this. The issue for the tests is #33320. --- .../smoketest/SmokeTestWatcherWithSecurityIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java index b4d60d3708ec..17fbf0769fd4 100644 --- a/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java +++ b/x-pack/qa/smoke-test-watcher-with-security/src/test/java/org/elasticsearch/smoketest/SmokeTestWatcherWithSecurityIT.java @@ -186,7 +186,7 @@ public void testSearchInputWithInsufficientPrivileges() throws Exception { assertThat(conditionMet, is(false)); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/29893") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33320") public void testSearchTransformHasPermissions() throws Exception { try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); @@ -269,7 +269,7 @@ public void testIndexActionHasPermissions() throws Exception { assertThat(spam, is("eggs")); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/30777") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33320") public void testIndexActionInsufficientPrivileges() throws Exception { try (XContentBuilder builder = jsonBuilder()) { builder.startObject(); From 00b272af327b7bfff63b5b8305a040b8b6dbfdb2 Mon Sep 17 00:00:00 2001 From: Vladimir Dolzhenko Date: Fri, 31 Aug 2018 22:05:40 +0200 Subject: [PATCH 273/283] completely drop `index.shard.check_on_startup: fix` for 7.0 (#33194) Relates to #32279 --- docs/reference/index-modules.asciidoc | 4 ---- docs/reference/migration/migrate_7_0/indices.asciidoc | 4 ++++ .../java/org/elasticsearch/index/IndexSettings.java | 3 +-- .../java/org/elasticsearch/index/shard/IndexShard.java | 4 ---- .../put/MetaDataIndexTemplateServiceTests.java | 2 +- .../org/elasticsearch/index/shard/IndexShardTests.java | 10 ---------- 6 files changed, 6 insertions(+), 21 deletions(-) diff --git a/docs/reference/index-modules.asciidoc b/docs/reference/index-modules.asciidoc index 214d77541779..53de67e55fdf 100644 --- a/docs/reference/index-modules.asciidoc +++ b/docs/reference/index-modules.asciidoc @@ -63,10 +63,6 @@ corruption is detected, it will prevent the shard from being opened. Accepts: Check for both physical and logical corruption. This is much more expensive in terms of CPU and memory usage. -`fix`:: - - The same as `false`. This option is deprecated and will be completely removed in 7.0. - WARNING: Expert only. Checking shards may take a lot of time on large indices. -- diff --git a/docs/reference/migration/migrate_7_0/indices.asciidoc b/docs/reference/migration/migrate_7_0/indices.asciidoc index bab7b6022201..a47cc6f4324a 100644 --- a/docs/reference/migration/migrate_7_0/indices.asciidoc +++ b/docs/reference/migration/migrate_7_0/indices.asciidoc @@ -78,3 +78,7 @@ The parent circuit breaker defines a new setting `indices.breaker.total.use_real heap memory instead of only considering the reserved memory by child circuit breakers. When this setting is `true`, the default parent breaker limit also changes from 70% to 95% of the JVM heap size. The previous behavior can be restored by setting `indices.breaker.total.use_real_memory` to `false`. + +==== `fix` value for `index.shard.check_on_startup` is removed + +Deprecated option value `fix` for setting `index.shard.check_on_startup` is not supported. \ No newline at end of file diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 3ea022bbebd4..2bfd5ed07079 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -75,11 +75,10 @@ public final class IndexSettings { switch(s) { case "false": case "true": - case "fix": case "checksum": return s; default: - throw new IllegalArgumentException("unknown value for [index.shard.check_on_startup] must be one of [true, false, fix, checksum] but was: " + s); + throw new IllegalArgumentException("unknown value for [index.shard.check_on_startup] must be one of [true, false, checksum] but was: " + s); } }, Property.IndexScope); 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 24c78c72033f..3cdb76db9279 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -301,10 +301,6 @@ public IndexShard( logger.debug("state: [CREATED]"); this.checkIndexOnStartup = indexSettings.getValue(IndexSettings.INDEX_CHECK_ON_STARTUP); - if ("fix".equals(checkIndexOnStartup)) { - deprecationLogger.deprecated("Setting [index.shard.check_on_startup] is set to deprecated value [fix], " - + "which has no effect and will not be accepted in future"); - } 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); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java index f0e9a57f7f3e..39f04c6b7b09 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/template/put/MetaDataIndexTemplateServiceTests.java @@ -69,7 +69,7 @@ public void testIndexTemplateInvalidNumberOfShards() { containsString("Failed to parse value [0] for setting [index.number_of_shards] must be >= 1")); assertThat(throwables.get(0).getMessage(), containsString("unknown value for [index.shard.check_on_startup] " + - "must be one of [true, false, fix, checksum] but was: blargh")); + "must be one of [true, false, checksum] but was: blargh")); } public void testIndexTemplateValidationAccumulatesValidationErrors() { 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 fdf7f5438d05..584ed7085a8a 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -2798,16 +2798,6 @@ public void testReadSnapshotAndCheckIndexConcurrently() throws Exception { closeShards(newShard); } - public void testCheckOnStartupDeprecatedValue() throws Exception { - final Settings settings = Settings.builder().put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), "fix").build(); - - final IndexShard newShard = newShard(true, settings); - closeShards(newShard); - - assertWarnings("Setting [index.shard.check_on_startup] is set to deprecated value [fix], " - + "which has no effect and will not be accepted in future"); - } - class Result { private final int localCheckpoint; private final int maxSeqNo; From 08b9247ce226bfc47ebe4f859d97b5ed87940e69 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 31 Aug 2018 16:50:08 -0400 Subject: [PATCH 274/283] Adjust soft-deletes version after backport into 6.5 Relates #33222 --- server/src/main/java/org/elasticsearch/index/IndexSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 2bfd5ed07079..6a612091b979 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -416,7 +416,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti generationThresholdSize = scopedSettings.get(INDEX_TRANSLOG_GENERATION_THRESHOLD_SIZE_SETTING); mergeSchedulerConfig = new MergeSchedulerConfig(this); gcDeletesInMillis = scopedSettings.get(INDEX_GC_DELETES_SETTING).getMillis(); - softDeleteEnabled = version.onOrAfter(Version.V_7_0_0_alpha1) && scopedSettings.get(INDEX_SOFT_DELETES_SETTING); + softDeleteEnabled = version.onOrAfter(Version.V_6_5_0) && scopedSettings.get(INDEX_SOFT_DELETES_SETTING); softDeleteRetentionOperations = scopedSettings.get(INDEX_SOFT_DELETES_RETENTION_OPERATIONS_SETTING); warmerEnabled = scopedSettings.get(INDEX_WARMER_ENABLED_SETTING); maxResultWindow = scopedSettings.get(MAX_RESULT_WINDOW_SETTING); From ebed8f26187fc315b095106a39edbe46d0ed5778 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Fri, 31 Aug 2018 17:18:22 -0400 Subject: [PATCH 275/283] [Rollup] Fix FullClusterRestart test We need to wait for the job to fully initialize and start before we can attempt to stop it. If we don't, it's possible for the stop API to be called before the persistent task is fully loaded and it'll throw an exception. Closes #32773 --- .../org/elasticsearch/xpack/restart/FullClusterRestartIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java index 6ead87aba610..7c4eda37d2fb 100644 --- a/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java +++ b/x-pack/qa/full-cluster-restart/src/test/java/org/elasticsearch/xpack/restart/FullClusterRestartIT.java @@ -325,7 +325,6 @@ public void testRollupAfterRestart() throws Exception { } } - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/32773") public void testRollupIDSchemeAfterRestart() throws Exception { assumeTrue("Rollup can be tested with 6.3.0 and onwards", oldClusterVersion.onOrAfter(Version.V_6_3_0)); assumeTrue("Rollup ID scheme changed in 6.4", oldClusterVersion.before(Version.V_6_4_0)); @@ -393,6 +392,8 @@ public void testRollupIDSchemeAfterRestart() throws Exception { indexRequest.setJsonEntity("{\"timestamp\":\"2018-01-02T00:00:01\",\"value\":345}"); client().performRequest(indexRequest); + assertRollUpJob("rollup-id-test"); + // stop the rollup job to force a state save, which will upgrade the ID final Request stopRollupJobRequest = new Request("POST", "_xpack/rollup/job/rollup-id-test/_stop"); Map stopRollupJobResponse = entityAsMap(client().performRequest(stopRollupJobRequest)); From ca94d052b85aa03c4ac610b8d3423aaf9598e0f0 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Fri, 31 Aug 2018 18:48:19 -0400 Subject: [PATCH 276/283] Mute test watcher usage stats output Tracked at #33326 --- .../resources/rest-api-spec/test/watcher/usage/10_basic.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/watcher/usage/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/watcher/usage/10_basic.yml index a33fcdb52974..7a22ad322bfc 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/watcher/usage/10_basic.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/watcher/usage/10_basic.yml @@ -1,6 +1,8 @@ --- "Test watcher usage stats output": - + - skip: + version: "all" + reason: AwaitsFix at https://github.com/elastic/elasticsearch/issues/33326 - do: catch: missing xpack.watcher.delete_watch: From b7a63f7e7dcf24771d0f0a225d3f01d9f969367b Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 31 Aug 2018 16:49:24 -0700 Subject: [PATCH 277/283] [DOCS] Moves machine learning APIs to docs folder (#31118) --- docs/build.gradle | 235 +++++++++++++++++- .../ml/apis}/calendarresource.asciidoc | 1 + .../reference/ml/apis}/close-job.asciidoc | 3 +- .../ml/apis}/datafeedresource.asciidoc | 1 + .../ml/apis}/delete-calendar-event.asciidoc | 4 +- .../ml/apis}/delete-calendar-job.asciidoc | 5 +- .../ml/apis}/delete-calendar.asciidoc | 5 +- .../ml/apis}/delete-datafeed.asciidoc | 3 +- .../reference/ml/apis}/delete-filter.asciidoc | 5 +- .../reference/ml/apis}/delete-job.asciidoc | 5 +- .../ml/apis}/delete-snapshot.asciidoc | 3 +- .../reference/ml/apis}/eventresource.asciidoc | 1 + .../ml/apis}/filterresource.asciidoc | 1 + .../reference/ml/apis}/flush-job.asciidoc | 7 +- .../reference/ml/apis}/forecast.asciidoc | 1 + .../reference/ml/apis}/get-bucket.asciidoc | 2 +- .../ml/apis}/get-calendar-event.asciidoc | 3 +- .../reference/ml/apis}/get-calendar.asciidoc | 5 +- .../reference/ml/apis}/get-category.asciidoc | 3 +- .../ml/apis}/get-datafeed-stats.asciidoc | 5 +- .../reference/ml/apis}/get-datafeed.asciidoc | 3 +- .../reference/ml/apis}/get-filter.asciidoc | 5 +- .../ml/apis}/get-influencer.asciidoc | 1 + .../reference/ml/apis}/get-job-stats.asciidoc | 1 + .../reference/ml/apis}/get-job.asciidoc | 3 +- .../ml/apis}/get-overall-buckets.asciidoc | 2 +- .../reference/ml/apis}/get-record.asciidoc | 1 + .../reference/ml/apis}/get-snapshot.asciidoc | 1 + .../reference/ml/apis}/jobcounts.asciidoc | 1 + .../reference/ml/apis}/jobresource.asciidoc | 1 + .../reference/ml/apis}/ml-api.asciidoc | 83 ++++--- .../reference/ml/apis}/open-job.asciidoc | 4 +- .../ml/apis}/post-calendar-event.asciidoc | 5 +- .../reference/ml/apis}/post-data.asciidoc | 1 + .../ml/apis}/preview-datafeed.asciidoc | 3 +- .../ml/apis}/put-calendar-job.asciidoc | 5 +- .../reference/ml/apis}/put-calendar.asciidoc | 4 +- .../reference/ml/apis}/put-datafeed.asciidoc | 5 +- .../reference/ml/apis}/put-filter.asciidoc | 4 +- .../reference/ml/apis}/put-job.asciidoc | 2 + .../ml/apis}/resultsresource.asciidoc | 1 + .../ml/apis}/revert-snapshot.asciidoc | 29 +-- .../ml/apis}/snapshotresource.asciidoc | 1 + .../ml/apis}/start-datafeed.asciidoc | 7 +- .../reference/ml/apis}/stop-datafeed.asciidoc | 8 +- .../ml/apis}/update-datafeed.asciidoc | 3 +- .../reference/ml/apis}/update-filter.asciidoc | 5 +- .../reference/ml/apis}/update-job.asciidoc | 5 +- .../ml/apis}/update-snapshot.asciidoc | 1 + .../ml/apis}/validate-detector.asciidoc | 2 + .../reference/ml/apis}/validate-job.asciidoc | 2 + docs/reference/redirects.asciidoc | 6 + docs/reference/rest-api/defs.asciidoc | 27 ++ docs/reference/rest-api/index.asciidoc | 4 +- x-pack/docs/build.gradle | 11 - x-pack/docs/en/rest-api/defs.asciidoc | 36 --- 56 files changed, 408 insertions(+), 173 deletions(-) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/calendarresource.asciidoc (94%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/close-job.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/datafeedresource.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/delete-calendar-event.asciidoc (96%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/delete-calendar-job.asciidoc (93%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/delete-calendar.asciidoc (92%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/delete-datafeed.asciidoc (94%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/delete-filter.asciidoc (92%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/delete-job.asciidoc (95%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/delete-snapshot.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/eventresource.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/filterresource.asciidoc (95%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/flush-job.asciidoc (92%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/forecast.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-bucket.asciidoc (98%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-calendar-event.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-calendar.asciidoc (94%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-category.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-datafeed-stats.asciidoc (96%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-datafeed.asciidoc (96%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-filter.asciidoc (94%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-influencer.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-job-stats.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-job.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-overall-buckets.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-record.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/get-snapshot.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/jobcounts.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/jobresource.asciidoc (99%) rename {x-pack/docs/en/rest-api => docs/reference/ml/apis}/ml-api.asciidoc (61%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/open-job.asciidoc (95%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/post-calendar-event.asciidoc (96%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/post-data.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/preview-datafeed.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/put-calendar-job.asciidoc (93%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/put-calendar.asciidoc (94%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/put-datafeed.asciidoc (98%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/put-filter.asciidoc (95%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/put-job.asciidoc (98%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/resultsresource.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/revert-snapshot.asciidoc (67%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/snapshotresource.asciidoc (99%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/start-datafeed.asciidoc (97%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/stop-datafeed.asciidoc (92%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/update-datafeed.asciidoc (98%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/update-filter.asciidoc (94%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/update-job.asciidoc (98%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/update-snapshot.asciidoc (98%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/validate-detector.asciidoc (95%) rename {x-pack/docs/en/rest-api/ml => docs/reference/ml/apis}/validate-job.asciidoc (96%) create mode 100644 docs/reference/rest-api/defs.asciidoc delete mode 100644 x-pack/docs/en/rest-api/defs.asciidoc diff --git a/docs/build.gradle b/docs/build.gradle index 88bccfef4a3e..c6a7a8d48374 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -19,10 +19,20 @@ apply plugin: 'elasticsearch.docs-test' -/* List of files that have snippets that require a gold or platinum licence -and therefore cannot be tested yet... */ +/* List of files that have snippets that will not work until platinum tests can occur ... */ buildRestTests.expectedUnconvertedCandidates = [ 'reference/ml/transforms.asciidoc', + 'reference/ml/apis/delete-calendar-event.asciidoc', + 'reference/ml/apis/get-bucket.asciidoc', + 'reference/ml/apis/get-category.asciidoc', + 'reference/ml/apis/get-influencer.asciidoc', + 'reference/ml/apis/get-job-stats.asciidoc', + 'reference/ml/apis/get-overall-buckets.asciidoc', + 'reference/ml/apis/get-record.asciidoc', + 'reference/ml/apis/get-snapshot.asciidoc', + 'reference/ml/apis/post-data.asciidoc', + 'reference/ml/apis/revert-snapshot.asciidoc', + 'reference/ml/apis/update-snapshot.asciidoc', ] integTestCluster { @@ -867,3 +877,224 @@ buildRestTests.setups['sensor_prefab_data'] = ''' {"node.terms.value":"c","temperature.sum.value":202.0,"temperature.max.value":202.0,"timestamp.date_histogram.time_zone":"UTC","temperature.min.value":202.0,"timestamp.date_histogram._count":1,"timestamp.date_histogram.interval":"1h","_rollup.computed":["temperature.sum","temperature.min","voltage.avg","temperature.max","node.terms","timestamp.date_histogram"],"voltage.avg.value":4.0,"node.terms._count":1,"_rollup.version":1,"timestamp.date_histogram.timestamp":1516294800000,"voltage.avg._count":1.0,"_rollup.id":"sensor"} ''' +buildRestTests.setups['sample_job'] = ''' + - do: + xpack.ml.put_job: + job_id: "sample_job" + body: > + { + "description" : "Very basic job", + "analysis_config" : { + "bucket_span":"10m", + "detectors" :[ + { + "function": "count" + } + ]}, + "data_description" : { + "time_field":"timestamp", + "time_format": "epoch_ms" + } + } +''' +buildRestTests.setups['farequote_index'] = ''' + - do: + indices.create: + index: farequote + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + metric: + properties: + time: + type: date + responsetime: + type: float + airline: + type: keyword + doc_count: + type: integer +''' +buildRestTests.setups['farequote_data'] = buildRestTests.setups['farequote_index'] + ''' + - do: + bulk: + index: farequote + type: metric + refresh: true + body: | + {"index": {"_id":"1"}} + {"airline":"JZA","responsetime":990.4628,"time":"2016-02-07T00:00:00+0000", "doc_count": 5} + {"index": {"_id":"2"}} + {"airline":"JBU","responsetime":877.5927,"time":"2016-02-07T00:00:00+0000", "doc_count": 23} + {"index": {"_id":"3"}} + {"airline":"KLM","responsetime":1355.4812,"time":"2016-02-07T00:00:00+0000", "doc_count": 42} +''' +buildRestTests.setups['farequote_job'] = buildRestTests.setups['farequote_data'] + ''' + - do: + xpack.ml.put_job: + job_id: "farequote" + body: > + { + "analysis_config": { + "bucket_span": "60m", + "detectors": [{ + "function": "mean", + "field_name": "responsetime", + "by_field_name": "airline" + }], + "summary_count_field_name": "doc_count" + }, + "data_description": { + "time_field": "time" + } + } +''' +buildRestTests.setups['farequote_datafeed'] = buildRestTests.setups['farequote_job'] + ''' + - do: + xpack.ml.put_datafeed: + datafeed_id: "datafeed-farequote" + body: > + { + "job_id":"farequote", + "indexes":"farequote" + } +''' +buildRestTests.setups['server_metrics_index'] = ''' + - do: + indices.create: + index: server-metrics + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + metric: + properties: + timestamp: + type: date + total: + type: long +''' +buildRestTests.setups['server_metrics_data'] = buildRestTests.setups['server_metrics_index'] + ''' + - do: + bulk: + index: server-metrics + type: metric + refresh: true + body: | + {"index": {"_id":"1177"}} + {"timestamp":"2017-03-23T13:00:00","total":40476} + {"index": {"_id":"1178"}} + {"timestamp":"2017-03-23T13:00:00","total":15287} + {"index": {"_id":"1179"}} + {"timestamp":"2017-03-23T13:00:00","total":-776} + {"index": {"_id":"1180"}} + {"timestamp":"2017-03-23T13:00:00","total":11366} + {"index": {"_id":"1181"}} + {"timestamp":"2017-03-23T13:00:00","total":3606} + {"index": {"_id":"1182"}} + {"timestamp":"2017-03-23T13:00:00","total":19006} + {"index": {"_id":"1183"}} + {"timestamp":"2017-03-23T13:00:00","total":38613} + {"index": {"_id":"1184"}} + {"timestamp":"2017-03-23T13:00:00","total":19516} + {"index": {"_id":"1185"}} + {"timestamp":"2017-03-23T13:00:00","total":-258} + {"index": {"_id":"1186"}} + {"timestamp":"2017-03-23T13:00:00","total":9551} + {"index": {"_id":"1187"}} + {"timestamp":"2017-03-23T13:00:00","total":11217} + {"index": {"_id":"1188"}} + {"timestamp":"2017-03-23T13:00:00","total":22557} + {"index": {"_id":"1189"}} + {"timestamp":"2017-03-23T13:00:00","total":40508} + {"index": {"_id":"1190"}} + {"timestamp":"2017-03-23T13:00:00","total":11887} + {"index": {"_id":"1191"}} + {"timestamp":"2017-03-23T13:00:00","total":31659} +''' +buildRestTests.setups['server_metrics_job'] = buildRestTests.setups['server_metrics_data'] + ''' + - do: + xpack.ml.put_job: + job_id: "total-requests" + body: > + { + "description" : "Total sum of requests", + "analysis_config" : { + "bucket_span":"10m", + "detectors" :[ + { + "detector_description": "Sum of total", + "function": "sum", + "field_name": "total" + } + ]}, + "data_description" : { + "time_field":"timestamp", + "time_format": "epoch_ms" + } + } +''' +buildRestTests.setups['server_metrics_datafeed'] = buildRestTests.setups['server_metrics_job'] + ''' + - do: + xpack.ml.put_datafeed: + datafeed_id: "datafeed-total-requests" + body: > + { + "job_id":"total-requests", + "indexes":"server-metrics" + } +''' +buildRestTests.setups['server_metrics_openjob'] = buildRestTests.setups['server_metrics_datafeed'] + ''' + - do: + xpack.ml.open_job: + job_id: "total-requests" +''' +buildRestTests.setups['server_metrics_startdf'] = buildRestTests.setups['server_metrics_openjob'] + ''' + - do: + xpack.ml.start_datafeed: + datafeed_id: "datafeed-total-requests" +''' +buildRestTests.setups['calendar_outages'] = ''' + - do: + xpack.ml.put_calendar: + calendar_id: "planned-outages" +''' +buildRestTests.setups['calendar_outages_addevent'] = buildRestTests.setups['calendar_outages'] + ''' + - do: + xpack.ml.post_calendar_events: + calendar_id: "planned-outages" + body: > + { "description": "event 1", "start_time": "2017-12-01T00:00:00Z", "end_time": "2017-12-02T00:00:00Z", "calendar_id": "planned-outages" } + + +''' +buildRestTests.setups['calendar_outages_openjob'] = buildRestTests.setups['server_metrics_openjob'] + ''' + - do: + xpack.ml.put_calendar: + calendar_id: "planned-outages" +''' +buildRestTests.setups['calendar_outages_addjob'] = buildRestTests.setups['server_metrics_openjob'] + ''' + - do: + xpack.ml.put_calendar: + calendar_id: "planned-outages" + body: > + { + "job_ids": ["total-requests"] + } +''' +buildRestTests.setups['calendar_outages_addevent'] = buildRestTests.setups['calendar_outages_addjob'] + ''' + - do: + xpack.ml.post_calendar_events: + calendar_id: "planned-outages" + body: > + { "events" : [ + { "description": "event 1", "start_time": "1513641600000", "end_time": "1513728000000"}, + { "description": "event 2", "start_time": "1513814400000", "end_time": "1513900800000"}, + { "description": "event 3", "start_time": "1514160000000", "end_time": "1514246400000"} + ]} +''' + + diff --git a/x-pack/docs/en/rest-api/ml/calendarresource.asciidoc b/docs/reference/ml/apis/calendarresource.asciidoc similarity index 94% rename from x-pack/docs/en/rest-api/ml/calendarresource.asciidoc rename to docs/reference/ml/apis/calendarresource.asciidoc index 8edb43ed7a39..4279102cd35f 100644 --- a/x-pack/docs/en/rest-api/ml/calendarresource.asciidoc +++ b/docs/reference/ml/apis/calendarresource.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-calendar-resource]] === Calendar Resources diff --git a/x-pack/docs/en/rest-api/ml/close-job.asciidoc b/docs/reference/ml/apis/close-job.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/close-job.asciidoc rename to docs/reference/ml/apis/close-job.asciidoc index 8e7e8eb0ce85..6dec6402c876 100644 --- a/x-pack/docs/en/rest-api/ml/close-job.asciidoc +++ b/docs/reference/ml/apis/close-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-close-job]] === Close Jobs API ++++ @@ -80,7 +81,7 @@ The following example closes the `total-requests` job: POST _xpack/ml/anomaly_detectors/total-requests/_close -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_openjob] +// TEST[skip:setup:server_metrics_openjob] When the job is closed, you receive the following results: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/datafeedresource.asciidoc b/docs/reference/ml/apis/datafeedresource.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/datafeedresource.asciidoc rename to docs/reference/ml/apis/datafeedresource.asciidoc index 0ffeb6bc89d7..6fe0b35d9518 100644 --- a/x-pack/docs/en/rest-api/ml/datafeedresource.asciidoc +++ b/docs/reference/ml/apis/datafeedresource.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-datafeed-resource]] === {dfeed-cap} Resources diff --git a/x-pack/docs/en/rest-api/ml/delete-calendar-event.asciidoc b/docs/reference/ml/apis/delete-calendar-event.asciidoc similarity index 96% rename from x-pack/docs/en/rest-api/ml/delete-calendar-event.asciidoc rename to docs/reference/ml/apis/delete-calendar-event.asciidoc index ef8dad39dba7..8961726f5732 100644 --- a/x-pack/docs/en/rest-api/ml/delete-calendar-event.asciidoc +++ b/docs/reference/ml/apis/delete-calendar-event.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-delete-calendar-event]] === Delete Events from Calendar API ++++ @@ -44,7 +45,7 @@ calendar: DELETE _xpack/ml/calendars/planned-outages/events/LS8LJGEBMTCMA-qz49st -------------------------------------------------- // CONSOLE -// TEST[catch:missing] +// TEST[skip:catch:missing] When the event is removed, you receive the following results: [source,js] @@ -53,4 +54,3 @@ When the event is removed, you receive the following results: "acknowledged": true } ---- -// NOTCONSOLE \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/delete-calendar-job.asciidoc b/docs/reference/ml/apis/delete-calendar-job.asciidoc similarity index 93% rename from x-pack/docs/en/rest-api/ml/delete-calendar-job.asciidoc rename to docs/reference/ml/apis/delete-calendar-job.asciidoc index 94388c0c4b68..4362a82b5cb7 100644 --- a/x-pack/docs/en/rest-api/ml/delete-calendar-job.asciidoc +++ b/docs/reference/ml/apis/delete-calendar-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-delete-calendar-job]] === Delete Jobs from Calendar API ++++ @@ -38,7 +39,7 @@ calendar and `total-requests` job: DELETE _xpack/ml/calendars/planned-outages/jobs/total-requests -------------------------------------------------- // CONSOLE -// TEST[setup:calendar_outages_addjob] +// TEST[skip:setup:calendar_outages_addjob] When the job is removed from the calendar, you receive the following results: @@ -50,4 +51,4 @@ results: "job_ids": [] } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/delete-calendar.asciidoc b/docs/reference/ml/apis/delete-calendar.asciidoc similarity index 92% rename from x-pack/docs/en/rest-api/ml/delete-calendar.asciidoc rename to docs/reference/ml/apis/delete-calendar.asciidoc index f7673b545748..9f9f3457f24d 100644 --- a/x-pack/docs/en/rest-api/ml/delete-calendar.asciidoc +++ b/docs/reference/ml/apis/delete-calendar.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-delete-calendar]] === Delete Calendar API ++++ @@ -40,7 +41,7 @@ The following example deletes the `planned-outages` calendar: DELETE _xpack/ml/calendars/planned-outages -------------------------------------------------- // CONSOLE -// TEST[setup:calendar_outages] +// TEST[skip:setup:calendar_outages] When the calendar is deleted, you receive the following results: [source,js] @@ -49,4 +50,4 @@ When the calendar is deleted, you receive the following results: "acknowledged": true } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/delete-datafeed.asciidoc b/docs/reference/ml/apis/delete-datafeed.asciidoc similarity index 94% rename from x-pack/docs/en/rest-api/ml/delete-datafeed.asciidoc rename to docs/reference/ml/apis/delete-datafeed.asciidoc index db4fd5c177ae..996d2c7dd2ea 100644 --- a/x-pack/docs/en/rest-api/ml/delete-datafeed.asciidoc +++ b/docs/reference/ml/apis/delete-datafeed.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-delete-datafeed]] === Delete {dfeeds-cap} API ++++ @@ -47,7 +48,7 @@ The following example deletes the `datafeed-total-requests` {dfeed}: DELETE _xpack/ml/datafeeds/datafeed-total-requests -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_datafeed] +// TEST[skip:setup:server_metrics_datafeed] When the {dfeed} is deleted, you receive the following results: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/delete-filter.asciidoc b/docs/reference/ml/apis/delete-filter.asciidoc similarity index 92% rename from x-pack/docs/en/rest-api/ml/delete-filter.asciidoc rename to docs/reference/ml/apis/delete-filter.asciidoc index b58d2980b888..21e35b66076f 100644 --- a/x-pack/docs/en/rest-api/ml/delete-filter.asciidoc +++ b/docs/reference/ml/apis/delete-filter.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-delete-filter]] === Delete Filter API ++++ @@ -41,7 +42,7 @@ The following example deletes the `safe_domains` filter: DELETE _xpack/ml/filters/safe_domains -------------------------------------------------- // CONSOLE -// TEST[setup:ml_filter_safe_domains] +// TEST[skip:setup:ml_filter_safe_domains] When the filter is deleted, you receive the following results: [source,js] @@ -50,4 +51,4 @@ When the filter is deleted, you receive the following results: "acknowledged": true } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/delete-job.asciidoc b/docs/reference/ml/apis/delete-job.asciidoc similarity index 95% rename from x-pack/docs/en/rest-api/ml/delete-job.asciidoc rename to docs/reference/ml/apis/delete-job.asciidoc index c01b08545b63..d5ef120ad040 100644 --- a/x-pack/docs/en/rest-api/ml/delete-job.asciidoc +++ b/docs/reference/ml/apis/delete-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-delete-job]] === Delete Jobs API ++++ @@ -56,7 +57,7 @@ The following example deletes the `total-requests` job: DELETE _xpack/ml/anomaly_detectors/total-requests -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_job] +// TEST[skip:setup:server_metrics_job] When the job is deleted, you receive the following results: [source,js] @@ -65,4 +66,4 @@ When the job is deleted, you receive the following results: "acknowledged": true } ---- -// TESTRESPONSE +// TESTRESPONSE \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/delete-snapshot.asciidoc b/docs/reference/ml/apis/delete-snapshot.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/delete-snapshot.asciidoc rename to docs/reference/ml/apis/delete-snapshot.asciidoc index 2ab0116fe74d..96a359005455 100644 --- a/x-pack/docs/en/rest-api/ml/delete-snapshot.asciidoc +++ b/docs/reference/ml/apis/delete-snapshot.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-delete-snapshot]] === Delete Model Snapshots API ++++ @@ -32,7 +33,6 @@ the `model_snapshot_id` in the results from the get jobs API. You must have `manage_ml`, or `manage` cluster privileges to use this API. For more information, see {xpack-ref}/security-privileges.html[Security Privileges]. -//<>. ==== Examples @@ -53,3 +53,4 @@ When the snapshot is deleted, you receive the following results: "acknowledged": true } ---- +// TESTRESPONSE \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/eventresource.asciidoc b/docs/reference/ml/apis/eventresource.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/eventresource.asciidoc rename to docs/reference/ml/apis/eventresource.asciidoc index c9ab78964213..a1e96f5c25a0 100644 --- a/x-pack/docs/en/rest-api/ml/eventresource.asciidoc +++ b/docs/reference/ml/apis/eventresource.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-event-resource]] === Scheduled Event Resources diff --git a/x-pack/docs/en/rest-api/ml/filterresource.asciidoc b/docs/reference/ml/apis/filterresource.asciidoc similarity index 95% rename from x-pack/docs/en/rest-api/ml/filterresource.asciidoc rename to docs/reference/ml/apis/filterresource.asciidoc index e942447c1ee6..e67c92dc8d09 100644 --- a/x-pack/docs/en/rest-api/ml/filterresource.asciidoc +++ b/docs/reference/ml/apis/filterresource.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-filter-resource]] === Filter Resources diff --git a/x-pack/docs/en/rest-api/ml/flush-job.asciidoc b/docs/reference/ml/apis/flush-job.asciidoc similarity index 92% rename from x-pack/docs/en/rest-api/ml/flush-job.asciidoc rename to docs/reference/ml/apis/flush-job.asciidoc index 934a2d81b177..f19d2aa648f6 100644 --- a/x-pack/docs/en/rest-api/ml/flush-job.asciidoc +++ b/docs/reference/ml/apis/flush-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-flush-job]] === Flush Jobs API ++++ @@ -74,7 +75,7 @@ POST _xpack/ml/anomaly_detectors/total-requests/_flush } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_openjob] +// TEST[skip:setup:server_metrics_openjob] When the operation succeeds, you receive the following results: [source,js] @@ -84,7 +85,7 @@ When the operation succeeds, you receive the following results: "last_finalized_bucket_end": 1455234900000 } ---- -// TESTRESPONSE[s/"last_finalized_bucket_end": 1455234900000/"last_finalized_bucket_end": $body.last_finalized_bucket_end/] +//TESTRESPONSE[s/"last_finalized_bucket_end": 1455234900000/"last_finalized_bucket_end": $body.last_finalized_bucket_end/] The `last_finalized_bucket_end` provides the timestamp (in milliseconds-since-the-epoch) of the end of the last bucket that was processed. @@ -101,7 +102,7 @@ POST _xpack/ml/anomaly_detectors/total-requests/_flush } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_openjob] +// TEST[skip:setup:server_metrics_openjob] When the operation succeeds, you receive the following results: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/forecast.asciidoc b/docs/reference/ml/apis/forecast.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/forecast.asciidoc rename to docs/reference/ml/apis/forecast.asciidoc index 99647ecae1b2..197876f3f04a 100644 --- a/x-pack/docs/en/rest-api/ml/forecast.asciidoc +++ b/docs/reference/ml/apis/forecast.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-forecast]] === Forecast Jobs API ++++ diff --git a/x-pack/docs/en/rest-api/ml/get-bucket.asciidoc b/docs/reference/ml/apis/get-bucket.asciidoc similarity index 98% rename from x-pack/docs/en/rest-api/ml/get-bucket.asciidoc rename to docs/reference/ml/apis/get-bucket.asciidoc index 95b05ff7f5dd..3a276c13e895 100644 --- a/x-pack/docs/en/rest-api/ml/get-bucket.asciidoc +++ b/docs/reference/ml/apis/get-bucket.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-bucket]] === Get Buckets API ++++ @@ -81,7 +82,6 @@ that stores the results. The `machine_learning_admin` and `machine_learning_user roles provide these privileges. For more information, see {xpack-ref}/security-privileges.html[Security Privileges] and {xpack-ref}/built-in-roles.html[Built-in Roles]. -//<> and <>. ==== Examples diff --git a/x-pack/docs/en/rest-api/ml/get-calendar-event.asciidoc b/docs/reference/ml/apis/get-calendar-event.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/get-calendar-event.asciidoc rename to docs/reference/ml/apis/get-calendar-event.asciidoc index e89173c3382d..43dd74e47c97 100644 --- a/x-pack/docs/en/rest-api/ml/get-calendar-event.asciidoc +++ b/docs/reference/ml/apis/get-calendar-event.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-calendar-event]] === Get Scheduled Events API ++++ @@ -66,7 +67,7 @@ The following example gets information about the scheduled events in the GET _xpack/ml/calendars/planned-outages/events -------------------------------------------------- // CONSOLE -// TEST[setup:calendar_outages_addevent] +// TEST[skip:setup:calendar_outages_addevent] The API returns the following results: diff --git a/x-pack/docs/en/rest-api/ml/get-calendar.asciidoc b/docs/reference/ml/apis/get-calendar.asciidoc similarity index 94% rename from x-pack/docs/en/rest-api/ml/get-calendar.asciidoc rename to docs/reference/ml/apis/get-calendar.asciidoc index ae95fd996889..f86875f326cd 100644 --- a/x-pack/docs/en/rest-api/ml/get-calendar.asciidoc +++ b/docs/reference/ml/apis/get-calendar.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-calendar]] === Get Calendars API ++++ @@ -62,7 +63,7 @@ calendar: GET _xpack/ml/calendars/planned-outages -------------------------------------------------- // CONSOLE -// TEST[setup:calendar_outages_addjob] +// TEST[skip:setup:calendar_outages_addjob] The API returns the following results: [source,js] @@ -79,4 +80,4 @@ The API returns the following results: ] } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/get-category.asciidoc b/docs/reference/ml/apis/get-category.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/get-category.asciidoc rename to docs/reference/ml/apis/get-category.asciidoc index 13f274133c0d..e5d6fe16802a 100644 --- a/x-pack/docs/en/rest-api/ml/get-category.asciidoc +++ b/docs/reference/ml/apis/get-category.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-category]] === Get Categories API ++++ @@ -18,7 +19,6 @@ Retrieves job results for one or more categories. For more information about categories, see {xpack-ref}/ml-configuring-categories.html[Categorizing Log Messages]. -//<>. ==== Path Parameters @@ -56,7 +56,6 @@ that stores the results. The `machine_learning_admin` and `machine_learning_user roles provide these privileges. For more information, see {xpack-ref}/security-privileges.html[Security Privileges] and {xpack-ref}/built-in-roles.html[Built-in Roles]. -//<> and <>. ==== Examples diff --git a/x-pack/docs/en/rest-api/ml/get-datafeed-stats.asciidoc b/docs/reference/ml/apis/get-datafeed-stats.asciidoc similarity index 96% rename from x-pack/docs/en/rest-api/ml/get-datafeed-stats.asciidoc rename to docs/reference/ml/apis/get-datafeed-stats.asciidoc index 2869e8222f86..9ca67cc17fb4 100644 --- a/x-pack/docs/en/rest-api/ml/get-datafeed-stats.asciidoc +++ b/docs/reference/ml/apis/get-datafeed-stats.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-datafeed-stats]] === Get {dfeed-cap} Statistics API ++++ @@ -66,7 +67,7 @@ The following example gets usage information for the GET _xpack/ml/datafeeds/datafeed-total-requests/_stats -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_startdf] +// TEST[skip:setup:server_metrics_startdf] The API returns the following results: [source,js] @@ -97,4 +98,4 @@ The API returns the following results: // TESTRESPONSE[s/"node-0"/$body.$_path/] // TESTRESPONSE[s/"hoXMLZB0RWKfR9UPPUCxXX"/$body.$_path/] // TESTRESPONSE[s/"127.0.0.1:9300"/$body.$_path/] -// TESTRESPONSE[s/"17179869184"/$body.datafeeds.0.node.attributes.ml\\.machine_memory/] +// TESTRESPONSE[s/"17179869184"/$body.datafeeds.0.node.attributes.ml\\.machine_memory/] \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/get-datafeed.asciidoc b/docs/reference/ml/apis/get-datafeed.asciidoc similarity index 96% rename from x-pack/docs/en/rest-api/ml/get-datafeed.asciidoc rename to docs/reference/ml/apis/get-datafeed.asciidoc index 0fa51773fd16..db5f4249669b 100644 --- a/x-pack/docs/en/rest-api/ml/get-datafeed.asciidoc +++ b/docs/reference/ml/apis/get-datafeed.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-datafeed]] === Get {dfeeds-cap} API ++++ @@ -60,7 +61,7 @@ The following example gets configuration information for the GET _xpack/ml/datafeeds/datafeed-total-requests -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_datafeed] +// TEST[skip:setup:server_metrics_datafeed] The API returns the following results: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/get-filter.asciidoc b/docs/reference/ml/apis/get-filter.asciidoc similarity index 94% rename from x-pack/docs/en/rest-api/ml/get-filter.asciidoc rename to docs/reference/ml/apis/get-filter.asciidoc index b4699e9d622c..2dbb5d16cc5a 100644 --- a/x-pack/docs/en/rest-api/ml/get-filter.asciidoc +++ b/docs/reference/ml/apis/get-filter.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-filter]] === Get Filters API ++++ @@ -62,7 +63,7 @@ filter: GET _xpack/ml/filters/safe_domains -------------------------------------------------- // CONSOLE -// TEST[setup:ml_filter_safe_domains] +// TEST[skip:setup:ml_filter_safe_domains] The API returns the following results: [source,js] @@ -81,4 +82,4 @@ The API returns the following results: ] } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/get-influencer.asciidoc b/docs/reference/ml/apis/get-influencer.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/get-influencer.asciidoc rename to docs/reference/ml/apis/get-influencer.asciidoc index bffd2b8e0963..182cca7aa991 100644 --- a/x-pack/docs/en/rest-api/ml/get-influencer.asciidoc +++ b/docs/reference/ml/apis/get-influencer.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-influencer]] === Get Influencers API ++++ diff --git a/x-pack/docs/en/rest-api/ml/get-job-stats.asciidoc b/docs/reference/ml/apis/get-job-stats.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/get-job-stats.asciidoc rename to docs/reference/ml/apis/get-job-stats.asciidoc index bd59ee8b258f..509d9448a693 100644 --- a/x-pack/docs/en/rest-api/ml/get-job-stats.asciidoc +++ b/docs/reference/ml/apis/get-job-stats.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-job-stats]] === Get Job Statistics API ++++ diff --git a/x-pack/docs/en/rest-api/ml/get-job.asciidoc b/docs/reference/ml/apis/get-job.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/get-job.asciidoc rename to docs/reference/ml/apis/get-job.asciidoc index 2e95d8e01bbb..c669ac6034ed 100644 --- a/x-pack/docs/en/rest-api/ml/get-job.asciidoc +++ b/docs/reference/ml/apis/get-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-job]] === Get Jobs API ++++ @@ -59,7 +60,7 @@ The following example gets configuration information for the `total-requests` jo GET _xpack/ml/anomaly_detectors/total-requests -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_job] +// TEST[skip:setup:server_metrics_job] The API returns the following results: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/get-overall-buckets.asciidoc b/docs/reference/ml/apis/get-overall-buckets.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/get-overall-buckets.asciidoc rename to docs/reference/ml/apis/get-overall-buckets.asciidoc index f2581f4904e3..f4818f3bbbe4 100644 --- a/x-pack/docs/en/rest-api/ml/get-overall-buckets.asciidoc +++ b/docs/reference/ml/apis/get-overall-buckets.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-overall-buckets]] === Get Overall Buckets API ++++ @@ -93,7 +94,6 @@ that stores the results. The `machine_learning_admin` and `machine_learning_user roles provide these privileges. For more information, see {xpack-ref}/security-privileges.html[Security Privileges] and {xpack-ref}/built-in-roles.html[Built-in Roles]. -//<> and <>. ==== Examples diff --git a/x-pack/docs/en/rest-api/ml/get-record.asciidoc b/docs/reference/ml/apis/get-record.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/get-record.asciidoc rename to docs/reference/ml/apis/get-record.asciidoc index 1870b4415976..199cce154842 100644 --- a/x-pack/docs/en/rest-api/ml/get-record.asciidoc +++ b/docs/reference/ml/apis/get-record.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-record]] === Get Records API ++++ diff --git a/x-pack/docs/en/rest-api/ml/get-snapshot.asciidoc b/docs/reference/ml/apis/get-snapshot.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/get-snapshot.asciidoc rename to docs/reference/ml/apis/get-snapshot.asciidoc index 24e82af1f199..e194d944b636 100644 --- a/x-pack/docs/en/rest-api/ml/get-snapshot.asciidoc +++ b/docs/reference/ml/apis/get-snapshot.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-get-snapshot]] === Get Model Snapshots API ++++ diff --git a/x-pack/docs/en/rest-api/ml/jobcounts.asciidoc b/docs/reference/ml/apis/jobcounts.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/jobcounts.asciidoc rename to docs/reference/ml/apis/jobcounts.asciidoc index d343cc23ae0a..d0169e228d54 100644 --- a/x-pack/docs/en/rest-api/ml/jobcounts.asciidoc +++ b/docs/reference/ml/apis/jobcounts.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-jobstats]] === Job Statistics diff --git a/x-pack/docs/en/rest-api/ml/jobresource.asciidoc b/docs/reference/ml/apis/jobresource.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/jobresource.asciidoc rename to docs/reference/ml/apis/jobresource.asciidoc index 5b109b1c21d7..e0c314724e76 100644 --- a/x-pack/docs/en/rest-api/ml/jobresource.asciidoc +++ b/docs/reference/ml/apis/jobresource.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-job-resource]] === Job Resources diff --git a/x-pack/docs/en/rest-api/ml-api.asciidoc b/docs/reference/ml/apis/ml-api.asciidoc similarity index 61% rename from x-pack/docs/en/rest-api/ml-api.asciidoc rename to docs/reference/ml/apis/ml-api.asciidoc index b48e9f934042..b8509f221524 100644 --- a/x-pack/docs/en/rest-api/ml-api.asciidoc +++ b/docs/reference/ml/apis/ml-api.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-apis]] == Machine Learning APIs @@ -70,57 +71,57 @@ machine learning APIs and in advanced job configuration options in Kibana. * <> //ADD -include::ml/post-calendar-event.asciidoc[] -include::ml/put-calendar-job.asciidoc[] +include::post-calendar-event.asciidoc[] +include::put-calendar-job.asciidoc[] //CLOSE -include::ml/close-job.asciidoc[] +include::close-job.asciidoc[] //CREATE -include::ml/put-calendar.asciidoc[] -include::ml/put-datafeed.asciidoc[] -include::ml/put-filter.asciidoc[] -include::ml/put-job.asciidoc[] +include::put-calendar.asciidoc[] +include::put-datafeed.asciidoc[] +include::put-filter.asciidoc[] +include::put-job.asciidoc[] //DELETE -include::ml/delete-calendar.asciidoc[] -include::ml/delete-datafeed.asciidoc[] -include::ml/delete-calendar-event.asciidoc[] -include::ml/delete-filter.asciidoc[] -include::ml/delete-job.asciidoc[] -include::ml/delete-calendar-job.asciidoc[] -include::ml/delete-snapshot.asciidoc[] +include::delete-calendar.asciidoc[] +include::delete-datafeed.asciidoc[] +include::delete-calendar-event.asciidoc[] +include::delete-filter.asciidoc[] +include::delete-job.asciidoc[] +include::delete-calendar-job.asciidoc[] +include::delete-snapshot.asciidoc[] //FLUSH -include::ml/flush-job.asciidoc[] +include::flush-job.asciidoc[] //FORECAST -include::ml/forecast.asciidoc[] +include::forecast.asciidoc[] //GET -include::ml/get-calendar.asciidoc[] -include::ml/get-bucket.asciidoc[] -include::ml/get-overall-buckets.asciidoc[] -include::ml/get-category.asciidoc[] -include::ml/get-datafeed.asciidoc[] -include::ml/get-datafeed-stats.asciidoc[] -include::ml/get-influencer.asciidoc[] -include::ml/get-job.asciidoc[] -include::ml/get-job-stats.asciidoc[] -include::ml/get-snapshot.asciidoc[] -include::ml/get-calendar-event.asciidoc[] -include::ml/get-filter.asciidoc[] -include::ml/get-record.asciidoc[] +include::get-calendar.asciidoc[] +include::get-bucket.asciidoc[] +include::get-overall-buckets.asciidoc[] +include::get-category.asciidoc[] +include::get-datafeed.asciidoc[] +include::get-datafeed-stats.asciidoc[] +include::get-influencer.asciidoc[] +include::get-job.asciidoc[] +include::get-job-stats.asciidoc[] +include::get-snapshot.asciidoc[] +include::get-calendar-event.asciidoc[] +include::get-filter.asciidoc[] +include::get-record.asciidoc[] //OPEN -include::ml/open-job.asciidoc[] +include::open-job.asciidoc[] //POST -include::ml/post-data.asciidoc[] +include::post-data.asciidoc[] //PREVIEW -include::ml/preview-datafeed.asciidoc[] +include::preview-datafeed.asciidoc[] //REVERT -include::ml/revert-snapshot.asciidoc[] +include::revert-snapshot.asciidoc[] //START/STOP -include::ml/start-datafeed.asciidoc[] -include::ml/stop-datafeed.asciidoc[] +include::start-datafeed.asciidoc[] +include::stop-datafeed.asciidoc[] //UPDATE -include::ml/update-datafeed.asciidoc[] -include::ml/update-filter.asciidoc[] -include::ml/update-job.asciidoc[] -include::ml/update-snapshot.asciidoc[] +include::update-datafeed.asciidoc[] +include::update-filter.asciidoc[] +include::update-job.asciidoc[] +include::update-snapshot.asciidoc[] //VALIDATE -//include::ml/validate-detector.asciidoc[] -//include::ml/validate-job.asciidoc[] +//include::validate-detector.asciidoc[] +//include::validate-job.asciidoc[] diff --git a/x-pack/docs/en/rest-api/ml/open-job.asciidoc b/docs/reference/ml/apis/open-job.asciidoc similarity index 95% rename from x-pack/docs/en/rest-api/ml/open-job.asciidoc rename to docs/reference/ml/apis/open-job.asciidoc index 59d5568ac775..c1e5977b734f 100644 --- a/x-pack/docs/en/rest-api/ml/open-job.asciidoc +++ b/docs/reference/ml/apis/open-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-open-job]] === Open Jobs API ++++ @@ -56,7 +57,7 @@ POST _xpack/ml/anomaly_detectors/total-requests/_open } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_job] +// TEST[skip:setup:server_metrics_job] When the job opens, you receive the following results: [source,js] @@ -65,5 +66,4 @@ When the job opens, you receive the following results: "opened": true } ---- -//CONSOLE // TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/post-calendar-event.asciidoc b/docs/reference/ml/apis/post-calendar-event.asciidoc similarity index 96% rename from x-pack/docs/en/rest-api/ml/post-calendar-event.asciidoc rename to docs/reference/ml/apis/post-calendar-event.asciidoc index 41af0841d2e8..998db409fc7d 100644 --- a/x-pack/docs/en/rest-api/ml/post-calendar-event.asciidoc +++ b/docs/reference/ml/apis/post-calendar-event.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-post-calendar-event]] === Add Events to Calendar API ++++ @@ -52,7 +53,7 @@ POST _xpack/ml/calendars/planned-outages/events } -------------------------------------------------- // CONSOLE -// TEST[setup:calendar_outages_addjob] +// TEST[skip:setup:calendar_outages_addjob] The API returns the following results: @@ -81,7 +82,7 @@ The API returns the following results: ] } ---- -//TESTRESPONSE +// TESTRESPONSE For more information about these properties, see <>. diff --git a/x-pack/docs/en/rest-api/ml/post-data.asciidoc b/docs/reference/ml/apis/post-data.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/post-data.asciidoc rename to docs/reference/ml/apis/post-data.asciidoc index 40354d7f6f76..6a5a3d3d6cb5 100644 --- a/x-pack/docs/en/rest-api/ml/post-data.asciidoc +++ b/docs/reference/ml/apis/post-data.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-post-data]] === Post Data to Jobs API ++++ diff --git a/x-pack/docs/en/rest-api/ml/preview-datafeed.asciidoc b/docs/reference/ml/apis/preview-datafeed.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/preview-datafeed.asciidoc rename to docs/reference/ml/apis/preview-datafeed.asciidoc index 637b506cb9af..7b9eccd9a592 100644 --- a/x-pack/docs/en/rest-api/ml/preview-datafeed.asciidoc +++ b/docs/reference/ml/apis/preview-datafeed.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-preview-datafeed]] === Preview {dfeeds-cap} API ++++ @@ -53,7 +54,7 @@ The following example obtains a preview of the `datafeed-farequote` {dfeed}: GET _xpack/ml/datafeeds/datafeed-farequote/_preview -------------------------------------------------- // CONSOLE -// TEST[setup:farequote_datafeed] +// TEST[skip:setup:farequote_datafeed] The data that is returned for this example is as follows: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/put-calendar-job.asciidoc b/docs/reference/ml/apis/put-calendar-job.asciidoc similarity index 93% rename from x-pack/docs/en/rest-api/ml/put-calendar-job.asciidoc rename to docs/reference/ml/apis/put-calendar-job.asciidoc index 6940957b1592..0563047043ae 100644 --- a/x-pack/docs/en/rest-api/ml/put-calendar-job.asciidoc +++ b/docs/reference/ml/apis/put-calendar-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-put-calendar-job]] === Add Jobs to Calendar API ++++ @@ -38,7 +39,7 @@ The following example associates the `planned-outages` calendar with the PUT _xpack/ml/calendars/planned-outages/jobs/total-requests -------------------------------------------------- // CONSOLE -// TEST[setup:calendar_outages_openjob] +// TEST[skip:setup:calendar_outages_openjob] The API returns the following results: @@ -51,4 +52,4 @@ The API returns the following results: ] } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/put-calendar.asciidoc b/docs/reference/ml/apis/put-calendar.asciidoc similarity index 94% rename from x-pack/docs/en/rest-api/ml/put-calendar.asciidoc rename to docs/reference/ml/apis/put-calendar.asciidoc index a82da5a2c0c0..06b8e55d7747 100644 --- a/x-pack/docs/en/rest-api/ml/put-calendar.asciidoc +++ b/docs/reference/ml/apis/put-calendar.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-put-calendar]] === Create Calendar API ++++ @@ -44,6 +45,7 @@ The following example creates the `planned-outages` calendar: PUT _xpack/ml/calendars/planned-outages -------------------------------------------------- // CONSOLE +// TEST[skip:need-license] When the calendar is created, you receive the following results: [source,js] @@ -53,4 +55,4 @@ When the calendar is created, you receive the following results: "job_ids": [] } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/put-datafeed.asciidoc b/docs/reference/ml/apis/put-datafeed.asciidoc similarity index 98% rename from x-pack/docs/en/rest-api/ml/put-datafeed.asciidoc rename to docs/reference/ml/apis/put-datafeed.asciidoc index 6b8ad932a1d4..b5c99fc8e36a 100644 --- a/x-pack/docs/en/rest-api/ml/put-datafeed.asciidoc +++ b/docs/reference/ml/apis/put-datafeed.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-put-datafeed]] === Create {dfeeds-cap} API ++++ @@ -107,7 +108,7 @@ PUT _xpack/ml/datafeeds/datafeed-total-requests } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_job] +// TEST[skip:setup:server_metrics_job] When the {dfeed} is created, you receive the following results: [source,js] @@ -132,4 +133,4 @@ When the {dfeed} is created, you receive the following results: } ---- // TESTRESPONSE[s/"query_delay": "83474ms"/"query_delay": $body.query_delay/] -// TESTRESPONSE[s/"query.boost": "1.0"/"query.boost": $body.query.boost/] +// TESTRESPONSE[s/"query.boost": "1.0"/"query.boost": $body.query.boost/] \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/put-filter.asciidoc b/docs/reference/ml/apis/put-filter.asciidoc similarity index 95% rename from x-pack/docs/en/rest-api/ml/put-filter.asciidoc rename to docs/reference/ml/apis/put-filter.asciidoc index d2982a56f612..165fe9697584 100644 --- a/x-pack/docs/en/rest-api/ml/put-filter.asciidoc +++ b/docs/reference/ml/apis/put-filter.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-put-filter]] === Create Filter API ++++ @@ -55,6 +56,7 @@ PUT _xpack/ml/filters/safe_domains } -------------------------------------------------- // CONSOLE +// TEST[skip:need-licence] When the filter is created, you receive the following response: [source,js] @@ -65,4 +67,4 @@ When the filter is created, you receive the following response: "items": ["*.google.com", "wikipedia.org"] } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/put-job.asciidoc b/docs/reference/ml/apis/put-job.asciidoc similarity index 98% rename from x-pack/docs/en/rest-api/ml/put-job.asciidoc rename to docs/reference/ml/apis/put-job.asciidoc index 1c436f53d32e..ce0534849060 100644 --- a/x-pack/docs/en/rest-api/ml/put-job.asciidoc +++ b/docs/reference/ml/apis/put-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-put-job]] === Create Jobs API ++++ @@ -104,6 +105,7 @@ PUT _xpack/ml/anomaly_detectors/total-requests } -------------------------------------------------- // CONSOLE +// TEST[skip:need-licence] When the job is created, you receive the following results: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/resultsresource.asciidoc b/docs/reference/ml/apis/resultsresource.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/resultsresource.asciidoc rename to docs/reference/ml/apis/resultsresource.asciidoc index c28ed72aedb3..d3abd094be79 100644 --- a/x-pack/docs/en/rest-api/ml/resultsresource.asciidoc +++ b/docs/reference/ml/apis/resultsresource.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-results-resource]] === Results Resources diff --git a/x-pack/docs/en/rest-api/ml/revert-snapshot.asciidoc b/docs/reference/ml/apis/revert-snapshot.asciidoc similarity index 67% rename from x-pack/docs/en/rest-api/ml/revert-snapshot.asciidoc rename to docs/reference/ml/apis/revert-snapshot.asciidoc index 1dc3046ac4f2..48fc65edf90f 100644 --- a/x-pack/docs/en/rest-api/ml/revert-snapshot.asciidoc +++ b/docs/reference/ml/apis/revert-snapshot.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-revert-snapshot]] === Revert Model Snapshots API ++++ @@ -22,33 +23,6 @@ then it might be appropriate to reset the model state to a time before this event. For example, you might consider reverting to a saved snapshot after Black Friday or a critical system failure. -//// -To revert to a saved snapshot, you must follow this sequence: -. Close the job -. Revert to a snapshot -. Open the job -. Send new data to the job - -When reverting to a snapshot, there is a choice to make about whether or not -you want to keep the results that were created between the time of the snapshot -and the current time. In the case of Black Friday for instance, you might want -to keep the results and carry on processing data from the current time, -though without the models learning the one-off behavior and compensating for it. -However, say in the event of a critical system failure and you decide to reset -and models to a previous known good state and process data from that time, -it makes sense to delete the intervening results for the known bad period and -resend data from that earlier time. - -Any gaps in data since the snapshot time will be treated as nulls and not modeled. -If there is a partial bucket at the end of the snapshot and/or at the beginning -of the new input data, then this will be ignored and treated as a gap. - -For jobs with many entities, the model state may be very large. -If a model state is several GB, this could take 10-20 mins to revert depending -upon machine spec and resources. If this is the case, please ensure this time -is planned for. -Model size (in bytes) is available as part of the Job Resource Model Size Stats. -//// IMPORTANT: Before you revert to a saved snapshot, you must close the job. @@ -77,7 +51,6 @@ If you want to resend data, then delete the intervening results. You must have `manage_ml`, or `manage` cluster privileges to use this API. For more information, see {xpack-ref}/security-privileges.html[Security Privileges]. -//<>. ==== Examples diff --git a/x-pack/docs/en/rest-api/ml/snapshotresource.asciidoc b/docs/reference/ml/apis/snapshotresource.asciidoc similarity index 99% rename from x-pack/docs/en/rest-api/ml/snapshotresource.asciidoc rename to docs/reference/ml/apis/snapshotresource.asciidoc index fb2e3d83de6d..f068f6d94ed0 100644 --- a/x-pack/docs/en/rest-api/ml/snapshotresource.asciidoc +++ b/docs/reference/ml/apis/snapshotresource.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-snapshot-resource]] === Model Snapshot Resources diff --git a/x-pack/docs/en/rest-api/ml/start-datafeed.asciidoc b/docs/reference/ml/apis/start-datafeed.asciidoc similarity index 97% rename from x-pack/docs/en/rest-api/ml/start-datafeed.asciidoc rename to docs/reference/ml/apis/start-datafeed.asciidoc index fa3ea35a751f..566e700dd043 100644 --- a/x-pack/docs/en/rest-api/ml/start-datafeed.asciidoc +++ b/docs/reference/ml/apis/start-datafeed.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-start-datafeed]] === Start {dfeeds-cap} API ++++ @@ -79,7 +80,6 @@ of the latest processed record. You must have `manage_ml`, or `manage` cluster privileges to use this API. For more information, see {xpack-ref}/security-privileges.html[Security Privileges]. -//<>. ==== Security Integration @@ -101,7 +101,7 @@ POST _xpack/ml/datafeeds/datafeed-total-requests/_start } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_openjob] +// TEST[skip:setup:server_metrics_openjob] When the {dfeed} starts, you receive the following results: [source,js] @@ -110,5 +110,4 @@ When the {dfeed} starts, you receive the following results: "started": true } ---- -// CONSOLE -// TESTRESPONSE +// TESTRESPONSE \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/stop-datafeed.asciidoc b/docs/reference/ml/apis/stop-datafeed.asciidoc similarity index 92% rename from x-pack/docs/en/rest-api/ml/stop-datafeed.asciidoc rename to docs/reference/ml/apis/stop-datafeed.asciidoc index 27872ff5a208..7ea48974f2df 100644 --- a/x-pack/docs/en/rest-api/ml/stop-datafeed.asciidoc +++ b/docs/reference/ml/apis/stop-datafeed.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-stop-datafeed]] === Stop {dfeeds-cap} API ++++ @@ -18,7 +19,6 @@ A {dfeed} can be started and stopped multiple times throughout its lifecycle. `POST _xpack/ml/datafeeds/_all/_stop` -//TBD: Can there be spaces between the items in the list? ===== Description @@ -63,14 +63,14 @@ POST _xpack/ml/datafeeds/datafeed-total-requests/_stop } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_startdf] +// TEST[skip:setup:server_metrics_startdf] When the {dfeed} stops, you receive the following results: + [source,js] ---- { "stopped": true } ---- -// CONSOLE -// TESTRESPONSE +// TESTRESPONSE \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/update-datafeed.asciidoc b/docs/reference/ml/apis/update-datafeed.asciidoc similarity index 98% rename from x-pack/docs/en/rest-api/ml/update-datafeed.asciidoc rename to docs/reference/ml/apis/update-datafeed.asciidoc index bc9462347c1c..be55d864c871 100644 --- a/x-pack/docs/en/rest-api/ml/update-datafeed.asciidoc +++ b/docs/reference/ml/apis/update-datafeed.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-update-datafeed]] === Update {dfeeds-cap} API ++++ @@ -106,7 +107,7 @@ POST _xpack/ml/datafeeds/datafeed-total-requests/_update } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_datafeed] +// TEST[skip:setup:server_metrics_datafeed] When the {dfeed} is updated, you receive the full {dfeed} configuration with with the updated values: diff --git a/x-pack/docs/en/rest-api/ml/update-filter.asciidoc b/docs/reference/ml/apis/update-filter.asciidoc similarity index 94% rename from x-pack/docs/en/rest-api/ml/update-filter.asciidoc rename to docs/reference/ml/apis/update-filter.asciidoc index 1b6760dfed65..f551c8e923b8 100644 --- a/x-pack/docs/en/rest-api/ml/update-filter.asciidoc +++ b/docs/reference/ml/apis/update-filter.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-update-filter]] === Update Filter API ++++ @@ -52,7 +53,7 @@ POST _xpack/ml/filters/safe_domains/_update } -------------------------------------------------- // CONSOLE -// TEST[setup:ml_filter_safe_domains] +// TEST[skip:setup:ml_filter_safe_domains] The API returns the following results: @@ -64,4 +65,4 @@ The API returns the following results: "items": ["*.google.com", "*.myorg.com"] } ---- -//TESTRESPONSE +// TESTRESPONSE diff --git a/x-pack/docs/en/rest-api/ml/update-job.asciidoc b/docs/reference/ml/apis/update-job.asciidoc similarity index 98% rename from x-pack/docs/en/rest-api/ml/update-job.asciidoc rename to docs/reference/ml/apis/update-job.asciidoc index 852745e9dd90..58bfb2679d93 100644 --- a/x-pack/docs/en/rest-api/ml/update-job.asciidoc +++ b/docs/reference/ml/apis/update-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-update-job]] === Update Jobs API ++++ @@ -121,7 +122,7 @@ POST _xpack/ml/anomaly_detectors/total-requests/_update } -------------------------------------------------- // CONSOLE -// TEST[setup:server_metrics_job] +// TEST[skip:setup:server_metrics_job] When the job is updated, you receive a summary of the job configuration information, including the updated property values. For example: @@ -177,4 +178,4 @@ information, including the updated property values. For example: } ---- // TESTRESPONSE[s/"job_version": "7.0.0-alpha1"/"job_version": $body.job_version/] -// TESTRESPONSE[s/"create_time": 1518808660505/"create_time": $body.create_time/] +// TESTRESPONSE[s/"create_time": 1518808660505/"create_time": $body.create_time/] \ No newline at end of file diff --git a/x-pack/docs/en/rest-api/ml/update-snapshot.asciidoc b/docs/reference/ml/apis/update-snapshot.asciidoc similarity index 98% rename from x-pack/docs/en/rest-api/ml/update-snapshot.asciidoc rename to docs/reference/ml/apis/update-snapshot.asciidoc index 8c98a7b73218..b58eebe810fa 100644 --- a/x-pack/docs/en/rest-api/ml/update-snapshot.asciidoc +++ b/docs/reference/ml/apis/update-snapshot.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-update-snapshot]] === Update Model Snapshots API ++++ diff --git a/x-pack/docs/en/rest-api/ml/validate-detector.asciidoc b/docs/reference/ml/apis/validate-detector.asciidoc similarity index 95% rename from x-pack/docs/en/rest-api/ml/validate-detector.asciidoc rename to docs/reference/ml/apis/validate-detector.asciidoc index ab8a0de442cf..e525b1a1b200 100644 --- a/x-pack/docs/en/rest-api/ml/validate-detector.asciidoc +++ b/docs/reference/ml/apis/validate-detector.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-valid-detector]] === Validate Detectors API ++++ @@ -44,6 +45,7 @@ POST _xpack/ml/anomaly_detectors/_validate/detector } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] When the validation completes, you receive the following results: [source,js] diff --git a/x-pack/docs/en/rest-api/ml/validate-job.asciidoc b/docs/reference/ml/apis/validate-job.asciidoc similarity index 96% rename from x-pack/docs/en/rest-api/ml/validate-job.asciidoc rename to docs/reference/ml/apis/validate-job.asciidoc index 0ccc5bc04e1d..b83260582602 100644 --- a/x-pack/docs/en/rest-api/ml/validate-job.asciidoc +++ b/docs/reference/ml/apis/validate-job.asciidoc @@ -1,4 +1,5 @@ [role="xpack"] +[testenv="platinum"] [[ml-valid-job]] === Validate Jobs API ++++ @@ -55,6 +56,7 @@ POST _xpack/ml/anomaly_detectors/_validate } -------------------------------------------------- // CONSOLE +// TEST[skip:needs-licence] When the validation is complete, you receive the following results: [source,js] diff --git a/docs/reference/redirects.asciidoc b/docs/reference/redirects.asciidoc index f0e055380537..1a932fdd4140 100644 --- a/docs/reference/redirects.asciidoc +++ b/docs/reference/redirects.asciidoc @@ -549,3 +549,9 @@ See <>. === X-Pack commands See <>. + +[role="exclude",id="ml-api-definitions"] +=== Machine learning API definitions + +See <>. + diff --git a/docs/reference/rest-api/defs.asciidoc b/docs/reference/rest-api/defs.asciidoc new file mode 100644 index 000000000000..4eeedc553999 --- /dev/null +++ b/docs/reference/rest-api/defs.asciidoc @@ -0,0 +1,27 @@ +[role="xpack"] +[[api-definitions]] +== Definitions + +These resource definitions are used in {ml} and {security} APIs and in {kib} +advanced {ml} job configuration options. + +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> + +include::{es-repo-dir}/ml/apis/calendarresource.asciidoc[] +include::{es-repo-dir}/ml/apis/datafeedresource.asciidoc[] +include::{es-repo-dir}/ml/apis/filterresource.asciidoc[] +include::{es-repo-dir}/ml/apis/jobresource.asciidoc[] +include::{es-repo-dir}/ml/apis/jobcounts.asciidoc[] +include::{es-repo-dir}/ml/apis/snapshotresource.asciidoc[] +include::{xes-repo-dir}/rest-api/security/role-mapping-resources.asciidoc[] +include::{es-repo-dir}/ml/apis/resultsresource.asciidoc[] +include::{es-repo-dir}/ml/apis/eventresource.asciidoc[] diff --git a/docs/reference/rest-api/index.asciidoc b/docs/reference/rest-api/index.asciidoc index e1d607948e1e..b80e8badf5bb 100644 --- a/docs/reference/rest-api/index.asciidoc +++ b/docs/reference/rest-api/index.asciidoc @@ -22,8 +22,8 @@ include::info.asciidoc[] include::{xes-repo-dir}/rest-api/graph/explore.asciidoc[] include::{es-repo-dir}/licensing/index.asciidoc[] include::{es-repo-dir}/migration/migration.asciidoc[] -include::{xes-repo-dir}/rest-api/ml-api.asciidoc[] +include::{es-repo-dir}/ml/apis/ml-api.asciidoc[] include::{es-repo-dir}/rollup/rollup-api.asciidoc[] include::{xes-repo-dir}/rest-api/security.asciidoc[] include::{xes-repo-dir}/rest-api/watcher.asciidoc[] -include::{xes-repo-dir}/rest-api/defs.asciidoc[] +include::defs.asciidoc[] diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle index 99e62532e2dc..f027493b0abe 100644 --- a/x-pack/docs/build.gradle +++ b/x-pack/docs/build.gradle @@ -14,17 +14,6 @@ buildRestTests.expectedUnconvertedCandidates = [ 'en/security/authorization/run-as-privilege.asciidoc', 'en/security/ccs-clients-integrations/http.asciidoc', 'en/security/authorization/custom-roles-provider.asciidoc', - 'en/rest-api/ml/delete-snapshot.asciidoc', - 'en/rest-api/ml/get-bucket.asciidoc', - 'en/rest-api/ml/get-job-stats.asciidoc', - 'en/rest-api/ml/get-overall-buckets.asciidoc', - 'en/rest-api/ml/get-category.asciidoc', - 'en/rest-api/ml/get-record.asciidoc', - 'en/rest-api/ml/get-influencer.asciidoc', - 'en/rest-api/ml/get-snapshot.asciidoc', - 'en/rest-api/ml/post-data.asciidoc', - 'en/rest-api/ml/revert-snapshot.asciidoc', - 'en/rest-api/ml/update-snapshot.asciidoc', 'en/rest-api/watcher/stats.asciidoc', 'en/watcher/example-watches/watching-time-series-data.asciidoc', ] diff --git a/x-pack/docs/en/rest-api/defs.asciidoc b/x-pack/docs/en/rest-api/defs.asciidoc deleted file mode 100644 index ed53929391bf..000000000000 --- a/x-pack/docs/en/rest-api/defs.asciidoc +++ /dev/null @@ -1,36 +0,0 @@ -[role="xpack"] -[[ml-api-definitions]] -== Definitions - -These resource definitions are used in {ml} and {security} APIs and in {kib} -advanced {ml} job configuration options. - -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> - -[role="xpack"] -include::ml/calendarresource.asciidoc[] -[role="xpack"] -include::ml/datafeedresource.asciidoc[] -[role="xpack"] -include::ml/filterresource.asciidoc[] -[role="xpack"] -include::ml/jobresource.asciidoc[] -[role="xpack"] -include::ml/jobcounts.asciidoc[] -[role="xpack"] -include::security/role-mapping-resources.asciidoc[] -[role="xpack"] -include::ml/snapshotresource.asciidoc[] -[role="xpack"] -include::ml/resultsresource.asciidoc[] -[role="xpack"] -include::ml/eventresource.asciidoc[] From ce635f5f150d1a9b8b9e380f52aac129e79fb381 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Sat, 1 Sep 2018 09:53:23 -0400 Subject: [PATCH 278/283] Mute testSyncerOnClosingShard Tracked at #33330 --- .../org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java index 29b16ca28f4d..36d52d4475b1 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/PrimaryReplicaSyncerTests.java @@ -125,6 +125,7 @@ public void testSyncerSendsOffCorrectDocuments() throws Exception { closeShards(shard); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33330") public void testSyncerOnClosingShard() throws Exception { IndexShard shard = newStartedShard(true); AtomicBoolean syncActionCalled = new AtomicBoolean(); From f28cddf9518fb83dd9db2ed88a3362dac6c8aeb3 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Sat, 1 Sep 2018 11:11:25 -0400 Subject: [PATCH 279/283] LLREST: Drop deprecated methods (#33223) In #29623 we added `Request` object flavored requests to the low level REST client and in #30315 we deprecated the old `performRequest`s. In a long series of PRs I've changed all of the old style requests. This drops the deprecated methods and will be released with 7.0. --- .../org/elasticsearch/client/RestClient.java | 279 +----------------- .../RestClientSingleHostIntegTests.java | 9 +- .../client/RestClientSingleHostTests.java | 91 +----- .../elasticsearch/client/RestClientTests.java | 155 ---------- docs/reference/migration/migrate_7_0.asciidoc | 4 +- .../migrate_7_0/low_level_restclient.asciidoc | 14 + 6 files changed, 32 insertions(+), 520 deletions(-) create mode 100644 docs/reference/migration/migrate_7_0/low_level_restclient.asciidoc diff --git a/client/rest/src/main/java/org/elasticsearch/client/RestClient.java b/client/rest/src/main/java/org/elasticsearch/client/RestClient.java index 934b95260867..a7afbc8ffbd6 100644 --- a/client/rest/src/main/java/org/elasticsearch/client/RestClient.java +++ b/client/rest/src/main/java/org/elasticsearch/client/RestClient.java @@ -85,7 +85,7 @@ * The hosts that are part of the cluster need to be provided at creation time, but can also be replaced later * by calling {@link #setNodes(Collection)}. *

    - * The method {@link #performRequest(String, String, Map, HttpEntity, Header...)} allows to send a request to the cluster. When + * The method {@link #performRequest(Request)} allows to send a request to the cluster. When * sending a request, a host gets selected out of the provided ones in a round-robin fashion. Failing hosts are marked dead and * retried after a certain amount of time (minimum 1 minute, maximum 30 minutes), depending on how many times they previously * failed (the more failures, the later they will be retried). In case of failures all of the alive nodes (or dead nodes that @@ -145,17 +145,6 @@ public static RestClientBuilder builder(HttpHost... hosts) { return new RestClientBuilder(hostsToNodes(hosts)); } - /** - * Replaces the hosts with which the client communicates. - * - * @deprecated prefer {@link #setNodes(Collection)} because it allows you - * to set metadata for use with {@link NodeSelector}s - */ - @Deprecated - public void setHosts(HttpHost... hosts) { - setNodes(hostsToNodes(hosts)); - } - /** * Replaces the nodes with which the client communicates. */ @@ -251,234 +240,6 @@ public void performRequestAsync(Request request, ResponseListener responseListen } } - /** - * Sends a request to the Elasticsearch cluster that the client points to and waits for the corresponding response - * to be returned. Shortcut to {@link #performRequest(String, String, Map, HttpEntity, Header...)} but without parameters - * and request body. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param headers the optional request headers - * @return the response returned by Elasticsearch - * @throws IOException in case of a problem or the connection was aborted - * @throws ClientProtocolException in case of an http protocol error - * @throws ResponseException in case Elasticsearch responded with a status code that indicated an error - * @deprecated prefer {@link #performRequest(Request)} - */ - @Deprecated - public Response performRequest(String method, String endpoint, Header... headers) throws IOException { - Request request = new Request(method, endpoint); - addHeaders(request, headers); - return performRequest(request); - } - - /** - * Sends a request to the Elasticsearch cluster that the client points to and waits for the corresponding response - * to be returned. Shortcut to {@link #performRequest(String, String, Map, HttpEntity, Header...)} but without request body. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param params the query_string parameters - * @param headers the optional request headers - * @return the response returned by Elasticsearch - * @throws IOException in case of a problem or the connection was aborted - * @throws ClientProtocolException in case of an http protocol error - * @throws ResponseException in case Elasticsearch responded with a status code that indicated an error - * @deprecated prefer {@link #performRequest(Request)} - */ - @Deprecated - public Response performRequest(String method, String endpoint, Map params, Header... headers) throws IOException { - Request request = new Request(method, endpoint); - addParameters(request, params); - addHeaders(request, headers); - return performRequest(request); - } - - /** - * Sends a request to the Elasticsearch cluster that the client points to and waits for the corresponding response - * to be returned. Shortcut to {@link #performRequest(String, String, Map, HttpEntity, HttpAsyncResponseConsumerFactory, Header...)} - * which doesn't require specifying an {@link HttpAsyncResponseConsumerFactory} instance, - * {@link HttpAsyncResponseConsumerFactory} will be used to create the needed instances of {@link HttpAsyncResponseConsumer}. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param params the query_string parameters - * @param entity the body of the request, null if not applicable - * @param headers the optional request headers - * @return the response returned by Elasticsearch - * @throws IOException in case of a problem or the connection was aborted - * @throws ClientProtocolException in case of an http protocol error - * @throws ResponseException in case Elasticsearch responded with a status code that indicated an error - * @deprecated prefer {@link #performRequest(Request)} - */ - @Deprecated - public Response performRequest(String method, String endpoint, Map params, - HttpEntity entity, Header... headers) throws IOException { - Request request = new Request(method, endpoint); - addParameters(request, params); - request.setEntity(entity); - addHeaders(request, headers); - return performRequest(request); - } - - /** - * Sends a request to the Elasticsearch cluster that the client points to. Blocks until the request is completed and returns - * its response or fails by throwing an exception. Selects a host out of the provided ones in a round-robin fashion. Failing hosts - * are marked dead and retried after a certain amount of time (minimum 1 minute, maximum 30 minutes), depending on how many times - * they previously failed (the more failures, the later they will be retried). In case of failures all of the alive nodes (or dead - * nodes that deserve a retry) are retried until one responds or none of them does, in which case an {@link IOException} will be thrown. - * - * This method works by performing an asynchronous call and waiting - * for the result. If the asynchronous call throws an exception we wrap - * it and rethrow it so that the stack trace attached to the exception - * contains the call site. While we attempt to preserve the original - * exception this isn't always possible and likely haven't covered all of - * the cases. You can get the original exception from - * {@link Exception#getCause()}. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param params the query_string parameters - * @param entity the body of the request, null if not applicable - * @param httpAsyncResponseConsumerFactory the {@link HttpAsyncResponseConsumerFactory} used to create one - * {@link HttpAsyncResponseConsumer} callback per retry. Controls how the response body gets streamed from a non-blocking HTTP - * connection on the client side. - * @param headers the optional request headers - * @return the response returned by Elasticsearch - * @throws IOException in case of a problem or the connection was aborted - * @throws ClientProtocolException in case of an http protocol error - * @throws ResponseException in case Elasticsearch responded with a status code that indicated an error - * @deprecated prefer {@link #performRequest(Request)} - */ - @Deprecated - public Response performRequest(String method, String endpoint, Map params, - HttpEntity entity, HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory, - Header... headers) throws IOException { - Request request = new Request(method, endpoint); - addParameters(request, params); - request.setEntity(entity); - setOptions(request, httpAsyncResponseConsumerFactory, headers); - return performRequest(request); - } - - /** - * Sends a request to the Elasticsearch cluster that the client points to. Doesn't wait for the response, instead - * the provided {@link ResponseListener} will be notified upon completion or failure. Shortcut to - * {@link #performRequestAsync(String, String, Map, HttpEntity, ResponseListener, Header...)} but without parameters and request body. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param responseListener the {@link ResponseListener} to notify when the request is completed or fails - * @param headers the optional request headers - * @deprecated prefer {@link #performRequestAsync(Request, ResponseListener)} - */ - @Deprecated - public void performRequestAsync(String method, String endpoint, ResponseListener responseListener, Header... headers) { - Request request; - try { - request = new Request(method, endpoint); - addHeaders(request, headers); - } catch (Exception e) { - responseListener.onFailure(e); - return; - } - performRequestAsync(request, responseListener); - } - - /** - * Sends a request to the Elasticsearch cluster that the client points to. Doesn't wait for the response, instead - * the provided {@link ResponseListener} will be notified upon completion or failure. Shortcut to - * {@link #performRequestAsync(String, String, Map, HttpEntity, ResponseListener, Header...)} but without request body. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param params the query_string parameters - * @param responseListener the {@link ResponseListener} to notify when the request is completed or fails - * @param headers the optional request headers - * @deprecated prefer {@link #performRequestAsync(Request, ResponseListener)} - */ - @Deprecated - public void performRequestAsync(String method, String endpoint, Map params, - ResponseListener responseListener, Header... headers) { - Request request; - try { - request = new Request(method, endpoint); - addParameters(request, params); - addHeaders(request, headers); - } catch (Exception e) { - responseListener.onFailure(e); - return; - } - performRequestAsync(request, responseListener); - } - - /** - * Sends a request to the Elasticsearch cluster that the client points to. Doesn't wait for the response, instead - * the provided {@link ResponseListener} will be notified upon completion or failure. - * Shortcut to {@link #performRequestAsync(String, String, Map, HttpEntity, HttpAsyncResponseConsumerFactory, ResponseListener, - * Header...)} which doesn't require specifying an {@link HttpAsyncResponseConsumerFactory} instance, - * {@link HttpAsyncResponseConsumerFactory} will be used to create the needed instances of {@link HttpAsyncResponseConsumer}. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param params the query_string parameters - * @param entity the body of the request, null if not applicable - * @param responseListener the {@link ResponseListener} to notify when the request is completed or fails - * @param headers the optional request headers - * @deprecated prefer {@link #performRequestAsync(Request, ResponseListener)} - */ - @Deprecated - public void performRequestAsync(String method, String endpoint, Map params, - HttpEntity entity, ResponseListener responseListener, Header... headers) { - Request request; - try { - request = new Request(method, endpoint); - addParameters(request, params); - request.setEntity(entity); - addHeaders(request, headers); - } catch (Exception e) { - responseListener.onFailure(e); - return; - } - performRequestAsync(request, responseListener); - } - - /** - * Sends a request to the Elasticsearch cluster that the client points to. The request is executed asynchronously - * and the provided {@link ResponseListener} gets notified upon request completion or failure. - * Selects a host out of the provided ones in a round-robin fashion. Failing hosts are marked dead and retried after a certain - * amount of time (minimum 1 minute, maximum 30 minutes), depending on how many times they previously failed (the more failures, - * the later they will be retried). In case of failures all of the alive nodes (or dead nodes that deserve a retry) are retried - * until one responds or none of them does, in which case an {@link IOException} will be thrown. - * - * @param method the http method - * @param endpoint the path of the request (without host and port) - * @param params the query_string parameters - * @param entity the body of the request, null if not applicable - * @param httpAsyncResponseConsumerFactory the {@link HttpAsyncResponseConsumerFactory} used to create one - * {@link HttpAsyncResponseConsumer} callback per retry. Controls how the response body gets streamed from a non-blocking HTTP - * connection on the client side. - * @param responseListener the {@link ResponseListener} to notify when the request is completed or fails - * @param headers the optional request headers - * @deprecated prefer {@link #performRequestAsync(Request, ResponseListener)} - */ - @Deprecated - public void performRequestAsync(String method, String endpoint, Map params, - HttpEntity entity, HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory, - ResponseListener responseListener, Header... headers) { - Request request; - try { - request = new Request(method, endpoint); - addParameters(request, params); - request.setEntity(entity); - setOptions(request, httpAsyncResponseConsumerFactory, headers); - } catch (Exception e) { - responseListener.onFailure(e); - return; - } - performRequestAsync(request, responseListener); - } - void performRequestAsyncNoCatch(Request request, ResponseListener listener) throws IOException { Map requestParams = new HashMap<>(request.getParameters()); //ignore is a special parameter supported by the clients, shouldn't be sent to es @@ -1035,42 +796,4 @@ public void remove() { itr.remove(); } } - - /** - * Add all headers from the provided varargs argument to a {@link Request}. This only exists - * to support methods that exist for backwards compatibility. - */ - @Deprecated - private static void addHeaders(Request request, Header... headers) { - setOptions(request, RequestOptions.DEFAULT.getHttpAsyncResponseConsumerFactory(), headers); - } - - /** - * Add all headers from the provided varargs argument to a {@link Request}. This only exists - * to support methods that exist for backwards compatibility. - */ - @Deprecated - private static void setOptions(Request request, HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory, - Header... headers) { - Objects.requireNonNull(headers, "headers cannot be null"); - RequestOptions.Builder options = request.getOptions().toBuilder(); - for (Header header : headers) { - Objects.requireNonNull(header, "header cannot be null"); - options.addHeader(header.getName(), header.getValue()); - } - options.setHttpAsyncResponseConsumerFactory(httpAsyncResponseConsumerFactory); - request.setOptions(options); - } - - /** - * Add all parameters from a map to a {@link Request}. This only exists - * to support methods that exist for backwards compatibility. - */ - @Deprecated - private static void addParameters(Request request, Map parameters) { - Objects.requireNonNull(parameters, "parameters cannot be null"); - for (Map.Entry entry : parameters.entrySet()) { - request.addParameter(entry.getKey(), entry.getValue()); - } - } } diff --git a/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostIntegTests.java b/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostIntegTests.java index 6b5bb3c98ee5..fb58f18d42af 100644 --- a/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostIntegTests.java +++ b/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostIntegTests.java @@ -45,7 +45,6 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -215,9 +214,15 @@ public void testHeaders() throws IOException { } final Header[] requestHeaders = RestClientTestUtil.randomHeaders(getRandom(), "Header"); final int statusCode = randomStatusCode(getRandom()); + Request request = new Request(method, "/" + statusCode); + RequestOptions.Builder options = request.getOptions().toBuilder(); + for (Header header : requestHeaders) { + options.addHeader(header.getName(), header.getValue()); + } + request.setOptions(options); Response esResponse; try { - esResponse = restClient.performRequest(method, "/" + statusCode, Collections.emptyMap(), requestHeaders); + esResponse = restClient.performRequest(request); } catch (ResponseException e) { esResponse = e.getResponse(); } diff --git a/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java b/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java index cb326f4a24c8..0c589e6a40c8 100644 --- a/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java +++ b/client/rest/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java @@ -59,7 +59,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -69,7 +68,6 @@ import static org.elasticsearch.client.RestClientTestUtil.getAllErrorStatusCodes; import static org.elasticsearch.client.RestClientTestUtil.getHttpMethods; import static org.elasticsearch.client.RestClientTestUtil.getOkStatusCodes; -import static org.elasticsearch.client.RestClientTestUtil.randomHttpMethod; import static org.elasticsearch.client.RestClientTestUtil.randomStatusCode; import static org.elasticsearch.client.SyncResponseListenerTests.assertExceptionStackContainsCallingMethod; import static org.hamcrest.CoreMatchers.equalTo; @@ -192,7 +190,7 @@ public void testInternalHttpRequest() throws Exception { public void testOkStatusCodes() throws IOException { for (String method : getHttpMethods()) { for (int okStatusCode : getOkStatusCodes()) { - Response response = performRequest(method, "/" + okStatusCode); + Response response = restClient.performRequest(new Request(method, "/" + okStatusCode)); assertThat(response.getStatusLine().getStatusCode(), equalTo(okStatusCode)); } } @@ -223,13 +221,11 @@ public void testErrorStatusCodes() throws IOException { //error status codes should cause an exception to be thrown for (int errorStatusCode : getAllErrorStatusCodes()) { try { - Map params; - if (ignoreParam.isEmpty()) { - params = Collections.emptyMap(); - } else { - params = Collections.singletonMap("ignore", ignoreParam); + Request request = new Request(method, "/" + errorStatusCode); + if (false == ignoreParam.isEmpty()) { + request.addParameter("ignore", ignoreParam); } - Response response = performRequest(method, "/" + errorStatusCode, params); + Response response = restClient.performRequest(request); if (expectedIgnores.contains(errorStatusCode)) { //no exception gets thrown although we got an error status code, as it was configured to be ignored assertEquals(errorStatusCode, response.getStatusLine().getStatusCode()); @@ -256,14 +252,14 @@ public void testIOExceptions() { for (String method : getHttpMethods()) { //IOExceptions should be let bubble up try { - performRequest(method, "/coe"); + restClient.performRequest(new Request(method, "/coe")); fail("request should have failed"); } catch(IOException e) { assertThat(e, instanceOf(ConnectTimeoutException.class)); } failureListener.assertCalled(singletonList(node)); try { - performRequest(method, "/soe"); + restClient.performRequest(new Request(method, "/soe")); fail("request should have failed"); } catch(IOException e) { assertThat(e, instanceOf(SocketTimeoutException.class)); @@ -313,48 +309,6 @@ public void testBody() throws IOException { } } - /** - * @deprecated will remove method in 7.0 but needs tests until then. Replaced by {@link RequestTests}. - */ - @Deprecated - public void tesPerformRequestOldStyleNullHeaders() throws IOException { - String method = randomHttpMethod(getRandom()); - int statusCode = randomStatusCode(getRandom()); - try { - performRequest(method, "/" + statusCode, (Header[])null); - fail("request should have failed"); - } catch(NullPointerException e) { - assertEquals("request headers must not be null", e.getMessage()); - } - try { - performRequest(method, "/" + statusCode, (Header)null); - fail("request should have failed"); - } catch(NullPointerException e) { - assertEquals("request header must not be null", e.getMessage()); - } - } - - /** - * @deprecated will remove method in 7.0 but needs tests until then. Replaced by {@link RequestTests#testAddParameters()}. - */ - @Deprecated - public void testPerformRequestOldStyleWithNullParams() throws IOException { - String method = randomHttpMethod(getRandom()); - int statusCode = randomStatusCode(getRandom()); - try { - restClient.performRequest(method, "/" + statusCode, (Map)null); - fail("request should have failed"); - } catch(NullPointerException e) { - assertEquals("parameters cannot be null", e.getMessage()); - } - try { - restClient.performRequest(method, "/" + statusCode, null, (HttpEntity)null); - fail("request should have failed"); - } catch(NullPointerException e) { - assertEquals("parameters cannot be null", e.getMessage()); - } - } - /** * End to end test for request and response headers. Exercises the mock http client ability to send back * whatever headers it has received. @@ -464,35 +418,4 @@ private HttpUriRequest performRandomRequest(String method) throws Exception { } return expectedRequest; } - - /** - * @deprecated prefer {@link RestClient#performRequest(Request)}. - */ - @Deprecated - private Response performRequest(String method, String endpoint, Header... headers) throws IOException { - return performRequest(method, endpoint, Collections.emptyMap(), headers); - } - - /** - * @deprecated prefer {@link RestClient#performRequest(Request)}. - */ - @Deprecated - private Response performRequest(String method, String endpoint, Map params, Header... headers) throws IOException { - int methodSelector; - if (params.isEmpty()) { - methodSelector = randomIntBetween(0, 2); - } else { - methodSelector = randomIntBetween(1, 2); - } - switch(methodSelector) { - case 0: - return restClient.performRequest(method, endpoint, headers); - case 1: - return restClient.performRequest(method, endpoint, params, headers); - case 2: - return restClient.performRequest(method, endpoint, params, (HttpEntity)null, headers); - default: - throw new UnsupportedOperationException(); - } - } } diff --git a/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java b/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java index ef94b70542f6..4a037b18404a 100644 --- a/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java +++ b/client/rest/src/test/java/org/elasticsearch/client/RestClientTests.java @@ -42,7 +42,6 @@ import java.util.concurrent.atomic.AtomicInteger; import static java.util.Collections.singletonList; -import static org.elasticsearch.client.RestClientTestUtil.getHttpMethods; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; @@ -90,88 +89,6 @@ public void onFailure(Exception exception) { } } - /** - * @deprecated will remove method in 7.0 but needs tests until then. Replaced by {@link #testPerformAsyncWithUnsupportedMethod()}. - */ - @Deprecated - public void testPerformAsyncOldStyleWithUnsupportedMethod() throws Exception { - final CountDownLatch latch = new CountDownLatch(1); - try (RestClient restClient = createRestClient()) { - restClient.performRequestAsync("unsupported", randomAsciiLettersOfLength(5), new ResponseListener() { - @Override - public void onSuccess(Response response) { - throw new UnsupportedOperationException("onSuccess cannot be called when using a mocked http client"); - } - - @Override - public void onFailure(Exception exception) { - try { - assertThat(exception, instanceOf(UnsupportedOperationException.class)); - assertEquals("http method not supported: unsupported", exception.getMessage()); - } finally { - latch.countDown(); - } - } - }); - assertTrue("time out waiting for request to return", latch.await(1000, TimeUnit.MILLISECONDS)); - } - } - - /** - * @deprecated will remove method in 7.0 but needs tests until then. Replaced by {@link RequestTests#testAddParameters()}. - */ - @Deprecated - public void testPerformOldStyleAsyncWithNullParams() throws Exception { - final CountDownLatch latch = new CountDownLatch(1); - try (RestClient restClient = createRestClient()) { - restClient.performRequestAsync(randomAsciiLettersOfLength(5), randomAsciiLettersOfLength(5), null, new ResponseListener() { - @Override - public void onSuccess(Response response) { - throw new UnsupportedOperationException("onSuccess cannot be called when using a mocked http client"); - } - - @Override - public void onFailure(Exception exception) { - try { - assertThat(exception, instanceOf(NullPointerException.class)); - assertEquals("parameters cannot be null", exception.getMessage()); - } finally { - latch.countDown(); - } - } - }); - assertTrue("time out waiting for request to return", latch.await(1000, TimeUnit.MILLISECONDS)); - } - } - - /** - * @deprecated will remove method in 7.0 but needs tests until then. Replaced by {@link RequestTests}. - */ - @Deprecated - public void testPerformOldStyleAsyncWithNullHeaders() throws Exception { - final CountDownLatch latch = new CountDownLatch(1); - try (RestClient restClient = createRestClient()) { - ResponseListener listener = new ResponseListener() { - @Override - public void onSuccess(Response response) { - throw new UnsupportedOperationException("onSuccess cannot be called when using a mocked http client"); - } - - @Override - public void onFailure(Exception exception) { - try { - assertThat(exception, instanceOf(NullPointerException.class)); - assertEquals("header cannot be null", exception.getMessage()); - } finally { - latch.countDown(); - } - } - }; - restClient.performRequestAsync("GET", randomAsciiLettersOfLength(5), listener, (Header) null); - assertTrue("time out waiting for request to return", latch.await(1000, TimeUnit.MILLISECONDS)); - } - } - public void testPerformAsyncWithWrongEndpoint() throws Exception { final CountDownLatch latch = new CountDownLatch(1); try (RestClient restClient = createRestClient()) { @@ -195,33 +112,6 @@ public void onFailure(Exception exception) { } } - /** - * @deprecated will remove method in 7.0 but needs tests until then. Replaced by {@link #testPerformAsyncWithWrongEndpoint()}. - */ - @Deprecated - public void testPerformAsyncOldStyleWithWrongEndpoint() throws Exception { - final CountDownLatch latch = new CountDownLatch(1); - try (RestClient restClient = createRestClient()) { - restClient.performRequestAsync("GET", "::http:///", new ResponseListener() { - @Override - public void onSuccess(Response response) { - throw new UnsupportedOperationException("onSuccess cannot be called when using a mocked http client"); - } - - @Override - public void onFailure(Exception exception) { - try { - assertThat(exception, instanceOf(IllegalArgumentException.class)); - assertEquals("Expected scheme name at index 0: ::http:///", exception.getMessage()); - } finally { - latch.countDown(); - } - } - }); - assertTrue("time out waiting for request to return", latch.await(1000, TimeUnit.MILLISECONDS)); - } - } - public void testBuildUriLeavesPathUntouched() { final Map emptyMap = Collections.emptyMap(); { @@ -259,34 +149,6 @@ public void testBuildUriLeavesPathUntouched() { } } - @Deprecated - public void testSetHostsWrongArguments() throws IOException { - try (RestClient restClient = createRestClient()) { - restClient.setHosts((HttpHost[]) null); - fail("setHosts should have failed"); - } catch (IllegalArgumentException e) { - assertEquals("hosts must not be null nor empty", e.getMessage()); - } - try (RestClient restClient = createRestClient()) { - restClient.setHosts(); - fail("setHosts should have failed"); - } catch (IllegalArgumentException e) { - assertEquals("hosts must not be null nor empty", e.getMessage()); - } - try (RestClient restClient = createRestClient()) { - restClient.setHosts((HttpHost) null); - fail("setHosts should have failed"); - } catch (IllegalArgumentException e) { - assertEquals("host cannot be null", e.getMessage()); - } - try (RestClient restClient = createRestClient()) { - restClient.setHosts(new HttpHost("localhost", 9200), null, new HttpHost("localhost", 9201)); - fail("setHosts should have failed"); - } catch (IllegalArgumentException e) { - assertEquals("host cannot be null", e.getMessage()); - } - } - public void testSetNodesWrongArguments() throws IOException { try (RestClient restClient = createRestClient()) { restClient.setNodes(null); @@ -348,23 +210,6 @@ public void testSetNodesDuplicatedHosts() throws Exception { } } - /** - * @deprecated will remove method in 7.0 but needs tests until then. Replaced by {@link RequestTests#testConstructor()}. - */ - @Deprecated - public void testNullPath() throws IOException { - try (RestClient restClient = createRestClient()) { - for (String method : getHttpMethods()) { - try { - restClient.performRequest(method, null); - fail("path set to null should fail!"); - } catch (NullPointerException e) { - assertEquals("endpoint cannot be null", e.getMessage()); - } - } - } - } - public void testSelectHosts() throws IOException { Node n1 = new Node(new HttpHost("1"), null, null, "1", null, null); Node n2 = new Node(new HttpHost("2"), null, null, "2", null, null); diff --git a/docs/reference/migration/migrate_7_0.asciidoc b/docs/reference/migration/migrate_7_0.asciidoc index 42fd6b7afbe7..924a6984dc04 100644 --- a/docs/reference/migration/migrate_7_0.asciidoc +++ b/docs/reference/migration/migrate_7_0.asciidoc @@ -39,6 +39,7 @@ Elasticsearch 6.x in order to be readable by Elasticsearch 7.x. * <> * <> * <> +* <> include::migrate_7_0/aggregations.asciidoc[] include::migrate_7_0/analysis.asciidoc[] @@ -53,4 +54,5 @@ include::migrate_7_0/java.asciidoc[] include::migrate_7_0/settings.asciidoc[] include::migrate_7_0/scripting.asciidoc[] include::migrate_7_0/snapshotstats.asciidoc[] -include::migrate_7_0/restclient.asciidoc[] \ No newline at end of file +include::migrate_7_0/restclient.asciidoc[] +include::migrate_7_0/low_level_restclient.asciidoc[] diff --git a/docs/reference/migration/migrate_7_0/low_level_restclient.asciidoc b/docs/reference/migration/migrate_7_0/low_level_restclient.asciidoc new file mode 100644 index 000000000000..77f5266763ff --- /dev/null +++ b/docs/reference/migration/migrate_7_0/low_level_restclient.asciidoc @@ -0,0 +1,14 @@ +[[breaking_70_low_level_restclient_changes]] +=== Low-level REST client changes + +==== Deprecated flavors of performRequest have been removed + +We deprecated the flavors of `performRequest` and `performRequestAsync` that +do not take `Request` objects in 6.4.0 in favor of the flavors that take +`Request` objects because those methods can be extended without breaking +backwards compatibility. + +==== Removed setHosts + +We deprecated `setHosts` in 6.4.0 in favor of `setNodes` because it supports +host metadata used by the `NodeSelector`. From 19b14fa5edde2e0a56ea179b5aeb51c59f852f61 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Sat, 1 Sep 2018 13:32:18 -0500 Subject: [PATCH 280/283] HLRC: Adding ML Job stats (#33183) * HLRC: Adding pojos for get job stats HLRC: Adding pojos for job stats request * HLRC: Adding job stats pojos * HLRC: ML job stats * Minor syntax changes and adding license headers * minor comment change * Moving to client package, minor changes * Addressing PR comments * removing bad sleep * addressing minor comment around test methods * adding toplevel random fields for tests * addressing minor review comments --- .../client/MLRequestConverters.java | 18 ++ .../client/MachineLearningClient.java | 44 ++++ .../client/ml/GetJobStatsRequest.java | 146 ++++++++++++ .../client/ml/GetJobStatsResponse.java | 88 +++++++ .../client/ml/NodeAttributes.java | 150 ++++++++++++ .../client/ml/job/config/JobState.java | 39 +++ .../client/ml/job/stats/ForecastStats.java | 174 ++++++++++++++ .../client/ml/job/stats/JobStats.java | 225 ++++++++++++++++++ .../client/ml/job/stats/SimpleStats.java | 117 +++++++++ .../client/MLRequestConvertersTests.java | 18 ++ .../client/MachineLearningIT.java | 66 ++++- .../MlClientDocumentationIT.java | 61 +++++ .../client/ml/GetJobResponseTests.java | 8 +- .../client/ml/GetJobStatsRequestTests.java | 69 ++++++ .../client/ml/GetJobStatsResponseTests.java | 53 +++++ .../client/ml/NodeAttributesTests.java | 64 +++++ .../ml/job/stats/ForecastStatsTests.java | 70 ++++++ .../client/ml/job/stats/JobStatsTests.java | 72 ++++++ .../client/ml/job/stats/SimpleStatsTests.java | 47 ++++ .../high-level/ml/get-job-stats.asciidoc | 67 ++++++ .../high-level/supported-apis.asciidoc | 2 + 21 files changed, 1596 insertions(+), 2 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsResponse.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/NodeAttributes.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/JobState.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/ForecastStats.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/JobStats.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/SimpleStats.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsResponseTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/NodeAttributesTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/ForecastStatsTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/JobStatsTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/SimpleStatsTests.java create mode 100644 docs/java-rest/high-level/ml/get-job-stats.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 30e79d1dce2f..3fda07e67280 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.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.GetBucketsRequest; import org.elasticsearch.client.ml.GetJobRequest; +import org.elasticsearch.client.ml.GetJobStatsRequest; import org.elasticsearch.client.ml.GetRecordsRequest; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.PutJobRequest; @@ -126,6 +127,23 @@ static Request getBuckets(GetBucketsRequest getBucketsRequest) throws IOExceptio return request; } + static Request getJobStats(GetJobStatsRequest getJobStatsRequest) { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(Strings.collectionToCommaDelimitedString(getJobStatsRequest.getJobIds())) + .addPathPartAsIs("_stats") + .build(); + Request request = new Request(HttpGet.METHOD_NAME, endpoint); + + RequestConverters.Params params = new RequestConverters.Params(request); + if (getJobStatsRequest.isAllowNoJobs() != null) { + params.putParam("allow_no_jobs", Boolean.toString(getJobStatsRequest.isAllowNoJobs())); + } + return request; + } + static Request getRecords(GetRecordsRequest getRecordsRequest) 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 a972f760d2fd..59e222c5e4cd 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,9 @@ package org.elasticsearch.client; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.ml.GetJobStatsRequest; +import org.elasticsearch.client.ml.GetJobStatsResponse; +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.DeleteJobRequest; @@ -288,6 +291,47 @@ public void getBucketsAsync(GetBucketsRequest request, RequestOptions options, A Collections.emptySet()); } + /** + * Gets usage statistics for one or more Machine Learning jobs + * + *

    + * For additional info + * see Get Job stats docs + *

    + * @param request {@link GetJobStatsRequest} Request containing a list of jobId(s) and additional options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return {@link GetJobStatsResponse} response object containing + * the {@link JobStats} objects and the number of jobs found + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public GetJobStatsResponse getJobStats(GetJobStatsRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::getJobStats, + options, + GetJobStatsResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Gets one or more Machine Learning job configuration info, asynchronously. + * + *

    + * For additional info + * see Get Job stats docs + *

    + * @param request {@link GetJobStatsRequest} Request containing a list of jobId(s) and additional 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 with {@link GetJobStatsResponse} upon request completion + */ + public void getJobStatsAsync(GetJobStatsRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::getJobStats, + options, + GetJobStatsResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Gets the records for a Machine Learning Job. *

    diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsRequest.java new file mode 100644 index 000000000000..d8eb350755dc --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsRequest.java @@ -0,0 +1,146 @@ +/* + * 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.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +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; + + +/** + * Request object to get {@link org.elasticsearch.client.ml.job.stats.JobStats} by their respective jobIds + * + * `_all` explicitly gets all the jobs' statistics in the cluster + * An empty request (no `jobId`s) implicitly gets all the jobs' statistics in the cluster + */ +public class GetJobStatsRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField ALLOW_NO_JOBS = new ParseField("allow_no_jobs"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "get_jobs_stats_request", a -> new GetJobStatsRequest((List) a[0])); + + static { + PARSER.declareField(ConstructingObjectParser.constructorArg(), + p -> Arrays.asList(Strings.commaDelimitedListToStringArray(p.text())), + Job.ID, ObjectParser.ValueType.STRING_ARRAY); + PARSER.declareBoolean(GetJobStatsRequest::setAllowNoJobs, ALLOW_NO_JOBS); + } + + private static final String ALL_JOBS = "_all"; + + private final List jobIds; + private Boolean allowNoJobs; + + /** + * Explicitly gets all jobs statistics + * + * @return a {@link GetJobStatsRequest} for all existing jobs + */ + public static GetJobStatsRequest getAllJobStatsRequest(){ + return new GetJobStatsRequest(ALL_JOBS); + } + + GetJobStatsRequest(List jobIds) { + if (jobIds.stream().anyMatch(Objects::isNull)) { + throw new NullPointerException("jobIds must not contain null values"); + } + this.jobIds = new ArrayList<>(jobIds); + } + + /** + * Get the specified Job's statistics via their unique jobIds + * + * @param jobIds must be non-null and each jobId must be non-null + */ + public GetJobStatsRequest(String... jobIds) { + this(Arrays.asList(jobIds)); + } + + /** + * All the jobIds for which to get statistics + */ + public List getJobIds() { + return jobIds; + } + + public Boolean isAllowNoJobs() { + return this.allowNoJobs; + } + + /** + * Whether to ignore if a wildcard expression matches no jobs. + * + * This includes `_all` string or when no jobs have been specified + * + * @param allowNoJobs When {@code true} ignore if wildcard or `_all` matches no jobs. Defaults to {@code true} + */ + public void setAllowNoJobs(boolean allowNoJobs) { + this.allowNoJobs = allowNoJobs; + } + + @Override + public int hashCode() { + return Objects.hash(jobIds, allowNoJobs); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + GetJobStatsRequest that = (GetJobStatsRequest) other; + return Objects.equals(jobIds, that.jobIds) && + Objects.equals(allowNoJobs, that.allowNoJobs); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Job.ID.getPreferredName(), Strings.collectionToCommaDelimitedString(jobIds)); + if (allowNoJobs != null) { + builder.field(ALLOW_NO_JOBS.getPreferredName(), allowNoJobs); + } + builder.endObject(); + return builder; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsResponse.java new file mode 100644 index 000000000000..2e3ba113d193 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/GetJobStatsResponse.java @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.client.ml.job.stats.JobStats; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * Contains a {@link List} of the found {@link JobStats} objects and the total count found + */ +public class GetJobStatsResponse extends AbstractResultResponse { + + public static final ParseField RESULTS_FIELD = new ParseField("jobs"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("jobs_stats_response", true, + a -> new GetJobStatsResponse((List) a[0], (long) a[1])); + + static { + PARSER.declareObjectArray(constructorArg(), JobStats.PARSER, RESULTS_FIELD); + PARSER.declareLong(constructorArg(), COUNT); + } + + GetJobStatsResponse(List jobStats, long count) { + super(RESULTS_FIELD, jobStats, count); + } + + /** + * The collection of {@link JobStats} objects found in the query + */ + public List jobStats() { + return results; + } + + public static GetJobStatsResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public int hashCode() { + return Objects.hash(results, count); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + GetJobStatsResponse other = (GetJobStatsResponse) obj; + return Objects.equals(results, other.results) && count == other.count; + } + + @Override + public final String toString() { + return Strings.toString(this); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/NodeAttributes.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/NodeAttributes.java new file mode 100644 index 000000000000..892df340abd6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/NodeAttributes.java @@ -0,0 +1,150 @@ +/* + * 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.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +/** + * A Pojo class containing an Elastic Node's attributes + */ +public class NodeAttributes implements ToXContentObject { + + public static final ParseField ID = new ParseField("id"); + public static final ParseField NAME = new ParseField("name"); + public static final ParseField EPHEMERAL_ID = new ParseField("ephemeral_id"); + public static final ParseField TRANSPORT_ADDRESS = new ParseField("transport_address"); + public static final ParseField ATTRIBUTES = new ParseField("attributes"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("node", true, + (a) -> { + int i = 0; + String id = (String) a[i++]; + String name = (String) a[i++]; + String ephemeralId = (String) a[i++]; + String transportAddress = (String) a[i++]; + Map attributes = (Map) a[i]; + return new NodeAttributes(id, name, ephemeralId, transportAddress, attributes); + }); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), ID); + PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME); + PARSER.declareString(ConstructingObjectParser.constructorArg(), EPHEMERAL_ID); + PARSER.declareString(ConstructingObjectParser.constructorArg(), TRANSPORT_ADDRESS); + PARSER.declareField(ConstructingObjectParser.constructorArg(), + (p, c) -> p.mapStrings(), + ATTRIBUTES, + ObjectParser.ValueType.OBJECT); + } + + private final String id; + private final String name; + private final String ephemeralId; + private final String transportAddress; + private final Map attributes; + + public NodeAttributes(String id, String name, String ephemeralId, String transportAddress, Map attributes) { + this.id = id; + this.name = name; + this.ephemeralId = ephemeralId; + this.transportAddress = transportAddress; + this.attributes = Collections.unmodifiableMap(attributes); + } + + /** + * The unique identifier of the node. + */ + public String getId() { + return id; + } + + /** + * The node name. + */ + public String getName() { + return name; + } + + /** + * The ephemeral id of the node. + */ + public String getEphemeralId() { + return ephemeralId; + } + + /** + * The host and port where transport HTTP connections are accepted. + */ + public String getTransportAddress() { + return transportAddress; + } + + /** + * Additional attributes related to this node e.g., {"ml.max_open_jobs": "10"}. + */ + public Map getAttributes() { + return attributes; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(ID.getPreferredName(), id); + builder.field(NAME.getPreferredName(), name); + builder.field(EPHEMERAL_ID.getPreferredName(), ephemeralId); + builder.field(TRANSPORT_ADDRESS.getPreferredName(), transportAddress); + builder.field(ATTRIBUTES.getPreferredName(), attributes); + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(id, name, ephemeralId, transportAddress, attributes); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + NodeAttributes that = (NodeAttributes) other; + return Objects.equals(id, that.id) && + Objects.equals(name, that.name) && + Objects.equals(ephemeralId, that.ephemeralId) && + Objects.equals(transportAddress, that.transportAddress) && + Objects.equals(attributes, that.attributes); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/JobState.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/JobState.java new file mode 100644 index 000000000000..32684bd7e62b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/JobState.java @@ -0,0 +1,39 @@ +/* + * 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.job.config; + +import java.util.Locale; + +/** + * Jobs whether running or complete are in one of these states. + * When a job is created it is initialised in the state closed + * i.e. it is not running. + */ +public enum JobState { + + CLOSING, CLOSED, OPENED, FAILED, OPENING; + + public static JobState fromString(String name) { + return valueOf(name.trim().toUpperCase(Locale.ROOT)); + } + + public String value() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/ForecastStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/ForecastStats.java new file mode 100644 index 000000000000..a6b41beca836 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/ForecastStats.java @@ -0,0 +1,174 @@ +/* + * 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.job.stats; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A class to hold statistics about forecasts. + */ +public class ForecastStats implements ToXContentObject { + + public static final ParseField TOTAL = new ParseField("total"); + public static final ParseField FORECASTED_JOBS = new ParseField("forecasted_jobs"); + public static final ParseField MEMORY_BYTES = new ParseField("memory_bytes"); + public static final ParseField PROCESSING_TIME_MS = new ParseField("processing_time_ms"); + public static final ParseField RECORDS = new ParseField("records"); + public static final ParseField STATUS = new ParseField("status"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("forecast_stats", + true, + (a) -> { + int i = 0; + long total = (long)a[i++]; + SimpleStats memoryStats = (SimpleStats)a[i++]; + SimpleStats recordStats = (SimpleStats)a[i++]; + SimpleStats runtimeStats = (SimpleStats)a[i++]; + Map statusCounts = (Map)a[i]; + return new ForecastStats(total, memoryStats, recordStats, runtimeStats, statusCounts); + }); + + static { + PARSER.declareLong(ConstructingObjectParser.constructorArg(), TOTAL); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), SimpleStats.PARSER, MEMORY_BYTES); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), SimpleStats.PARSER, RECORDS); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), SimpleStats.PARSER, PROCESSING_TIME_MS); + PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), + p -> { + Map counts = new HashMap<>(); + p.map().forEach((key, value) -> counts.put(key, ((Number)value).longValue())); + return counts; + }, STATUS, ObjectParser.ValueType.OBJECT); + } + + private final long total; + private final long forecastedJobs; + private SimpleStats memoryStats; + private SimpleStats recordStats; + private SimpleStats runtimeStats; + private Map statusCounts; + + public ForecastStats(long total, + SimpleStats memoryStats, + SimpleStats recordStats, + SimpleStats runtimeStats, + Map statusCounts) { + this.total = total; + this.forecastedJobs = total > 0 ? 1 : 0; + if (total > 0) { + this.memoryStats = Objects.requireNonNull(memoryStats); + this.recordStats = Objects.requireNonNull(recordStats); + this.runtimeStats = Objects.requireNonNull(runtimeStats); + this.statusCounts = Collections.unmodifiableMap(statusCounts); + } + } + + /** + * The number of forecasts currently available for this model. + */ + public long getTotal() { + return total; + } + + /** + * The number of jobs that have at least one forecast. + */ + public long getForecastedJobs() { + return forecastedJobs; + } + + /** + * Statistics about the memory usage: minimum, maximum, average and total. + */ + public SimpleStats getMemoryStats() { + return memoryStats; + } + + /** + * Statistics about the number of forecast records: minimum, maximum, average and total. + */ + public SimpleStats getRecordStats() { + return recordStats; + } + + /** + * Statistics about the forecast runtime in milliseconds: minimum, maximum, average and total + */ + public SimpleStats getRuntimeStats() { + return runtimeStats; + } + + /** + * Counts per forecast status, for example: {"finished" : 2}. + */ + public Map getStatusCounts() { + return statusCounts; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(TOTAL.getPreferredName(), total); + builder.field(FORECASTED_JOBS.getPreferredName(), forecastedJobs); + + if (total > 0) { + builder.field(MEMORY_BYTES.getPreferredName(), memoryStats); + builder.field(RECORDS.getPreferredName(), recordStats); + builder.field(PROCESSING_TIME_MS.getPreferredName(), runtimeStats); + builder.field(STATUS.getPreferredName(), statusCounts); + } + return builder.endObject(); + } + + @Override + public int hashCode() { + return Objects.hash(total, forecastedJobs, memoryStats, recordStats, runtimeStats, statusCounts); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ForecastStats other = (ForecastStats) obj; + return Objects.equals(total, other.total) && + Objects.equals(forecastedJobs, other.forecastedJobs) && + Objects.equals(memoryStats, other.memoryStats) && + Objects.equals(recordStats, other.recordStats) && + Objects.equals(runtimeStats, other.runtimeStats) && + Objects.equals(statusCounts, other.statusCounts); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/JobStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/JobStats.java new file mode 100644 index 000000000000..df5be4aa4c5c --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/JobStats.java @@ -0,0 +1,225 @@ +/* + * 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.job.stats; + +import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.client.ml.job.config.JobState; +import org.elasticsearch.client.ml.job.process.DataCounts; +import org.elasticsearch.client.ml.job.process.ModelSizeStats; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +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.client.ml.NodeAttributes; + +import java.io.IOException; +import java.util.Objects; + +/** + * Class containing the statistics for a Machine Learning job. + * + */ +public class JobStats implements ToXContentObject { + + private static final ParseField DATA_COUNTS = new ParseField("data_counts"); + private static final ParseField MODEL_SIZE_STATS = new ParseField("model_size_stats"); + private static final ParseField FORECASTS_STATS = new ParseField("forecasts_stats"); + private static final ParseField STATE = new ParseField("state"); + private static final ParseField NODE = new ParseField("node"); + private static final ParseField OPEN_TIME = new ParseField("open_time"); + private static final ParseField ASSIGNMENT_EXPLANATION = new ParseField("assignment_explanation"); + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("job_stats", + true, + (a) -> { + int i = 0; + String jobId = (String) a[i++]; + DataCounts dataCounts = (DataCounts) a[i++]; + JobState jobState = (JobState) a[i++]; + ModelSizeStats.Builder modelSizeStatsBuilder = (ModelSizeStats.Builder) a[i++]; + ModelSizeStats modelSizeStats = modelSizeStatsBuilder == null ? null : modelSizeStatsBuilder.build(); + ForecastStats forecastStats = (ForecastStats) a[i++]; + NodeAttributes node = (NodeAttributes) a[i++]; + String assignmentExplanation = (String) a[i++]; + TimeValue openTime = (TimeValue) a[i]; + return new JobStats(jobId, + dataCounts, + jobState, + modelSizeStats, + forecastStats, + node, + assignmentExplanation, + openTime); + }); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID); + PARSER.declareObject(ConstructingObjectParser.constructorArg(), DataCounts.PARSER, DATA_COUNTS); + PARSER.declareField(ConstructingObjectParser.constructorArg(), + (p) -> JobState.fromString(p.text()), + STATE, + ObjectParser.ValueType.VALUE); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), ModelSizeStats.PARSER, MODEL_SIZE_STATS); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), ForecastStats.PARSER, FORECASTS_STATS); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), NodeAttributes.PARSER, NODE); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), ASSIGNMENT_EXPLANATION); + PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> TimeValue.parseTimeValue(p.textOrNull(), OPEN_TIME.getPreferredName()), + OPEN_TIME, + ObjectParser.ValueType.STRING_OR_NULL); + } + + + private final String jobId; + private final DataCounts dataCounts; + private final JobState state; + private final ModelSizeStats modelSizeStats; + private final ForecastStats forecastStats; + private final NodeAttributes node; + private final String assignmentExplanation; + private final TimeValue openTime; + + JobStats(String jobId, DataCounts dataCounts, JobState state, @Nullable ModelSizeStats modelSizeStats, + @Nullable ForecastStats forecastStats, @Nullable NodeAttributes node, + @Nullable String assignmentExplanation, @Nullable TimeValue opentime) { + this.jobId = Objects.requireNonNull(jobId); + this.dataCounts = Objects.requireNonNull(dataCounts); + this.state = Objects.requireNonNull(state); + this.modelSizeStats = modelSizeStats; + this.forecastStats = forecastStats; + this.node = node; + this.assignmentExplanation = assignmentExplanation; + this.openTime = opentime; + } + + /** + * The jobId referencing the job for these statistics + */ + public String getJobId() { + return jobId; + } + + /** + * An object that describes the number of records processed and any related error counts + * See {@link DataCounts} + */ + public DataCounts getDataCounts() { + return dataCounts; + } + + /** + * An object that provides information about the size and contents of the model. + * See {@link ModelSizeStats} + */ + public ModelSizeStats getModelSizeStats() { + return modelSizeStats; + } + + /** + * An object that provides statistical information about forecasts of this job. + * See {@link ForecastStats} + */ + public ForecastStats getForecastStats() { + return forecastStats; + } + + /** + * The status of the job + * See {@link JobState} + */ + public JobState getState() { + return state; + } + + /** + * For open jobs only, contains information about the node where the job runs + * See {@link NodeAttributes} + */ + public NodeAttributes getNode() { + return node; + } + + /** + * For open jobs only, contains messages relating to the selection of a node to run the job. + */ + public String getAssignmentExplanation() { + return assignmentExplanation; + } + + /** + * For open jobs only, the elapsed time for which the job has been open + */ + public TimeValue getOpenTime() { + return openTime; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Job.ID.getPreferredName(), jobId); + builder.field(DATA_COUNTS.getPreferredName(), dataCounts); + builder.field(STATE.getPreferredName(), state.toString()); + if (modelSizeStats != null) { + builder.field(MODEL_SIZE_STATS.getPreferredName(), modelSizeStats); + } + if (forecastStats != null) { + builder.field(FORECASTS_STATS.getPreferredName(), forecastStats); + } + if (node != null) { + builder.field(NODE.getPreferredName(), node); + } + if (assignmentExplanation != null) { + builder.field(ASSIGNMENT_EXPLANATION.getPreferredName(), assignmentExplanation); + } + if (openTime != null) { + builder.field(OPEN_TIME.getPreferredName(), openTime.getStringRep()); + } + return builder.endObject(); + } + + @Override + public int hashCode() { + return Objects.hash(jobId, dataCounts, modelSizeStats, forecastStats, state, node, assignmentExplanation, openTime); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + JobStats other = (JobStats) obj; + return Objects.equals(jobId, other.jobId) && + Objects.equals(this.dataCounts, other.dataCounts) && + Objects.equals(this.modelSizeStats, other.modelSizeStats) && + Objects.equals(this.forecastStats, other.forecastStats) && + Objects.equals(this.state, other.state) && + Objects.equals(this.node, other.node) && + Objects.equals(this.assignmentExplanation, other.assignmentExplanation) && + Objects.equals(this.openTime, other.openTime); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/SimpleStats.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/SimpleStats.java new file mode 100644 index 000000000000..f4c8aa0fa3b2 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/stats/SimpleStats.java @@ -0,0 +1,117 @@ +/* + * 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.job.stats; + +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; + +/** + * Helper class for min, max, avg and total statistics for a quantity + */ +public class SimpleStats implements ToXContentObject { + + public static final ParseField MIN = new ParseField("min"); + public static final ParseField MAX = new ParseField("max"); + public static final ParseField AVG = new ParseField("avg"); + public static final ParseField TOTAL = new ParseField("total"); + + public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("simple_stats", true, + (a) -> { + int i = 0; + double total = (double)a[i++]; + double min = (double)a[i++]; + double max = (double)a[i++]; + double avg = (double)a[i++]; + return new SimpleStats(total, min, max, avg); + }); + + static { + PARSER.declareDouble(ConstructingObjectParser.constructorArg(), TOTAL); + PARSER.declareDouble(ConstructingObjectParser.constructorArg(), MIN); + PARSER.declareDouble(ConstructingObjectParser.constructorArg(), MAX); + PARSER.declareDouble(ConstructingObjectParser.constructorArg(), AVG); + } + + private final double total; + private final double min; + private final double max; + private final double avg; + + SimpleStats(double total, double min, double max, double avg) { + this.total = total; + this.min = min; + this.max = max; + this.avg = avg; + } + + public double getMin() { + return min; + } + + public double getMax() { + return max; + } + + public double getAvg() { + return avg; + } + + public double getTotal() { + return total; + } + + @Override + public int hashCode() { + return Objects.hash(total, min, max, avg); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + SimpleStats other = (SimpleStats) obj; + return Objects.equals(total, other.total) && + Objects.equals(min, other.min) && + Objects.equals(avg, other.avg) && + Objects.equals(max, other.max); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(MIN.getPreferredName(), min); + builder.field(MAX.getPreferredName(), max); + builder.field(AVG.getPreferredName(), avg); + builder.field(TOTAL.getPreferredName(), total); + 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 43f3ef41a8d7..4950a1c8139f 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 @@ -36,6 +36,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.client.ml.GetJobStatsRequest; import org.elasticsearch.test.ESTestCase; import java.io.ByteArrayOutputStream; @@ -139,6 +140,23 @@ public void testGetBuckets() throws IOException { } } + public void testGetJobStats() { + GetJobStatsRequest getJobStatsRequestRequest = new GetJobStatsRequest(); + + Request request = MLRequestConverters.getJobStats(getJobStatsRequestRequest); + + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/_stats", request.getEndpoint()); + assertFalse(request.getParameters().containsKey("allow_no_jobs")); + + getJobStatsRequestRequest = new GetJobStatsRequest("job1", "jobs*"); + getJobStatsRequestRequest.setAllowNoJobs(true); + request = MLRequestConverters.getJobStats(getJobStatsRequestRequest); + + assertEquals("/_xpack/ml/anomaly_detectors/job1,jobs*/_stats", request.getEndpoint()); + assertEquals(Boolean.toString(true), request.getParameters().get("allow_no_jobs")); + } + private static Job createValidJob(String jobId) { AnalysisConfig.Builder analysisConfig = AnalysisConfig.builder(Collections.singletonList( Detector.builder().setFunction("count").build())); 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 cb9dbea129d2..7b8e4b3e4c4c 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 @@ -19,6 +19,12 @@ package org.elasticsearch.client; import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; +import org.elasticsearch.ElasticsearchStatusException; +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.DeleteJobRequest; @@ -33,7 +39,6 @@ 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.common.unit.TimeValue; import org.junit.After; import java.io.IOException; @@ -41,6 +46,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; @@ -138,6 +144,64 @@ public void testCloseJob() throws Exception { assertTrue(response.isClosed()); } + public void testGetJobStats() throws Exception { + String jobId1 = "ml-get-job-stats-test-id-1"; + String jobId2 = "ml-get-job-stats-test-id-2"; + + Job job1 = buildJob(jobId1); + Job job2 = buildJob(jobId2); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job1), RequestOptions.DEFAULT); + machineLearningClient.putJob(new PutJobRequest(job2), RequestOptions.DEFAULT); + + machineLearningClient.openJob(new OpenJobRequest(jobId1), RequestOptions.DEFAULT); + + GetJobStatsRequest request = new GetJobStatsRequest(jobId1, jobId2); + + // Test getting specific + GetJobStatsResponse response = execute(request, machineLearningClient::getJobStats, machineLearningClient::getJobStatsAsync); + + assertEquals(2, response.count()); + assertThat(response.jobStats(), hasSize(2)); + assertThat(response.jobStats().stream().map(JobStats::getJobId).collect(Collectors.toList()), containsInAnyOrder(jobId1, jobId2)); + for (JobStats stats : response.jobStats()) { + if (stats.getJobId().equals(jobId1)) { + assertEquals(JobState.OPENED, stats.getState()); + } else { + assertEquals(JobState.CLOSED, stats.getState()); + } + } + + // Test getting all explicitly + request = GetJobStatsRequest.getAllJobStatsRequest(); + response = execute(request, machineLearningClient::getJobStats, machineLearningClient::getJobStatsAsync); + + assertTrue(response.count() >= 2L); + assertTrue(response.jobStats().size() >= 2L); + assertThat(response.jobStats().stream().map(JobStats::getJobId).collect(Collectors.toList()), hasItems(jobId1, jobId2)); + + // Test getting all implicitly + response = execute(new GetJobStatsRequest(), machineLearningClient::getJobStats, machineLearningClient::getJobStatsAsync); + + assertTrue(response.count() >= 2L); + assertTrue(response.jobStats().size() >= 2L); + assertThat(response.jobStats().stream().map(JobStats::getJobId).collect(Collectors.toList()), hasItems(jobId1, jobId2)); + + // Test getting all with wildcard + request = new GetJobStatsRequest("ml-get-job-stats-test-id-*"); + response = execute(request, machineLearningClient::getJobStats, machineLearningClient::getJobStatsAsync); + assertTrue(response.count() >= 2L); + assertTrue(response.jobStats().size() >= 2L); + assertThat(response.jobStats().stream().map(JobStats::getJobId).collect(Collectors.toList()), hasItems(jobId1, jobId2)); + + // Test when allow_no_jobs is false + final GetJobStatsRequest erroredRequest = new GetJobStatsRequest("jobs-that-do-not-exist*"); + erroredRequest.setAllowNoJobs(false); + ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class, + () -> execute(erroredRequest, machineLearningClient::getJobStats, machineLearningClient::getJobStatsAsync)); + assertThat(exception.status().getStatus(), equalTo(404)); + } + 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 94793f0ab791..d97db0a311f7 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 @@ -35,6 +35,8 @@ import org.elasticsearch.client.ml.GetBucketsResponse; 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.GetRecordsRequest; import org.elasticsearch.client.ml.GetRecordsResponse; import org.elasticsearch.client.ml.OpenJobRequest; @@ -50,6 +52,7 @@ import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.client.ml.job.stats.JobStats; import org.junit.After; import java.io.IOException; @@ -458,6 +461,64 @@ public void onFailure(Exception e) { } } + public void testGetJobStats() throws Exception { + RestHighLevelClient client = highLevelClient(); + + Job job = MachineLearningIT.buildJob("get-machine-learning-job-stats1"); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + Job secondJob = MachineLearningIT.buildJob("get-machine-learning-job-stats2"); + client.machineLearning().putJob(new PutJobRequest(secondJob), RequestOptions.DEFAULT); + + { + //tag::x-pack-ml-get-job-stats-request + GetJobStatsRequest request = new GetJobStatsRequest("get-machine-learning-job-stats1", "get-machine-learning-job-*"); //<1> + request.setAllowNoJobs(true); //<2> + //end::x-pack-ml-get-job-stats-request + + //tag::x-pack-ml-get-job-stats-execute + GetJobStatsResponse response = client.machineLearning().getJobStats(request, RequestOptions.DEFAULT); + //end::x-pack-ml-get-job-stats-execute + + //tag::x-pack-ml-get-job-stats-response + long numberOfJobStats = response.count(); //<1> + List jobStats = response.jobStats(); //<2> + //end::x-pack-ml-get-job-stats-response + + assertEquals(2, response.count()); + assertThat(response.jobStats(), hasSize(2)); + assertThat(response.jobStats().stream().map(JobStats::getJobId).collect(Collectors.toList()), + containsInAnyOrder(job.getId(), secondJob.getId())); + } + { + GetJobStatsRequest request = new GetJobStatsRequest("get-machine-learning-job-stats1", "get-machine-learning-job-*"); + + // tag::x-pack-ml-get-job-stats-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(GetJobStatsResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::x-pack-ml-get-job-stats-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-job-stats-execute-async + client.machineLearning().getJobStatsAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::x-pack-ml-get-job-stats-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGetRecords() throws IOException, InterruptedException { RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobResponseTests.java index 181804c9676f..8cc990730f78 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobResponseTests.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.function.Predicate; public class GetJobResponseTests extends AbstractXContentTestCase { @@ -46,8 +47,13 @@ protected GetJobResponse doParseInstance(XContentParser parser) throws IOExcepti return GetJobResponse.fromXContent(parser); } + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return field -> !field.isEmpty(); + } + @Override protected boolean supportsUnknownFields() { - return false; + return true; } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsRequestTests.java new file mode 100644 index 000000000000..690e58297665 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsRequestTests.java @@ -0,0 +1,69 @@ +/* + * 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.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GetJobStatsRequestTests extends AbstractXContentTestCase { + + public void testAllJobsRequest() { + GetJobStatsRequest request = GetJobStatsRequest.getAllJobStatsRequest(); + + assertEquals(request.getJobIds().size(), 1); + assertEquals(request.getJobIds().get(0), "_all"); + } + + public void testNewWithJobId() { + Exception exception = expectThrows(NullPointerException.class, () -> new GetJobStatsRequest("job", null)); + assertEquals(exception.getMessage(), "jobIds must not contain null values"); + } + + @Override + protected GetJobStatsRequest createTestInstance() { + int jobCount = randomIntBetween(0, 10); + List jobIds = new ArrayList<>(jobCount); + + for (int i = 0; i < jobCount; i++) { + jobIds.add(randomAlphaOfLength(10)); + } + + GetJobStatsRequest request = new GetJobStatsRequest(jobIds); + + if (randomBoolean()) { + request.setAllowNoJobs(randomBoolean()); + } + + return request; + } + + @Override + protected GetJobStatsRequest doParseInstance(XContentParser parser) throws IOException { + return GetJobStatsRequest.PARSER.parse(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsResponseTests.java new file mode 100644 index 000000000000..23f7bcc042b4 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/GetJobStatsResponseTests.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.common.xcontent.XContentParser; +import org.elasticsearch.client.ml.job.stats.JobStats; +import org.elasticsearch.client.ml.job.stats.JobStatsTests; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class GetJobStatsResponseTests extends AbstractXContentTestCase { + + @Override + protected GetJobStatsResponse createTestInstance() { + + int count = randomIntBetween(1, 5); + List results = new ArrayList<>(count); + for(int i = 0; i < count; i++) { + results.add(JobStatsTests.createRandomInstance()); + } + + return new GetJobStatsResponse(results, count); + } + + @Override + protected GetJobStatsResponse doParseInstance(XContentParser parser) throws IOException { + return GetJobStatsResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/NodeAttributesTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/NodeAttributesTests.java new file mode 100644 index 000000000000..cee1710a6223 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/NodeAttributesTests.java @@ -0,0 +1,64 @@ +/* + * 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.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +public class NodeAttributesTests extends AbstractXContentTestCase { + + public static NodeAttributes createRandom() { + int numberOfAttributes = randomIntBetween(1, 10); + Map attributes = new HashMap<>(numberOfAttributes); + for(int i = 0; i < numberOfAttributes; i++) { + String val = randomAlphaOfLength(10); + attributes.put("key-"+i, val); + } + return new NodeAttributes(randomAlphaOfLength(10), + randomAlphaOfLength(10), + randomAlphaOfLength(10), + randomAlphaOfLength(10), + attributes); + } + + @Override + protected NodeAttributes createTestInstance() { + return createRandom(); + } + + @Override + protected NodeAttributes doParseInstance(XContentParser parser) throws IOException { + return NodeAttributes.PARSER.parse(parser, null); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return field -> !field.isEmpty(); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/ForecastStatsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/ForecastStatsTests.java new file mode 100644 index 000000000000..16dfa305479b --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/ForecastStatsTests.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.job.stats; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +public class ForecastStatsTests extends AbstractXContentTestCase { + + @Override + public ForecastStats createTestInstance() { + if (randomBoolean()) { + return createRandom(1, 22); + } + return new ForecastStats(0, null,null,null,null); + } + + @Override + protected ForecastStats doParseInstance(XContentParser parser) throws IOException { + return ForecastStats.PARSER.parse(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return field -> !field.isEmpty(); + } + + public static ForecastStats createRandom(long minTotal, long maxTotal) { + return new ForecastStats( + randomLongBetween(minTotal, maxTotal), + SimpleStatsTests.createRandom(), + SimpleStatsTests.createRandom(), + SimpleStatsTests.createRandom(), + createCountStats()); + } + + private static Map createCountStats() { + Map countStats = new HashMap<>(); + for (int i = 0; i < randomInt(10); ++i) { + countStats.put(randomAlphaOfLengthBetween(1, 20), randomLongBetween(1L, 100L)); + } + return countStats; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/JobStatsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/JobStatsTests.java new file mode 100644 index 000000000000..5d00f879140e --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/JobStatsTests.java @@ -0,0 +1,72 @@ +/* + * 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.job.stats; + +import org.elasticsearch.client.ml.NodeAttributes; +import org.elasticsearch.client.ml.NodeAttributesTests; +import org.elasticsearch.client.ml.job.process.DataCounts; +import org.elasticsearch.client.ml.job.process.DataCountsTests; +import org.elasticsearch.client.ml.job.process.ModelSizeStats; +import org.elasticsearch.client.ml.job.process.ModelSizeStatsTests; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.client.ml.job.config.JobState; +import org.elasticsearch.client.ml.job.config.JobTests; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.function.Predicate; + + +public class JobStatsTests extends AbstractXContentTestCase { + + public static JobStats createRandomInstance() { + String jobId = JobTests.randomValidJobId(); + JobState state = randomFrom(JobState.CLOSING, JobState.CLOSED, JobState.OPENED, JobState.FAILED, JobState.OPENING); + DataCounts dataCounts = DataCountsTests.createTestInstance(jobId); + + ModelSizeStats modelSizeStats = randomBoolean() ? ModelSizeStatsTests.createRandomized() : null; + ForecastStats forecastStats = randomBoolean() ? ForecastStatsTests.createRandom(1, 22) : null; + NodeAttributes nodeAttributes = randomBoolean() ? NodeAttributesTests.createRandom() : null; + String assigmentExplanation = randomBoolean() ? randomAlphaOfLength(10) : null; + TimeValue openTime = randomBoolean() ? TimeValue.timeValueMillis(randomIntBetween(1, 10000)) : null; + + return new JobStats(jobId, dataCounts, state, modelSizeStats, forecastStats, nodeAttributes, assigmentExplanation, openTime); + } + + @Override + protected JobStats createTestInstance() { + return createRandomInstance(); + } + + @Override + protected JobStats doParseInstance(XContentParser parser) throws IOException { + return JobStats.PARSER.parse(parser, null); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return field -> !field.isEmpty(); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/SimpleStatsTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/SimpleStatsTests.java new file mode 100644 index 000000000000..eb9e47af9ba2 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/stats/SimpleStatsTests.java @@ -0,0 +1,47 @@ +/* + * 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.job.stats; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + + +public class SimpleStatsTests extends AbstractXContentTestCase { + + @Override + protected SimpleStats createTestInstance() { + return createRandom(); + } + + @Override + protected SimpleStats doParseInstance(XContentParser parser) throws IOException { + return SimpleStats.PARSER.parse(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + public static SimpleStats createRandom() { + return new SimpleStats(randomDouble(), randomDouble(), randomDouble(), randomDouble()); + } +} diff --git a/docs/java-rest/high-level/ml/get-job-stats.asciidoc b/docs/java-rest/high-level/ml/get-job-stats.asciidoc new file mode 100644 index 000000000000..90f7794ae765 --- /dev/null +++ b/docs/java-rest/high-level/ml/get-job-stats.asciidoc @@ -0,0 +1,67 @@ +[[java-rest-high-x-pack-ml-get-job-stats]] +=== Get Job Stats API + +The Get Job Stats API provides the ability to get any number of + {ml} job's statistics in the cluster. +It accepts a `GetJobStatsRequest` object and responds +with a `GetJobStatsResponse` object. + +[[java-rest-high-x-pack-ml-get-job-stats-request]] +==== Get Job Stats Request + +A `GetJobsStatsRequest` object can have any number of `jobId` +entries. However, they all must be non-null. An empty list is the same as +requesting statistics for all jobs. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-job-stats-request] +-------------------------------------------------- +<1> Constructing a new request referencing existing `jobIds`, can contain wildcards +<2> Whether to ignore if a wildcard expression matches no jobs. + (This includes `_all` string or when no jobs have been specified) + +[[java-rest-high-x-pack-ml-get-job-stats-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-job-stats-execute] +-------------------------------------------------- + +[[java-rest-high-x-pack-ml-get-job-stats-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-job-stats-execute-async] +-------------------------------------------------- +<1> The `GetJobsStatsRequest` 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 `GetJobsStatsResponse` may +look like + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-job-stats-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-get-job-stats-response]] +==== Get Job Stats Response +The returned `GetJobStatsResponse` contains the requested job statistics: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-job-stats-response] +-------------------------------------------------- +<1> `getCount()` indicates the number of jobs statistics found +<2> `getJobStats()` is the collection of {ml} `JobStats` objects found \ 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 2b72ca74f6aa..76fbc223a1b2 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -211,6 +211,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> * <> * <> @@ -219,6 +220,7 @@ include::ml/get-job.asciidoc[] include::ml/delete-job.asciidoc[] include::ml/open-job.asciidoc[] include::ml/close-job.asciidoc[] +include::ml/get-job-stats.asciidoc[] include::ml/get-buckets.asciidoc[] include::ml/get-records.asciidoc[] From 6770a456b8a1b054948744b2e661cf6ff55c9e01 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Sat, 1 Sep 2018 16:01:23 -0500 Subject: [PATCH 281/283] HLRC: ML Flush job (#33187) * HLRC: ML Flush job * Fixing package, paths, and test * Addressing comments --- .../client/MLRequestConverters.java | 14 ++ .../client/MachineLearningClient.java | 56 +++++ .../client/ml/FlushJobRequest.java | 195 ++++++++++++++++++ .../client/ml/FlushJobResponse.java | 112 ++++++++++ .../client/MLRequestConvertersTests.java | 22 ++ .../client/MachineLearningIT.java | 16 ++ .../MlClientDocumentationIT.java | 65 ++++++ .../client/ml/FlushJobRequestTests.java | 59 ++++++ .../client/ml/FlushJobResponseTests.java | 44 ++++ .../high-level/ml/flush-job.asciidoc | 83 ++++++++ .../high-level/supported-apis.asciidoc | 2 + 11 files changed, 668 insertions(+) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/FlushJobRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/FlushJobResponseTests.java create mode 100644 docs/java-rest/high-level/ml/flush-job.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 3fda07e67280..8a04c229de26 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 @@ -33,6 +33,7 @@ import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.common.Strings; +import org.elasticsearch.client.ml.FlushJobRequest; import java.io.IOException; @@ -127,6 +128,19 @@ static Request getBuckets(GetBucketsRequest getBucketsRequest) throws IOExceptio return request; } + static Request flushJob(FlushJobRequest flushJobRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("anomaly_detectors") + .addPathPart(flushJobRequest.getJobId()) + .addPathPartAsIs("_flush") + .build(); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + request.setEntity(createEntity(flushJobRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request getJobStats(GetJobStatsRequest getJobStatsRequest) { 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 59e222c5e4cd..ac44f16b80b1 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.client.ml.FlushJobRequest; +import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.GetJobStatsRequest; import org.elasticsearch.client.ml.GetJobStatsResponse; import org.elasticsearch.client.ml.job.stats.JobStats; @@ -292,6 +294,60 @@ public void getBucketsAsync(GetBucketsRequest request, RequestOptions options, A } /** + * Flushes internally buffered data for the given Machine Learning Job ensuring all data sent to the has been processed. + * This may cause new results to be calculated depending on the contents of the buffer + * + * Both flush and close operations are similar, + * however the flush is more efficient if you are expecting to send more data for analysis. + * + * When flushing, the job remains open and is available to continue analyzing data. + * A close operation additionally prunes and persists the model state to disk and the + * job must be opened again before analyzing further data. + * + *

    + * For additional info + * see Flush ML job documentation + * + * @param request The {@link FlushJobRequest} object enclosing the `jobId` and additional request options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + */ + public FlushJobResponse flushJob(FlushJobRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::flushJob, + options, + FlushJobResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Flushes internally buffered data for the given Machine Learning Job asynchronously ensuring all data sent to the has been processed. + * This may cause new results to be calculated depending on the contents of the buffer + * + * Both flush and close operations are similar, + * however the flush is more efficient if you are expecting to send more data for analysis. + * + * When flushing, the job remains open and is available to continue analyzing data. + * A close operation additionally prunes and persists the model state to disk and the + * job must be opened again before analyzing further data. + * + *

    + * For additional info + * see Flush ML job documentation + * + * @param request The {@link FlushJobRequest} object enclosing the `jobId` and additional request 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 flushJobAsync(FlushJobRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::flushJob, + options, + FlushJobResponse::fromXContent, + listener, + Collections.emptySet()); + } + + /** * Gets usage statistics for one or more Machine Learning jobs * *

    diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobRequest.java new file mode 100644 index 000000000000..067851d45266 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobRequest.java @@ -0,0 +1,195 @@ +/* + * 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.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * Request object to flush a given Machine Learning job. + */ +public class FlushJobRequest extends ActionRequest implements ToXContentObject { + + public static final ParseField CALC_INTERIM = new ParseField("calc_interim"); + public static final ParseField START = new ParseField("start"); + public static final ParseField END = new ParseField("end"); + public static final ParseField ADVANCE_TIME = new ParseField("advance_time"); + public static final ParseField SKIP_TIME = new ParseField("skip_time"); + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("flush_job_request", (a) -> new FlushJobRequest((String) a[0])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), Job.ID); + PARSER.declareBoolean(FlushJobRequest::setCalcInterim, CALC_INTERIM); + PARSER.declareString(FlushJobRequest::setStart, START); + PARSER.declareString(FlushJobRequest::setEnd, END); + PARSER.declareString(FlushJobRequest::setAdvanceTime, ADVANCE_TIME); + PARSER.declareString(FlushJobRequest::setSkipTime, SKIP_TIME); + } + + private final String jobId; + private Boolean calcInterim; + private String start; + private String end; + private String advanceTime; + private String skipTime; + + /** + * Create new Flush job request + * + * @param jobId The job ID of the job to flush + */ + public FlushJobRequest(String jobId) { + this.jobId = jobId; + } + + public String getJobId() { + return jobId; + } + + public boolean getCalcInterim() { + return calcInterim; + } + + /** + * When {@code true} calculates the interim results for the most recent bucket or all buckets within the latency period. + * + * @param calcInterim defaults to {@code false}. + */ + public void setCalcInterim(boolean calcInterim) { + this.calcInterim = calcInterim; + } + + public String getStart() { + return start; + } + + /** + * When used in conjunction with {@link FlushJobRequest#calcInterim}, + * specifies the start of the range of buckets on which to calculate interim results. + * + * @param start the beginning of the range of buckets; may be an epoch seconds, epoch millis or an ISO string + */ + public void setStart(String start) { + this.start = start; + } + + public String getEnd() { + return end; + } + + /** + * When used in conjunction with {@link FlushJobRequest#calcInterim}, specifies the end of the range + * of buckets on which to calculate interim results + * + * @param end the end of the range of buckets; may be an epoch seconds, epoch millis or an ISO string + */ + public void setEnd(String end) { + this.end = end; + } + + public String getAdvanceTime() { + return advanceTime; + } + + /** + * Specifies to advance to a particular time value. + * Results are generated and the model is updated for data from the specified time interval. + * + * @param advanceTime String representation of a timestamp; may be an epoch seconds, epoch millis or an ISO string + */ + public void setAdvanceTime(String advanceTime) { + this.advanceTime = advanceTime; + } + + public String getSkipTime() { + return skipTime; + } + + /** + * Specifies to skip to a particular time value. + * Results are not generated and the model is not updated for data from the specified time interval. + * + * @param skipTime String representation of a timestamp; may be an epoch seconds, epoch millis or an ISO string + */ + public void setSkipTime(String skipTime) { + this.skipTime = skipTime; + } + + @Override + public int hashCode() { + return Objects.hash(jobId, calcInterim, start, end, advanceTime, skipTime); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + FlushJobRequest other = (FlushJobRequest) obj; + return Objects.equals(jobId, other.jobId) && + calcInterim == other.calcInterim && + Objects.equals(start, other.start) && + Objects.equals(end, other.end) && + Objects.equals(advanceTime, other.advanceTime) && + Objects.equals(skipTime, other.skipTime); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(Job.ID.getPreferredName(), jobId); + if (calcInterim != null) { + builder.field(CALC_INTERIM.getPreferredName(), calcInterim); + } + if (start != null) { + builder.field(START.getPreferredName(), start); + } + if (end != null) { + builder.field(END.getPreferredName(), end); + } + if (advanceTime != null) { + builder.field(ADVANCE_TIME.getPreferredName(), advanceTime); + } + if (skipTime != null) { + builder.field(SKIP_TIME.getPreferredName(), skipTime); + } + builder.endObject(); + return builder; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobResponse.java new file mode 100644 index 000000000000..048b07b504ae --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/FlushJobResponse.java @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Date; +import java.util.Objects; + +/** + * Response object containing flush acknowledgement and additional data + */ +public class FlushJobResponse extends ActionResponse implements ToXContentObject { + + public static final ParseField FLUSHED = new ParseField("flushed"); + public static final ParseField LAST_FINALIZED_BUCKET_END = new ParseField("last_finalized_bucket_end"); + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("flush_job_response", + true, + (a) -> { + boolean flushed = (boolean) a[0]; + Date date = a[1] == null ? null : new Date((long) a[1]); + return new FlushJobResponse(flushed, date); + }); + + static { + PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), FLUSHED); + PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), LAST_FINALIZED_BUCKET_END); + } + + public static FlushJobResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + private final boolean flushed; + private final Date lastFinalizedBucketEnd; + + public FlushJobResponse(boolean flushed, @Nullable Date lastFinalizedBucketEnd) { + this.flushed = flushed; + this.lastFinalizedBucketEnd = lastFinalizedBucketEnd; + } + + /** + * Was the job successfully flushed or not + */ + public boolean isFlushed() { + return flushed; + } + + /** + * Provides the timestamp (in milliseconds-since-the-epoch) of the end of the last bucket that was processed. + */ + @Nullable + public Date getLastFinalizedBucketEnd() { + return lastFinalizedBucketEnd; + } + + @Override + public int hashCode() { + return Objects.hash(flushed, lastFinalizedBucketEnd); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + FlushJobResponse that = (FlushJobResponse) other; + return that.flushed == flushed && Objects.equals(lastFinalizedBucketEnd, that.lastFinalizedBucketEnd); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FLUSHED.getPreferredName(), flushed); + if (lastFinalizedBucketEnd != null) { + builder.timeField(LAST_FINALIZED_BUCKET_END.getPreferredName(), + LAST_FINALIZED_BUCKET_END.getPreferredName() + "_string", lastFinalizedBucketEnd.getTime()); + } + 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 4950a1c8139f..d84099d9a3c4 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 @@ -36,6 +36,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.GetJobStatsRequest; import org.elasticsearch.test.ESTestCase; @@ -140,6 +141,27 @@ public void testGetBuckets() throws IOException { } } + public void testFlushJob() throws Exception { + String jobId = randomAlphaOfLength(10); + FlushJobRequest flushJobRequest = new FlushJobRequest(jobId); + + Request request = MLRequestConverters.flushJob(flushJobRequest); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/anomaly_detectors/" + jobId + "/_flush", request.getEndpoint()); + assertEquals("{\"job_id\":\"" + jobId + "\"}", requestEntityToString(request)); + + flushJobRequest.setSkipTime("1000"); + flushJobRequest.setStart("105"); + flushJobRequest.setEnd("200"); + flushJobRequest.setAdvanceTime("100"); + flushJobRequest.setCalcInterim(true); + request = MLRequestConverters.flushJob(flushJobRequest); + assertEquals( + "{\"job_id\":\"" + jobId + "\",\"calc_interim\":true,\"start\":\"105\"," + + "\"end\":\"200\",\"advance_time\":\"100\",\"skip_time\":\"1000\"}", + requestEntityToString(request)); + } + public void testGetJobStats() { GetJobStatsRequest getJobStatsRequestRequest = new GetJobStatsRequest(); 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 7b8e4b3e4c4c..cd4b6ffc7691 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 @@ -39,6 +39,9 @@ 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.common.unit.TimeValue; +import org.elasticsearch.client.ml.FlushJobRequest; +import org.elasticsearch.client.ml.FlushJobResponse; import org.junit.After; import java.io.IOException; @@ -144,6 +147,19 @@ public void testCloseJob() throws Exception { assertTrue(response.isClosed()); } + public void testFlushJob() throws Exception { + String jobId = randomValidJobId(); + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + machineLearningClient.openJob(new OpenJobRequest(jobId), RequestOptions.DEFAULT); + + FlushJobResponse response = execute(new FlushJobRequest(jobId), + machineLearningClient::flushJob, + machineLearningClient::flushJobAsync); + assertTrue(response.isFlushed()); + } + public void testGetJobStats() throws Exception { String jobId1 = "ml-get-job-stats-test-id-1"; String jobId2 = "ml-get-job-stats-test-id-2"; 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 d97db0a311f7..f92f01f6bad1 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 @@ -52,6 +52,8 @@ import org.elasticsearch.client.ml.job.util.PageParams; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.client.ml.FlushJobRequest; +import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.job.stats.JobStats; import org.junit.After; @@ -461,6 +463,69 @@ public void onFailure(Exception e) { } } + public void testFlushJob() throws Exception { + RestHighLevelClient client = highLevelClient(); + + Job job = MachineLearningIT.buildJob("flushing-my-first-machine-learning-job"); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + client.machineLearning().openJob(new OpenJobRequest(job.getId()), RequestOptions.DEFAULT); + + Job secondJob = MachineLearningIT.buildJob("flushing-my-second-machine-learning-job"); + client.machineLearning().putJob(new PutJobRequest(secondJob), RequestOptions.DEFAULT); + client.machineLearning().openJob(new OpenJobRequest(secondJob.getId()), RequestOptions.DEFAULT); + + { + //tag::x-pack-ml-flush-job-request + FlushJobRequest flushJobRequest = new FlushJobRequest("flushing-my-first-machine-learning-job"); //<1> + //end::x-pack-ml-flush-job-request + + //tag::x-pack-ml-flush-job-request-options + flushJobRequest.setCalcInterim(true); //<1> + flushJobRequest.setAdvanceTime("2018-08-31T16:35:07+00:00"); //<2> + flushJobRequest.setStart("2018-08-31T16:35:17+00:00"); //<3> + flushJobRequest.setEnd("2018-08-31T16:35:27+00:00"); //<4> + flushJobRequest.setSkipTime("2018-08-31T16:35:00+00:00"); //<5> + //end::x-pack-ml-flush-job-request-options + + //tag::x-pack-ml-flush-job-execute + FlushJobResponse flushJobResponse = client.machineLearning().flushJob(flushJobRequest, RequestOptions.DEFAULT); + //end::x-pack-ml-flush-job-execute + + //tag::x-pack-ml-flush-job-response + boolean isFlushed = flushJobResponse.isFlushed(); //<1> + Date lastFinalizedBucketEnd = flushJobResponse.getLastFinalizedBucketEnd(); //<2> + //end::x-pack-ml-flush-job-response + + } + { + //tag::x-pack-ml-flush-job-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(FlushJobResponse FlushJobResponse) { + //<1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::x-pack-ml-flush-job-listener + FlushJobRequest flushJobRequest = new FlushJobRequest("flushing-my-second-machine-learning-job"); + + // 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-flush-job-execute-async + client.machineLearning().flushJobAsync(flushJobRequest, RequestOptions.DEFAULT, listener); //<1> + // end::x-pack-ml-flush-job-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/FlushJobRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/FlushJobRequestTests.java new file mode 100644 index 000000000000..c2bddd436ccd --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/FlushJobRequestTests.java @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.client.ml; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class FlushJobRequestTests extends AbstractXContentTestCase { + + @Override + protected FlushJobRequest createTestInstance() { + FlushJobRequest request = new FlushJobRequest(randomAlphaOfLengthBetween(1, 20)); + + if (randomBoolean()) { + request.setCalcInterim(randomBoolean()); + } + if (randomBoolean()) { + request.setAdvanceTime(String.valueOf(randomLong())); + } + if (randomBoolean()) { + request.setStart(String.valueOf(randomLong())); + } + if (randomBoolean()) { + request.setEnd(String.valueOf(randomLong())); + } + if (randomBoolean()) { + request.setSkipTime(String.valueOf(randomLong())); + } + return request; + } + + @Override + protected FlushJobRequest doParseInstance(XContentParser parser) throws IOException { + return FlushJobRequest.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/FlushJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/FlushJobResponseTests.java new file mode 100644 index 000000000000..bc968ff4564a --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/FlushJobResponseTests.java @@ -0,0 +1,44 @@ +/* + * 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.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.Date; + +public class FlushJobResponseTests extends AbstractXContentTestCase { + + @Override + protected FlushJobResponse createTestInstance() { + return new FlushJobResponse(randomBoolean(), + randomBoolean() ? null : new Date(randomNonNegativeLong())); + } + + @Override + protected FlushJobResponse doParseInstance(XContentParser parser) throws IOException { + return FlushJobResponse.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/docs/java-rest/high-level/ml/flush-job.asciidoc b/docs/java-rest/high-level/ml/flush-job.asciidoc new file mode 100644 index 000000000000..1f815bba0d56 --- /dev/null +++ b/docs/java-rest/high-level/ml/flush-job.asciidoc @@ -0,0 +1,83 @@ +[[java-rest-high-x-pack-ml-flush-job]] +=== Flush Job API + +The Flush Job API provides the ability to flush a {ml} job's +datafeed in the cluster. +It accepts a `FlushJobRequest` object and responds +with a `FlushJobResponse` object. + +[[java-rest-high-x-pack-ml-flush-job-request]] +==== Flush Job Request + +A `FlushJobRequest` 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-flush-job-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-flush-job-request-options] +-------------------------------------------------- +<1> Set request to calculate the interim results +<2> Set the advanced time to flush to the particular time value +<3> Set the start time for the range of buckets on which +to calculate the interim results (requires `calc_interim` to be `true`) +<4> Set the end time for the range of buckets on which +to calculate interim results (requires `calc_interim` to be `true`) +<5> Set the skip time to skip a particular time value + +[[java-rest-high-x-pack-ml-flush-job-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-flush-job-execute] +-------------------------------------------------- + +[[java-rest-high-x-pack-ml-flush-job-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-flush-job-execute-async] +-------------------------------------------------- +<1> The `FlushJobRequest` 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 `FlushJobResponse` may +look like + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-flush-job-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-flush-job-response]] +==== Flush Job Response + +A `FlushJobResponse` contains an acknowledgement and an optional end date for the +last finalized bucket + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-flush-job-response] +-------------------------------------------------- +<1> `isFlushed()` indicates if the job was successfully flushed or not. +<2> `getLastFinalizedBucketEnd()` provides the timestamp +(in milliseconds-since-the-epoch) of the end of the last bucket that was processed. \ 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 76fbc223a1b2..68320fbfe9ff 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -211,6 +211,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -220,6 +221,7 @@ include::ml/get-job.asciidoc[] include::ml/delete-job.asciidoc[] include::ml/open-job.asciidoc[] include::ml/close-job.asciidoc[] +include::ml/flush-job.asciidoc[] include::ml/get-job-stats.asciidoc[] include::ml/get-buckets.asciidoc[] include::ml/get-records.asciidoc[] From c6b011f8ea6dde55f361df0e0f52a1aca0f517bf Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Sun, 2 Sep 2018 09:28:47 -0400 Subject: [PATCH 282/283] TEST: Increase timeout testFollowIndexAndCloseNode (#33333) This test fails several times due to timeout when asserting the number of docs on the following and leading indices. This change reduces the number of docs to index and increases the timeout. --- .../xpack/ccr/ShardChangesIT.java | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) 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 ea258ca6b681..708287c16f65 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 @@ -47,7 +47,6 @@ import org.elasticsearch.xpack.core.XPackSettings; import java.io.IOException; -import java.io.UncheckedIOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -338,26 +337,12 @@ public void testFollowIndexAndCloseNode() throws Exception { ShardFollowNodeTask.DEFAULT_MAX_WRITE_BUFFER_SIZE, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); client().execute(FollowIndexAction.INSTANCE, followRequest).get(); - long maxNumDocsReplicated = Math.min(3000, randomLongBetween(followRequest.getMaxBatchOperationCount(), + long maxNumDocsReplicated = Math.min(1000, randomLongBetween(followRequest.getMaxBatchOperationCount(), followRequest.getMaxBatchOperationCount() * 10)); long minNumDocsReplicated = maxNumDocsReplicated / 3L; logger.info("waiting for at least [{}] documents to be indexed and then stop a random data node", minNumDocsReplicated); - awaitBusy(() -> { - SearchRequest request = new SearchRequest("index2"); - request.source(new SearchSourceBuilder().size(0)); - SearchResponse response = client().search(request).actionGet(); - if (response.getHits().getTotalHits() >= minNumDocsReplicated) { - try { - internalCluster().stopRandomNonMasterNode(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return true; - } else { - return false; - } - }, 30, TimeUnit.SECONDS); - + atLeastDocsIndexed("index2", minNumDocsReplicated); + internalCluster().stopRandomNonMasterNode(); logger.info("waiting for at least [{}] documents to be indexed", maxNumDocsReplicated); atLeastDocsIndexed("index2", maxNumDocsReplicated); run.set(false); @@ -548,7 +533,7 @@ private void unfollowIndex(String index) throws Exception { } } assertThat(numNodeTasks, equalTo(0)); - }); + }, 30, TimeUnit.SECONDS); } private CheckedRunnable assertExpectedDocumentRunnable(final int value) { @@ -660,7 +645,7 @@ private void atLeastDocsIndexed(String index, long numDocsReplicated) throws Int request.source(new SearchSourceBuilder().size(0)); SearchResponse response = client().search(request).actionGet(); return response.getHits().getTotalHits() >= numDocsReplicated; - }, 30, TimeUnit.SECONDS); + }, 60, TimeUnit.SECONDS); } private void assertSameDocCount(String index1, String index2) throws Exception { @@ -674,7 +659,7 @@ private void assertSameDocCount(String index1, String index2) throws Exception { request2.source(new SearchSourceBuilder().size(0)); SearchResponse response2 = client().search(request2).actionGet(); assertThat(response2.getHits().getTotalHits(), equalTo(response1.getHits().getTotalHits())); - }); + }, 60, TimeUnit.SECONDS); } public static FollowIndexAction.Request createFollowRequest(String leaderIndex, String followIndex) { From 389bf67275fea488b482149093c2efaab8239a4a Mon Sep 17 00:00:00 2001 From: Sohaib Iftikhar Date: Sun, 2 Sep 2018 21:15:00 +0200 Subject: [PATCH 283/283] HLREST: add update by query API (#32760) Adds update by query to the high level rest client. --- .../client/RequestConverters.java | 29 +++ .../client/RestHighLevelClient.java | 30 +++ .../java/org/elasticsearch/client/CrudIT.java | 67 +++++++ .../client/RequestConvertersTests.java | 56 ++++++ .../client/RestHighLevelClientTests.java | 3 +- .../documentation/CRUDDocumentationIT.java | 121 ++++++++++++ .../document/update-by-query.asciidoc | 181 ++++++++++++++++++ .../high-level/supported-apis.asciidoc | 2 + .../reindex/RestUpdateByQueryAction.java | 3 +- .../index/reindex/RoundTripTests.java | 2 +- .../reindex/UpdateByQueryMetadataTests.java | 3 +- .../reindex/UpdateByQueryWithScriptTests.java | 3 +- .../reindex/AbstractBulkByScrollRequest.java | 4 +- .../index/reindex/BulkByScrollTask.java | 79 +++----- .../index/reindex/UpdateByQueryRequest.java | 100 +++++++++- .../reindex/BulkByScrollResponseTests.java | 37 +++- ...ulkByScrollTaskStatusOrExceptionTests.java | 9 +- .../reindex/BulkByScrollTaskStatusTests.java | 34 +++- .../reindex/UpdateByQueryRequestTests.java | 9 +- 19 files changed, 691 insertions(+), 81 deletions(-) create mode 100644 docs/java-rest/high-level/document/update-by-query.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index c45a8e1005e4..5e74262fa20e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -107,7 +107,9 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.rankeval.RankEvalRequest; +import org.elasticsearch.index.reindex.AbstractBulkByScrollRequest; import org.elasticsearch.index.reindex.ReindexRequest; +import org.elasticsearch.index.reindex.UpdateByQueryRequest; import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.XPackUsageRequest; import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest; @@ -837,6 +839,33 @@ static Request reindex(ReindexRequest reindexRequest) throws IOException { return request; } + static Request updateByQuery(UpdateByQueryRequest updateByQueryRequest) throws IOException { + String endpoint = + endpoint(updateByQueryRequest.indices(), updateByQueryRequest.getDocTypes(), "_update_by_query"); + Request request = new Request(HttpPost.METHOD_NAME, endpoint); + Params params = new Params(request) + .withRouting(updateByQueryRequest.getRouting()) + .withPipeline(updateByQueryRequest.getPipeline()) + .withRefresh(updateByQueryRequest.isRefresh()) + .withTimeout(updateByQueryRequest.getTimeout()) + .withWaitForActiveShards(updateByQueryRequest.getWaitForActiveShards()) + .withIndicesOptions(updateByQueryRequest.indicesOptions()); + if (updateByQueryRequest.isAbortOnVersionConflict() == false) { + params.putParam("conflicts", "proceed"); + } + if (updateByQueryRequest.getBatchSize() != AbstractBulkByScrollRequest.DEFAULT_SCROLL_SIZE) { + params.putParam("scroll_size", Integer.toString(updateByQueryRequest.getBatchSize())); + } + if (updateByQueryRequest.getScrollTime() != AbstractBulkByScrollRequest.DEFAULT_SCROLL_TIMEOUT) { + params.putParam("scroll", updateByQueryRequest.getScrollTime()); + } + if (updateByQueryRequest.getSize() > 0) { + params.putParam("size", Integer.toString(updateByQueryRequest.getSize())); + } + request.setEntity(createEntity(updateByQueryRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request rollover(RolloverRequest rolloverRequest) throws IOException { String endpoint = new EndpointBuilder().addPathPart(rolloverRequest.getAlias()).addPathPartAsIs("_rollover") .addPathPart(rolloverRequest.getNewIndexName()).build(); diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 4ac5bfd080f1..6e3c5a6fb831 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -66,6 +66,7 @@ import org.elasticsearch.index.rankeval.RankEvalResponse; import org.elasticsearch.index.reindex.BulkByScrollResponse; import org.elasticsearch.index.reindex.ReindexRequest; +import org.elasticsearch.index.reindex.UpdateByQueryRequest; import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestStatus; @@ -424,6 +425,35 @@ public final void reindexAsync(ReindexRequest reindexRequest, RequestOptions opt ); } + /** + * Executes a update by query request. + * See + * Update By Query API on elastic.co + * @param updateByQueryRequest the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public final BulkByScrollResponse updateByQuery(UpdateByQueryRequest updateByQueryRequest, RequestOptions options) throws IOException { + return performRequestAndParseEntity( + updateByQueryRequest, RequestConverters::updateByQuery, options, BulkByScrollResponse::fromXContent, emptySet() + ); + } + + /** + * Asynchronously executes an update by query request. + * See + * Update By Query API on elastic.co + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public final void updateByQueryAsync(UpdateByQueryRequest reindexRequest, RequestOptions options, + ActionListener listener) { + performRequestAsyncAndParseEntity( + reindexRequest, RequestConverters::updateByQuery, options, BulkByScrollResponse::fromXContent, listener, emptySet() + ); + } + /** * Pings the remote Elasticsearch cluster and returns true if the ping succeeded, false otherwise * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java index 7978d76c56d7..e02d9f451ebe 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CrudIT.java @@ -51,6 +51,7 @@ import org.elasticsearch.index.query.IdsQueryBuilder; import org.elasticsearch.index.reindex.BulkByScrollResponse; import org.elasticsearch.index.reindex.ReindexRequest; +import org.elasticsearch.index.reindex.UpdateByQueryRequest; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; @@ -691,6 +692,72 @@ public void testReindex() throws IOException { } } + public void testUpdateByQuery() throws IOException { + final String sourceIndex = "source1"; + { + // Prepare + Settings settings = Settings.builder() + .put("number_of_shards", 1) + .put("number_of_replicas", 0) + .build(); + createIndex(sourceIndex, settings); + assertEquals( + RestStatus.OK, + highLevelClient().bulk( + new BulkRequest() + .add(new IndexRequest(sourceIndex, "type", "1") + .source(Collections.singletonMap("foo", 1), XContentType.JSON)) + .add(new IndexRequest(sourceIndex, "type", "2") + .source(Collections.singletonMap("foo", 2), XContentType.JSON)) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE), + RequestOptions.DEFAULT + ).status() + ); + } + { + // test1: create one doc in dest + UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(); + updateByQueryRequest.indices(sourceIndex); + updateByQueryRequest.setQuery(new IdsQueryBuilder().addIds("1").types("type")); + updateByQueryRequest.setRefresh(true); + BulkByScrollResponse bulkResponse = + execute(updateByQueryRequest, highLevelClient()::updateByQuery, highLevelClient()::updateByQueryAsync); + assertEquals(1, bulkResponse.getTotal()); + assertEquals(1, bulkResponse.getUpdated()); + assertEquals(0, bulkResponse.getNoops()); + assertEquals(0, bulkResponse.getVersionConflicts()); + assertEquals(1, bulkResponse.getBatches()); + assertTrue(bulkResponse.getTook().getMillis() > 0); + assertEquals(1, bulkResponse.getBatches()); + assertEquals(0, bulkResponse.getBulkFailures().size()); + assertEquals(0, bulkResponse.getSearchFailures().size()); + } + { + // test2: update using script + UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(); + updateByQueryRequest.indices(sourceIndex); + updateByQueryRequest.setScript(new Script("if (ctx._source.foo == 2) ctx._source.foo++;")); + updateByQueryRequest.setRefresh(true); + BulkByScrollResponse bulkResponse = + execute(updateByQueryRequest, highLevelClient()::updateByQuery, highLevelClient()::updateByQueryAsync); + assertEquals(2, bulkResponse.getTotal()); + assertEquals(2, bulkResponse.getUpdated()); + assertEquals(0, bulkResponse.getDeleted()); + assertEquals(0, bulkResponse.getNoops()); + assertEquals(0, bulkResponse.getVersionConflicts()); + assertEquals(1, bulkResponse.getBatches()); + assertTrue(bulkResponse.getTook().getMillis() > 0); + assertEquals(1, bulkResponse.getBatches()); + assertEquals(0, bulkResponse.getBulkFailures().size()); + assertEquals(0, bulkResponse.getSearchFailures().size()); + assertEquals( + 3, + (int) (highLevelClient().get(new GetRequest(sourceIndex, "type", "2"), RequestOptions.DEFAULT) + .getSourceAsMap().get("foo")) + ); + } + } + public void testBulkProcessorIntegration() throws IOException { int nbItems = randomIntBetween(10, 100); boolean[] errors = new boolean[nbItems]; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 44b4ae05b57c..92930d14cf4a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -129,6 +129,7 @@ import org.elasticsearch.index.rankeval.RestRankEvalAction; import org.elasticsearch.index.reindex.ReindexRequest; import org.elasticsearch.index.reindex.RemoteInfo; +import org.elasticsearch.index.reindex.UpdateByQueryRequest; import org.elasticsearch.protocol.xpack.XPackInfoRequest; import org.elasticsearch.protocol.xpack.migration.IndexUpgradeInfoRequest; import org.elasticsearch.protocol.xpack.watcher.DeleteWatchRequest; @@ -137,6 +138,7 @@ import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest; import org.elasticsearch.repositories.fs.FsRepository; import org.elasticsearch.rest.action.search.RestSearchAction; +import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.script.mustache.MultiSearchTemplateRequest; import org.elasticsearch.script.mustache.SearchTemplateRequest; @@ -470,6 +472,60 @@ public void testReindex() throws IOException { assertToXContentBody(reindexRequest, request.getEntity()); } + public void testUpdateByQuery() throws IOException { + UpdateByQueryRequest updateByQueryRequest = new UpdateByQueryRequest(); + updateByQueryRequest.indices(randomIndicesNames(1, 5)); + Map expectedParams = new HashMap<>(); + if (randomBoolean()) { + updateByQueryRequest.setDocTypes(generateRandomStringArray(5, 5, false, false)); + } + if (randomBoolean()) { + int batchSize = randomInt(100); + updateByQueryRequest.setBatchSize(batchSize); + expectedParams.put("scroll_size", Integer.toString(batchSize)); + } + if (randomBoolean()) { + updateByQueryRequest.setPipeline("my_pipeline"); + expectedParams.put("pipeline", "my_pipeline"); + } + if (randomBoolean()) { + updateByQueryRequest.setRouting("=cat"); + expectedParams.put("routing", "=cat"); + } + if (randomBoolean()) { + int size = randomIntBetween(100, 1000); + updateByQueryRequest.setSize(size); + expectedParams.put("size", Integer.toString(size)); + } + if (randomBoolean()) { + updateByQueryRequest.setAbortOnVersionConflict(false); + expectedParams.put("conflicts", "proceed"); + } + if (randomBoolean()) { + String ts = randomTimeValue(); + updateByQueryRequest.setScroll(TimeValue.parseTimeValue(ts, "scroll")); + expectedParams.put("scroll", ts); + } + if (randomBoolean()) { + updateByQueryRequest.setQuery(new TermQueryBuilder("foo", "fooval")); + } + if (randomBoolean()) { + updateByQueryRequest.setScript(new Script("ctx._source.last = \"lastname\"")); + } + setRandomIndicesOptions(updateByQueryRequest::setIndicesOptions, updateByQueryRequest::indicesOptions, expectedParams); + setRandomTimeout(updateByQueryRequest::setTimeout, ReplicationRequest.DEFAULT_TIMEOUT, expectedParams); + Request request = RequestConverters.updateByQuery(updateByQueryRequest); + StringJoiner joiner = new StringJoiner("/", "/", ""); + joiner.add(String.join(",", updateByQueryRequest.indices())); + if (updateByQueryRequest.getDocTypes().length > 0) + joiner.add(String.join(",", updateByQueryRequest.getDocTypes())); + joiner.add("_update_by_query"); + assertEquals(joiner.toString(), request.getEndpoint()); + assertEquals(HttpPost.METHOD_NAME, request.getMethod()); + assertEquals(expectedParams, request.getParameters()); + assertToXContentBody(updateByQueryRequest, request.getEntity()); + } + public void testPutMapping() throws IOException { PutMappingRequest putMappingRequest = new PutMappingRequest(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index d2585b6f3f4c..7df4e0725768 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -664,8 +664,7 @@ public void testApiNamingConventions() throws Exception { "render_search_template", "scripts_painless_execute", "tasks.get", - "termvectors", - "update_by_query" + "termvectors" }; //These API are not required for high-level client feature completeness String[] notRequiredApi = new String[] { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java index 9c69a2a48361..ac9d42c65ca5 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CRUDDocumentationIT.java @@ -39,6 +39,7 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.ActiveShardCount; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.action.support.replication.ReplicationResponse; @@ -67,6 +68,7 @@ import org.elasticsearch.index.reindex.ReindexRequest; import org.elasticsearch.index.reindex.RemoteInfo; import org.elasticsearch.index.reindex.ScrollableHitSource; +import org.elasticsearch.index.reindex.UpdateByQueryRequest; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; @@ -899,6 +901,125 @@ public void onFailure(Exception e) { } } + public void testUpdateByQuery() throws Exception { + RestHighLevelClient client = highLevelClient(); + { + String mapping = + "\"doc\": {\n" + + " \"properties\": {\n" + + " \"user\": {\n" + + " \"type\": \"text\"\n" + + " },\n" + + " \"field1\": {\n" + + " \"type\": \"integer\"\n" + + " },\n" + + " \"field2\": {\n" + + " \"type\": \"integer\"\n" + + " }\n" + + " }\n" + + " }"; + createIndex("source1", Settings.EMPTY, mapping); + createIndex("source2", Settings.EMPTY, mapping); + createPipeline("my_pipeline"); + } + { + // tag::update-by-query-request + UpdateByQueryRequest request = new UpdateByQueryRequest("source1", "source2"); // <1> + // end::update-by-query-request + // tag::update-by-query-request-conflicts + request.setConflicts("proceed"); // <1> + // end::update-by-query-request-conflicts + // tag::update-by-query-request-typeOrQuery + request.setDocTypes("doc"); // <1> + request.setQuery(new TermQueryBuilder("user", "kimchy")); // <2> + // end::update-by-query-request-typeOrQuery + // tag::update-by-query-request-size + request.setSize(10); // <1> + // end::update-by-query-request-size + // tag::update-by-query-request-scrollSize + request.setBatchSize(100); // <1> + // end::update-by-query-request-scrollSize + // tag::update-by-query-request-pipeline + request.setPipeline("my_pipeline"); // <1> + // end::update-by-query-request-pipeline + // tag::update-by-query-request-script + request.setScript( + new Script( + ScriptType.INLINE, "painless", + "if (ctx._source.user == 'kimchy') {ctx._source.likes++;}", + Collections.emptyMap())); // <1> + // end::update-by-query-request-script + // tag::update-by-query-request-timeout + request.setTimeout(TimeValue.timeValueMinutes(2)); // <1> + // end::update-by-query-request-timeout + // tag::update-by-query-request-refresh + request.setRefresh(true); // <1> + // end::update-by-query-request-refresh + // tag::update-by-query-request-slices + request.setSlices(2); // <1> + // end::update-by-query-request-slices + // tag::update-by-query-request-scroll + request.setScroll(TimeValue.timeValueMinutes(10)); // <1> + // end::update-by-query-request-scroll + // tag::update-by-query-request-routing + request.setRouting("=cat"); // <1> + // end::update-by-query-request-routing + // tag::update-by-query-request-indicesOptions + request.setIndicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN); // <1> + // end::update-by-query-request-indicesOptions + + // tag::update-by-query-execute + BulkByScrollResponse bulkResponse = client.updateByQuery(request, RequestOptions.DEFAULT); + // end::update-by-query-execute + assertSame(0, bulkResponse.getSearchFailures().size()); + assertSame(0, bulkResponse.getBulkFailures().size()); + // tag::update-by-query-response + TimeValue timeTaken = bulkResponse.getTook(); // <1> + boolean timedOut = bulkResponse.isTimedOut(); // <2> + long totalDocs = bulkResponse.getTotal(); // <3> + long updatedDocs = bulkResponse.getUpdated(); // <4> + long deletedDocs = bulkResponse.getDeleted(); // <5> + long batches = bulkResponse.getBatches(); // <6> + long noops = bulkResponse.getNoops(); // <7> + long versionConflicts = bulkResponse.getVersionConflicts(); // <8> + long bulkRetries = bulkResponse.getBulkRetries(); // <9> + long searchRetries = bulkResponse.getSearchRetries(); // <10> + TimeValue throttledMillis = bulkResponse.getStatus().getThrottled(); // <11> + TimeValue throttledUntilMillis = bulkResponse.getStatus().getThrottledUntil(); // <12> + List searchFailures = bulkResponse.getSearchFailures(); // <13> + List bulkFailures = bulkResponse.getBulkFailures(); // <14> + // end::update-by-query-response + } + { + UpdateByQueryRequest request = new UpdateByQueryRequest(); + request.indices("source1"); + + // tag::update-by-query-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(BulkByScrollResponse bulkResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::update-by-query-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::update-by-query-execute-async + client.updateByQueryAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::update-by-query-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGet() throws Exception { RestHighLevelClient client = highLevelClient(); { diff --git a/docs/java-rest/high-level/document/update-by-query.asciidoc b/docs/java-rest/high-level/document/update-by-query.asciidoc new file mode 100644 index 000000000000..324385a442b5 --- /dev/null +++ b/docs/java-rest/high-level/document/update-by-query.asciidoc @@ -0,0 +1,181 @@ +[[java-rest-high-document-update-by-query]] +=== Update By Query API + +[[java-rest-high-document-update-by-query-request]] +==== Update By Query Request + +A `UpdateByQueryRequest` can be used to update documents in an index. + +It requires an existing index (or a set of indices) on which the update is to be performed. + +The simplest form of a `UpdateByQueryRequest` looks like follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request] +-------------------------------------------------- +<1> Creates the `UpdateByQueryRequest` on a set of indices. + +By default version conflicts abort the `UpdateByQueryRequest` process but you can just count them by settings it to +`proceed` in the request body + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-conflicts] +-------------------------------------------------- +<1> Set `proceed` on version conflict + +You can limit the documents by adding a type to the source or by adding a query. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-typeOrQuery] +-------------------------------------------------- +<1> Only copy `doc` type +<2> Only copy documents which have field `user` set to `kimchy` + +It’s also possible to limit the number of processed documents by setting size. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-size] +-------------------------------------------------- +<1> Only copy 10 documents + +By default `UpdateByQueryRequest` uses batches of 1000. You can change the batch size with `setBatchSize`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-scrollSize] +-------------------------------------------------- +<1> Use batches of 100 documents + +Update by query can also use the ingest feature by specifying a `pipeline`. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-pipeline] +-------------------------------------------------- +<1> set pipeline to `my_pipeline` + +`UpdateByQueryRequest` also supports a `script` that modifies the document. The following example illustrates that. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-script] +-------------------------------------------------- +<1> `setScript` to increment the `likes` field on all documents with user `kimchy`. + +`UpdateByQueryRequest` also helps in automatically parallelizing using `sliced-scroll` to +slice on `_uid`. Use `setSlices` to specify the number of slices to use. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-slices] +-------------------------------------------------- +<1> set number of slices to use + +`UpdateByQueryRequest` uses the `scroll` parameter to control how long it keeps the "search context" alive. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-scroll] +-------------------------------------------------- +<1> set scroll time + +If you provide routing then the routing is copied to the scroll query, limiting the process to the shards that match +that routing value. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-routing] +-------------------------------------------------- +<1> set routing + + +==== Optional arguments +In addition to the options above the following arguments can optionally be also provided: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-timeout] +-------------------------------------------------- +<1> Timeout to wait for the update by query request to be performed as a `TimeValue` + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-refresh] +-------------------------------------------------- +<1> Refresh index after calling update by query + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-request-indicesOptions] +-------------------------------------------------- +<1> Set indices options + + +[[java-rest-high-document-update-by-query-sync]] +==== Synchronous Execution + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-execute] +-------------------------------------------------- + +[[java-rest-high-document-update-by-query-async]] +==== Asynchronous Execution + +The asynchronous execution of an update by query request requires both the `UpdateByQueryRequest` +instance and an `ActionListener` instance to be passed to the asynchronous +method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-execute-async] +-------------------------------------------------- +<1> The `UpdateByQueryRequest` 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 `BulkByScrollResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. The response is +provided as an argument and contains a list of individual results for each +operation that was executed. Note that one or more operations might have +failed while the others have been successfully executed. +<2> Called when the whole `UpdateByQueryRequest` fails. In this case the raised +exception is provided as an argument and no operation has been executed. + +[[java-rest-high-document-update-by-query-execute-listener-response]] +==== Update By Query Response + +The returned `BulkByScrollResponse` contains information about the executed operations and + allows to iterate over each result as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/CRUDDocumentationIT.java[update-by-query-response] +-------------------------------------------------- +<1> Get total time taken +<2> Check if the request timed out +<3> Get total number of docs processed +<4> Number of docs that were updated +<5> Number of docs that were deleted +<6> Number of batches that were executed +<7> Number of skipped docs +<8> Number of version conflicts +<9> Number of times request had to retry bulk index operations +<10> Number of times request had to retry search operations +<11> The total time this request has throttled itself not including the current throttle time if it is currently sleeping +<12> Remaining delay of any current throttle sleep or 0 if not sleeping +<13> Failures during search phase +<14> Failures during bulk index operation diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 68320fbfe9ff..b791dbc0f8cf 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -16,6 +16,7 @@ Multi-document APIs:: * <> * <> * <> +* <> include::document/index.asciidoc[] include::document/get.asciidoc[] @@ -25,6 +26,7 @@ include::document/update.asciidoc[] include::document/bulk.asciidoc[] include::document/multi-get.asciidoc[] include::document/reindex.asciidoc[] +include::document/update-by-query.asciidoc[] == Search APIs diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestUpdateByQueryAction.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestUpdateByQueryAction.java index bf0adc6e1429..72a2a0d73356 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestUpdateByQueryAction.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RestUpdateByQueryAction.java @@ -20,7 +20,6 @@ package org.elasticsearch.index.reindex; import org.elasticsearch.ElasticsearchParseException; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; @@ -63,7 +62,7 @@ protected UpdateByQueryRequest buildRequest(RestRequest request) throws IOExcept * it to set its own defaults which differ from SearchRequest's * defaults. Then the parse can override them. */ - UpdateByQueryRequest internal = new UpdateByQueryRequest(new SearchRequest()); + UpdateByQueryRequest internal = new UpdateByQueryRequest(); Map> consumers = new HashMap<>(); consumers.put("conflicts", o -> internal.setConflicts((String) o)); diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java index cc848900b781..30621ab607bf 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java @@ -81,7 +81,7 @@ public void testReindexRequest() throws IOException { } public void testUpdateByQueryRequest() throws IOException { - UpdateByQueryRequest update = new UpdateByQueryRequest(new SearchRequest()); + UpdateByQueryRequest update = new UpdateByQueryRequest(); randomRequest(update); if (randomBoolean()) { update.setPipeline(randomAlphaOfLength(5)); diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryMetadataTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryMetadataTests.java index b688ce019e3d..d3f62af907d9 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryMetadataTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryMetadataTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.index.reindex.ScrollableHitSource.Hit; import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.settings.Settings; public class UpdateByQueryMetadataTests @@ -39,7 +38,7 @@ protected TestAction action() { @Override protected UpdateByQueryRequest request() { - return new UpdateByQueryRequest(new SearchRequest()); + return new UpdateByQueryRequest(); } private class TestAction extends TransportUpdateByQueryAction.AsyncIndexBySearchAction { diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryWithScriptTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryWithScriptTests.java index 4006d16fbcb1..8c9744aa0dd9 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryWithScriptTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryWithScriptTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.reindex; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.script.ScriptService; @@ -50,7 +49,7 @@ public void testModifyingCtxNotAllowed() { @Override protected UpdateByQueryRequest request() { - return new UpdateByQueryRequest(new SearchRequest()); + return new UpdateByQueryRequest(); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java b/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java index 3b635c823878..b2e6f98f1268 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/AbstractBulkByScrollRequest.java @@ -44,8 +44,8 @@ public abstract class AbstractBulkByScrollRequest> extends ActionRequest { public static final int SIZE_ALL_MATCHES = -1; - static final TimeValue DEFAULT_SCROLL_TIMEOUT = timeValueMinutes(5); - static final int DEFAULT_SCROLL_SIZE = 1000; + public static final TimeValue DEFAULT_SCROLL_TIMEOUT = timeValueMinutes(5); + public static final int DEFAULT_SCROLL_SIZE = 1000; public static final int AUTO_SLICES = 0; public static final String AUTO_SLICES_VALUE = "auto"; diff --git a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java index 5beb86fae6ba..7aa2c8a1b75a 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/BulkByScrollTask.java @@ -209,8 +209,8 @@ public boolean shouldCancelChildrenOnCancellation() { public static class StatusBuilder { private Integer sliceId = null; private Long total = null; - private Long updated = null; - private Long created = null; + private long updated = 0; // Not present during deleteByQuery + private long created = 0; // Not present during updateByQuery private Long deleted = null; private Integer batches = null; private Long versionConflicts = null; @@ -221,7 +221,7 @@ public static class StatusBuilder { private Float requestsPerSecond = null; private String reasonCancelled = null; private TimeValue throttledUntil = null; - private List sliceStatuses = emptyList(); + private List sliceStatuses = new ArrayList<>(); public void setSliceId(Integer sliceId) { this.sliceId = sliceId; @@ -295,10 +295,14 @@ public void setThrottledUntil(Long throttledUntil) { public void setSliceStatuses(List sliceStatuses) { if (sliceStatuses != null) { - this.sliceStatuses = sliceStatuses; + this.sliceStatuses.addAll(sliceStatuses); } } + public void addToSliceStatuses(StatusOrException statusOrException) { + this.sliceStatuses.add(statusOrException); + } + public Status buildStatus() { if (sliceStatuses.isEmpty()) { try { @@ -613,37 +617,20 @@ public static Status innerFromXContent(XContentParser parser) throws IOException Token token = parser.currentToken(); String fieldName = parser.currentName(); ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation); - Integer sliceId = null; - Long total = null; - Long updated = null; - Long created = null; - Long deleted = null; - Integer batches = null; - Long versionConflicts = null; - Long noOps = null; - Long bulkRetries = null; - Long searchRetries = null; - TimeValue throttled = null; - Float requestsPerSecond = null; - String reasonCancelled = null; - TimeValue throttledUntil = null; - List sliceStatuses = new ArrayList<>(); + StatusBuilder builder = new StatusBuilder(); while ((token = parser.nextToken()) != Token.END_OBJECT) { if (token == Token.FIELD_NAME) { fieldName = parser.currentName(); } else if (token == Token.START_OBJECT) { if (fieldName.equals(Status.RETRIES_FIELD)) { - Tuple retries = - Status.RETRIES_PARSER.parse(parser, null); - bulkRetries = retries.v1(); - searchRetries = retries.v2(); + builder.setRetries(Status.RETRIES_PARSER.parse(parser, null)); } else { parser.skipChildren(); } } else if (token == Token.START_ARRAY) { if (fieldName.equals(Status.SLICES_FIELD)) { while ((token = parser.nextToken()) != Token.END_ARRAY) { - sliceStatuses.add(StatusOrException.fromXContent(parser)); + builder.addToSliceStatuses(StatusOrException.fromXContent(parser)); } } else { parser.skipChildren(); @@ -651,57 +638,47 @@ public static Status innerFromXContent(XContentParser parser) throws IOException } else { // else if it is a value switch (fieldName) { case Status.SLICE_ID_FIELD: - sliceId = parser.intValue(); + builder.setSliceId(parser.intValue()); break; case Status.TOTAL_FIELD: - total = parser.longValue(); + builder.setTotal(parser.longValue()); break; case Status.UPDATED_FIELD: - updated = parser.longValue(); + builder.setUpdated(parser.longValue()); break; case Status.CREATED_FIELD: - created = parser.longValue(); + builder.setCreated(parser.longValue()); break; case Status.DELETED_FIELD: - deleted = parser.longValue(); + builder.setDeleted(parser.longValue()); break; case Status.BATCHES_FIELD: - batches = parser.intValue(); + builder.setBatches(parser.intValue()); break; case Status.VERSION_CONFLICTS_FIELD: - versionConflicts = parser.longValue(); + builder.setVersionConflicts(parser.longValue()); break; case Status.NOOPS_FIELD: - noOps = parser.longValue(); + builder.setNoops(parser.longValue()); break; case Status.THROTTLED_RAW_FIELD: - throttled = new TimeValue(parser.longValue(), TimeUnit.MILLISECONDS); + builder.setThrottled(parser.longValue()); break; case Status.REQUESTS_PER_SEC_FIELD: - requestsPerSecond = parser.floatValue(); - requestsPerSecond = requestsPerSecond == -1 ? Float.POSITIVE_INFINITY : requestsPerSecond; + builder.setRequestsPerSecond(parser.floatValue()); break; case Status.CANCELED_FIELD: - reasonCancelled = parser.text(); + builder.setReasonCancelled(parser.text()); break; case Status.THROTTLED_UNTIL_RAW_FIELD: - throttledUntil = new TimeValue(parser.longValue(), TimeUnit.MILLISECONDS); + builder.setThrottledUntil(parser.longValue()); break; default: break; } } } - if (sliceStatuses.isEmpty()) { - return - new Status( - sliceId, total, updated, created, deleted, batches, versionConflicts, noOps, bulkRetries, - searchRetries, throttled, requestsPerSecond, reasonCancelled, throttledUntil - ); - } else { - return new Status(sliceStatuses, reasonCancelled); - } - + return builder.buildStatus(); } @Override @@ -838,15 +815,15 @@ public int hashCode() { ); } - public boolean equalsWithoutSliceStatus(Object o) { + public boolean equalsWithoutSliceStatus(Object o, boolean includeUpdated, boolean includeCreated) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Status other = (Status) o; return Objects.equals(sliceId, other.sliceId) && total == other.total && - updated == other.updated && - created == other.created && + (!includeUpdated || updated == other.updated) && + (!includeCreated || created == other.created) && deleted == other.deleted && batches == other.batches && versionConflicts == other.versionConflicts && @@ -861,7 +838,7 @@ public boolean equalsWithoutSliceStatus(Object o) { @Override public boolean equals(Object o) { - if (equalsWithoutSliceStatus(o)) { + if (equalsWithoutSliceStatus(o, true, true)) { return Objects.equals(sliceStatuses, ((Status) o).sliceStatuses); } else { return false; diff --git a/server/src/main/java/org/elasticsearch/index/reindex/UpdateByQueryRequest.java b/server/src/main/java/org/elasticsearch/index/reindex/UpdateByQueryRequest.java index eb4fd59a7bc5..71ffadc93032 100644 --- a/server/src/main/java/org/elasticsearch/index/reindex/UpdateByQueryRequest.java +++ b/server/src/main/java/org/elasticsearch/index/reindex/UpdateByQueryRequest.java @@ -24,6 +24,9 @@ import org.elasticsearch.action.support.IndicesOptions; 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 org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.tasks.TaskId; import java.io.IOException; @@ -34,16 +37,22 @@ * representative set of subrequests. This is best-effort but better than {@linkplain ReindexRequest} because scripts can't change the * destination index and things. */ -public class UpdateByQueryRequest extends AbstractBulkIndexByScrollRequest implements IndicesRequest.Replaceable { +public class UpdateByQueryRequest extends AbstractBulkIndexByScrollRequest + implements IndicesRequest.Replaceable, ToXContentObject { /** * Ingest pipeline to set on index requests made by this action. */ private String pipeline; public UpdateByQueryRequest() { + this(new SearchRequest()); } - public UpdateByQueryRequest(SearchRequest search) { + public UpdateByQueryRequest(String... indices) { + this(new SearchRequest(indices)); + } + + UpdateByQueryRequest(SearchRequest search) { this(search, true); } @@ -59,8 +68,81 @@ private UpdateByQueryRequest(SearchRequest search, boolean setDefaults) { /** * Set the ingest pipeline to set on index requests made by this action. */ - public void setPipeline(String pipeline) { + public UpdateByQueryRequest setPipeline(String pipeline) { this.pipeline = pipeline; + return this; + } + + /** + * Set the query for selective update + */ + public UpdateByQueryRequest setQuery(QueryBuilder query) { + if (query != null) { + getSearchRequest().source().query(query); + } + return this; + } + + /** + * Set the document types for the update + */ + public UpdateByQueryRequest setDocTypes(String... types) { + if (types != null) { + getSearchRequest().types(types); + } + return this; + } + + /** + * Set routing limiting the process to the shards that match that routing value + */ + public UpdateByQueryRequest setRouting(String routing) { + if (routing != null) { + getSearchRequest().routing(routing); + } + return this; + } + + /** + * The scroll size to control number of documents processed per batch + */ + public UpdateByQueryRequest setBatchSize(int size) { + getSearchRequest().source().size(size); + return this; + } + + /** + * Set the IndicesOptions for controlling unavailable indices + */ + public UpdateByQueryRequest setIndicesOptions(IndicesOptions indicesOptions) { + getSearchRequest().indicesOptions(indicesOptions); + return this; + } + + /** + * Gets the batch size for this request + */ + public int getBatchSize() { + return getSearchRequest().source().size(); + } + + /** + * Gets the routing value used for this request + */ + public String getRouting() { + return getSearchRequest().routing(); + } + + /** + * Gets the document types on which this request would be executed. Returns an empty array if all + * types are to be processed. + */ + public String[] getDocTypes() { + if (getSearchRequest().types() != null) { + return getSearchRequest().types(); + } else { + return new String[0]; + } } /** @@ -121,4 +203,16 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeOptionalString(pipeline); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (getScript() != null) { + builder.field("script"); + getScript().toXContent(builder, params); + } + getSearchRequest().source().innerToXContent(builder, params); + builder.endObject(); + return builder; + } } diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java index 0dd4d6bc8497..71aab8ca9f9f 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollResponseTests.java @@ -24,11 +24,15 @@ import org.elasticsearch.action.bulk.BulkItemResponse.Failure; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.index.reindex.BulkByScrollTask.Status; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -38,6 +42,9 @@ public class BulkByScrollResponseTests extends AbstractXContentTestCase { + private boolean includeUpdated; + private boolean includeCreated; + public void testRountTrip() throws IOException { BulkByScrollResponse response = new BulkByScrollResponse(timeValueMillis(randomNonNegativeLong()), BulkByScrollTaskStatusTests.randomStatus(), randomIndexingFailures(), randomSearchFailures(), randomBoolean()); @@ -97,10 +104,11 @@ private void assertResponseEquals(BulkByScrollResponse expected, BulkByScrollRes } } - @Override - protected void assertEqualInstances(BulkByScrollResponse expected, BulkByScrollResponse actual) { + public static void assertEqualBulkResponse(BulkByScrollResponse expected, BulkByScrollResponse actual, + boolean includeUpdated, boolean includeCreated) { assertEquals(expected.getTook(), actual.getTook()); - BulkByScrollTaskStatusTests.assertEqualStatus(expected.getStatus(), actual.getStatus()); + BulkByScrollTaskStatusTests + .assertEqualStatus(expected.getStatus(), actual.getStatus(), includeUpdated, includeCreated); assertEquals(expected.getBulkFailures().size(), actual.getBulkFailures().size()); for (int i = 0; i < expected.getBulkFailures().size(); i++) { Failure expectedFailure = expected.getBulkFailures().get(i); @@ -122,6 +130,11 @@ protected void assertEqualInstances(BulkByScrollResponse expected, BulkByScrollR } } + @Override + protected void assertEqualInstances(BulkByScrollResponse expected, BulkByScrollResponse actual) { + assertEqualBulkResponse(expected, actual, includeUpdated, includeCreated); + } + @Override protected BulkByScrollResponse createTestInstance() { // failures are tested separately, so we can test XContent equivalence at least when we have no failures @@ -141,4 +154,22 @@ protected BulkByScrollResponse doParseInstance(XContentParser parser) throws IOE protected boolean supportsUnknownFields() { return true; } + + @Override + protected ToXContent.Params getToXContentParams() { + Map params = new HashMap<>(); + if (randomBoolean()) { + includeUpdated = false; + params.put(Status.INCLUDE_UPDATED, "false"); + } else { + includeUpdated = true; + } + if (randomBoolean()) { + includeCreated = false; + params.put(Status.INCLUDE_CREATED, "false"); + } else { + includeCreated = true; + } + return new ToXContent.MapParams(params); + } } diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java index 33c56bacd912..0d84b0e1412b 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusOrExceptionTests.java @@ -55,11 +55,14 @@ protected StatusOrException doParseInstance(XContentParser parser) throws IOExce return StatusOrException.fromXContent(parser); } - public static void assertEqualStatusOrException(StatusOrException expected, StatusOrException actual) { + public static void assertEqualStatusOrException(StatusOrException expected, StatusOrException actual, + boolean includeUpdated, boolean includeCreated) { if (expected != null && actual != null) { assertNotSame(expected, actual); if (expected.getException() == null) { - BulkByScrollTaskStatusTests.assertEqualStatus(expected.getStatus(), actual.getStatus()); + BulkByScrollTaskStatusTests + // we test includeCreated params in the Status tests + .assertEqualStatus(expected.getStatus(), actual.getStatus(), includeUpdated, includeCreated); } else { assertThat( actual.getException().getMessage(), @@ -74,7 +77,7 @@ public static void assertEqualStatusOrException(StatusOrException expected, Stat @Override protected void assertEqualInstances(StatusOrException expected, StatusOrException actual) { - assertEqualStatusOrException(expected, actual); + assertEqualStatusOrException(expected, actual, true, true); } @Override diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java index 368e1b3bdac0..13db9f4766e7 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/BulkByScrollTaskStatusTests.java @@ -33,7 +33,9 @@ import org.elasticsearch.index.reindex.BulkByScrollTask.Status; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.IntStream; @@ -44,6 +46,10 @@ import static org.hamcrest.Matchers.equalTo; public class BulkByScrollTaskStatusTests extends AbstractXContentTestCase { + + private boolean includeUpdated; + private boolean includeCreated; + public void testBulkByTaskStatus() throws IOException { BulkByScrollTask.Status status = randomStatus(); BytesStreamOutput out = new BytesStreamOutput(); @@ -144,21 +150,21 @@ bulkRetries, searchRetries, throttled, abs(Randomness.get().nextFloat()), ); } - public static void assertEqualStatus(BulkByScrollTask.Status expected, BulkByScrollTask.Status actual) { + public static void assertEqualStatus(BulkByScrollTask.Status expected, BulkByScrollTask.Status actual, + boolean includeUpdated, boolean includeCreated) { assertNotSame(expected, actual); - assertTrue(expected.equalsWithoutSliceStatus(actual)); + assertTrue(expected.equalsWithoutSliceStatus(actual, includeUpdated, includeCreated)); assertThat(expected.getSliceStatuses().size(), equalTo(actual.getSliceStatuses().size())); for (int i = 0; i< expected.getSliceStatuses().size(); i++) { BulkByScrollTaskStatusOrExceptionTests.assertEqualStatusOrException( - expected.getSliceStatuses().get(i), - actual.getSliceStatuses().get(i) + expected.getSliceStatuses().get(i), actual.getSliceStatuses().get(i), includeUpdated, includeCreated ); } } @Override protected void assertEqualInstances(BulkByScrollTask.Status first, BulkByScrollTask.Status second) { - assertEqualStatus(first, second); + assertEqualStatus(first, second, includeUpdated, includeCreated); } @Override @@ -193,4 +199,22 @@ public void testFromXContentWithFailures() throws IOException { getRandomFieldsExcludeFilter(), this::createParser, this::doParseInstance, this::assertEqualInstances, assertToXContentEquivalence, ToXContent.EMPTY_PARAMS); } + + @Override + protected ToXContent.Params getToXContentParams() { + Map params = new HashMap<>(); + if (randomBoolean()) { + includeUpdated = false; + params.put(Status.INCLUDE_UPDATED, "false"); + } else { + includeUpdated = true; + } + if (randomBoolean()) { + includeCreated = false; + params.put(Status.INCLUDE_CREATED, "false"); + } else { + includeCreated = true; + } + return new ToXContent.MapParams(params); + } } diff --git a/server/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryRequestTests.java b/server/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryRequestTests.java index b30968cf056b..47449eb73919 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryRequestTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/UpdateByQueryRequestTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.reindex; -import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.IndicesOptions; import static org.apache.lucene.util.TestUtil.randomSimpleString; @@ -32,11 +31,11 @@ public void testUpdateByQueryRequestImplementsIndicesRequestReplaceable() { indices[i] = randomSimpleString(random(), 1, 30); } - SearchRequest searchRequest = new SearchRequest(indices); IndicesOptions indicesOptions = IndicesOptions.fromOptions(randomBoolean(), randomBoolean(), randomBoolean(), randomBoolean()); - searchRequest.indicesOptions(indicesOptions); - UpdateByQueryRequest request = new UpdateByQueryRequest(searchRequest); + UpdateByQueryRequest request = new UpdateByQueryRequest(); + request.indices(indices); + request.setIndicesOptions(indicesOptions); for (int i = 0; i < numIndices; i++) { assertEquals(indices[i], request.indices()[i]); } @@ -60,7 +59,7 @@ public void testUpdateByQueryRequestImplementsIndicesRequestReplaceable() { @Override protected UpdateByQueryRequest newRequest() { - return new UpdateByQueryRequest(new SearchRequest(randomAlphaOfLength(5))); + return new UpdateByQueryRequest(randomAlphaOfLength(5)); } @Override

    + * For additional info + * see ML GET records documentation + * + * @param request the request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + */ + public GetRecordsResponse getRecords(GetRecordsRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::getRecords, + options, + GetRecordsResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Gets the records for a Machine Learning Job, notifies listener once the requested records are retrieved. + *

    Tl1%bLywQZkle)ZyT`G! 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