diff --git a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCheck.java b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCheck.java index a0c834aa35f..a466eb2e7c6 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCheck.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/BootstrapCheck.java @@ -44,6 +44,8 @@ import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * We enforce limits once any network host is configured. In this case we assume the node is running in production @@ -172,6 +174,7 @@ final class BootstrapCheck { checks.add(new UseSerialGCCheck()); checks.add(new OnErrorCheck()); checks.add(new OnOutOfMemoryErrorCheck()); + checks.add(new G1GCCheck()); return Collections.unmodifiableList(checks); } @@ -549,4 +552,51 @@ final class BootstrapCheck { } + /** + * Bootstrap check for versions of HotSpot that are known to have issues that can lead to index corruption when G1GC is enabled. + */ + static class G1GCCheck implements BootstrapCheck.Check { + + @Override + public boolean check() { + if ("Oracle Corporation".equals(jvmVendor()) && isG1GCEnabled()) { + final String jvmVersion = jvmVersion(); + final Pattern pattern = Pattern.compile("(\\d+)\\.(\\d+)-b\\d+"); + final Matcher matcher = pattern.matcher(jvmVersion); + final boolean matches = matcher.matches(); + assert matches : jvmVersion; + final int major = Integer.parseInt(matcher.group(1)); + final int update = Integer.parseInt(matcher.group(2)); + return major == 25 && update < 40; + } else { + return false; + } + } + + // visible for testing + String jvmVendor() { + return Constants.JVM_VENDOR; + } + + // visible for testing + boolean isG1GCEnabled() { + assert "Oracle Corporation".equals(jvmVendor()); + return JvmInfo.jvmInfo().useG1GC().equals("true"); + } + + // visible for testing + String jvmVersion() { + assert "Oracle Corporation".equals(jvmVendor()); + return Constants.JVM_VERSION; + } + + @Override + public String errorMessage() { + return String.format( + Locale.ROOT, + "JVM version [%s] can cause data corruption when used with G1GC; upgrade to at least Java 8u40", jvmVersion()); + } + + } + } diff --git a/core/src/main/java/org/elasticsearch/bootstrap/JVMCheck.java b/core/src/main/java/org/elasticsearch/bootstrap/JVMCheck.java index fd7c9c4b20b..8afebbce82d 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/JVMCheck.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/JVMCheck.java @@ -31,174 +31,21 @@ import java.util.Optional; /** Checks that the JVM is ok and won't cause index corruption */ final class JVMCheck { + /** no instantiation */ private JVMCheck() {} - /** - * URL with latest JVM recommendations - */ - static final String JVM_RECOMMENDATIONS = "http://www.elastic.co/guide/en/elasticsearch/reference/current/_installation.html"; - /** * System property which if set causes us to bypass the check completely (but issues a warning in doing so) */ static final String JVM_BYPASS = "es.bypass.vm.check"; - /** - * Metadata and messaging for checking and reporting HotSpot - * issues. - */ - interface HotSpotCheck { - /** - * If this HotSpot check should be executed. - * - * @return true if this HotSpot check should be executed - */ - boolean check(); - - /** - * The error message to display when this HotSpot issue is - * present. - * - * @return the error message for this HotSpot issue - */ - String getErrorMessage(); - - /** - * The warning message for this HotSpot issue if a workaround - * exists and is used. - * - * @return the warning message for this HotSpot issue - */ - Optional getWarningMessage(); - - /** - * The workaround for this HotSpot issue, if one exists. - * - * @return the workaround for this HotSpot issue, if one exists - */ - Optional getWorkaround(); - } - - /** - * Metadata and messaging for hotspot bugs. - */ - static class HotspotBug implements HotSpotCheck { - - /** OpenJDK bug URL */ - final String bugUrl; - - /** Compiler workaround flag (null if there is no workaround) */ - final String workAround; - - HotspotBug(String bugUrl, String workAround) { - this.bugUrl = bugUrl; - this.workAround = workAround; - } - - /** Returns an error message to the user for a broken version */ - public String getErrorMessage() { - StringBuilder sb = new StringBuilder(); - sb.append("Java version: ").append(fullVersion()); - sb.append(" suffers from critical bug ").append(bugUrl); - sb.append(" which can cause data corruption."); - sb.append(System.lineSeparator()); - sb.append("Please upgrade the JVM, see ").append(JVM_RECOMMENDATIONS); - sb.append(" for current recommendations."); - if (workAround != null) { - sb.append(System.lineSeparator()); - sb.append("If you absolutely cannot upgrade, please add ").append(workAround); - sb.append(" to the ES_JAVA_OPTS environment variable."); - sb.append(System.lineSeparator()); - sb.append("Upgrading is preferred, this workaround will result in degraded performance."); - } - return sb.toString(); - } - - /** Warns the user when a workaround is being used to dodge the bug */ - public Optional getWarningMessage() { - StringBuilder sb = new StringBuilder(); - sb.append("Workaround flag ").append(workAround); - sb.append(" for bug ").append(bugUrl); - sb.append(" found. "); - sb.append(System.lineSeparator()); - sb.append("This will result in degraded performance!"); - sb.append(System.lineSeparator()); - sb.append("Upgrading is preferred, see ").append(JVM_RECOMMENDATIONS); - sb.append(" for current recommendations."); - return Optional.of(sb.toString()); - } - - public boolean check() { - return true; - } - - @Override - public Optional getWorkaround() { - return Optional.of(workAround); - } - } - - static class G1GCCheck implements HotSpotCheck { - @Override - public boolean check() { - return JvmInfo.jvmInfo().useG1GC().equals("true"); - } - - /** Returns an error message to the user for a broken version */ - public String getErrorMessage() { - StringBuilder sb = new StringBuilder(); - sb.append("Java version: ").append(fullVersion()); - sb.append(" can cause data corruption"); - sb.append(" when used with G1GC."); - sb.append(System.lineSeparator()); - sb.append("Please upgrade the JVM, see ").append(JVM_RECOMMENDATIONS); - sb.append(" for current recommendations."); - return sb.toString(); - } - - @Override - public Optional getWarningMessage() { - return Optional.empty(); - } - - @Override - public Optional getWorkaround() { - return Optional.empty(); - } - } - - /** mapping of hotspot version to hotspot bug information for the most serious bugs */ - static final Map JVM_BROKEN_HOTSPOT_VERSIONS; - - static { - Map bugs = new HashMap<>(); - - G1GCCheck g1GcCheck = new G1GCCheck(); - bugs.put("25.0-b70", g1GcCheck); - bugs.put("25.11-b03", g1GcCheck); - bugs.put("25.20-b23", g1GcCheck); - bugs.put("25.25-b02", g1GcCheck); - bugs.put("25.31-b07", g1GcCheck); - - JVM_BROKEN_HOTSPOT_VERSIONS = Collections.unmodifiableMap(bugs); - } - /** * Checks that the current JVM is "ok". This means it doesn't have severe bugs that cause data corruption. */ static void check() { if (Boolean.parseBoolean(System.getProperty(JVM_BYPASS))) { Loggers.getLogger(JVMCheck.class).warn("bypassing jvm version check for version [{}], this can result in data corruption!", fullVersion()); - } else if ("Oracle Corporation".equals(Constants.JVM_VENDOR)) { - HotSpotCheck bug = JVM_BROKEN_HOTSPOT_VERSIONS.get(Constants.JVM_VERSION); - if (bug != null && bug.check()) { - if (bug.getWorkaround().isPresent() && ManagementFactory.getRuntimeMXBean().getInputArguments().contains(bug.getWorkaround().get())) { - Loggers.getLogger(JVMCheck.class).warn("{}", bug.getWarningMessage().get()); - } else { - throw new RuntimeException(bug.getErrorMessage()); - } - } } else if ("IBM Corporation".equals(Constants.JVM_VENDOR)) { // currently some old JVM versions from IBM will easily result in index corruption. // 2.8+ seems ok for ES from testing. @@ -237,4 +84,5 @@ final class JVMCheck { sb.append("]"); return sb.toString(); } + } diff --git a/core/src/test/java/org/elasticsearch/bootstrap/BootstrapCheckTests.java b/core/src/test/java/org/elasticsearch/bootstrap/BootstrapCheckTests.java index 2a00ca889d6..6566ed607d0 100644 --- a/core/src/test/java/org/elasticsearch/bootstrap/BootstrapCheckTests.java +++ b/core/src/test/java/org/elasticsearch/bootstrap/BootstrapCheckTests.java @@ -33,6 +33,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -42,6 +43,7 @@ import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.not; import static org.mockito.Mockito.mock; @@ -530,6 +532,60 @@ public class BootstrapCheckTests extends ESTestCase { consumer.accept(e); } + public void testG1GCCheck() throws NodeValidationException { + final AtomicBoolean isG1GCEnabled = new AtomicBoolean(true); + final AtomicReference jvmVersion = + new AtomicReference<>(String.format(Locale.ROOT, "25.%d-b%d", randomIntBetween(0, 39), randomIntBetween(1, 128))); + final BootstrapCheck.G1GCCheck oracleCheck = new BootstrapCheck.G1GCCheck() { + + @Override + String jvmVendor() { + return "Oracle Corporation"; + } + + @Override + boolean isG1GCEnabled() { + return isG1GCEnabled.get(); + } + + @Override + String jvmVersion() { + return jvmVersion.get(); + } + + }; + + final NodeValidationException e = + expectThrows( + NodeValidationException.class, + () -> BootstrapCheck.check(true, Collections.singletonList(oracleCheck), "testG1GCCheck")); + assertThat( + e.getMessage(), + containsString( + "JVM version [" + jvmVersion.get() + "] can cause data corruption when used with G1GC; upgrade to at least Java 8u40")); + + // if G1GC is disabled, nothing should happen + isG1GCEnabled.set(false); + BootstrapCheck.check(true, Collections.singletonList(oracleCheck), "testG1GCCheck"); + + // if on or after update 40, nothing should happen independent of whether or not G1GC is enabled + isG1GCEnabled.set(randomBoolean()); + jvmVersion.set(String.format(Locale.ROOT, "25.%d-b%d", randomIntBetween(40, 112), randomIntBetween(1, 128))); + BootstrapCheck.check(true, Collections.singletonList(oracleCheck), "testG1GCCheck"); + + final BootstrapCheck.G1GCCheck nonOracleCheck = new BootstrapCheck.G1GCCheck() { + + @Override + String jvmVendor() { + return randomAsciiOfLength(8); + } + + }; + + // if not on an Oracle JVM, nothing should happen + BootstrapCheck.check(true, Collections.singletonList(nonOracleCheck), "testG1GCCheck"); + } + public void testAlwaysEnforcedChecks() { final BootstrapCheck.Check check = new BootstrapCheck.Check() { @Override diff --git a/docs/reference/setup/bootstrap-checks.asciidoc b/docs/reference/setup/bootstrap-checks.asciidoc index 9c2276bc213..9e199ba8375 100644 --- a/docs/reference/setup/bootstrap-checks.asciidoc +++ b/docs/reference/setup/bootstrap-checks.asciidoc @@ -161,3 +161,11 @@ enabled. This check is always enforced. To pass this check do not enable use the JVM flag `ExitOnOutOfMemoryError`. While this does not have the full capabilities of `OnError` nor `OnOutOfMemoryError`, arbitrary forking will not be supported with seccomp enabled. + +=== G1GC check + +Early versions of the HotSpot JVM that shipped with JDK 8 are known to have +issues that can lead to index corruption when the G1GC collector is enabled. +The versions impacted are those earlier than the version of HotSpot that +shipped with JDK 8u40. The G1GC check detects these early versions of the +HotSpot JVM.