Add test for dying with dignity (#28987)

I have long wanted an actual test that dying with dignity works. It is
tricky because if dying with dignity works, it means the test JVM dies
which is usually an abnormal condition. And anyway, how does one force a
fatal error to be thrown. I was motivated to investigate this again by
the fact that I missed a backport to one branch leading to an issue
where Elasticsearch would not successfully die with dignity. And now we
have a solution: we install a plugin that throws an out of memory error
when it receives a request. We hack the standalone test infrastructure
to prevent this from failing the test. To do this, we bypass the
security manager and remove the PID file for the node; this tricks the
test infrastructure into thinking that it does not need to stop the
node. We also bypass seccomp so that we can fork jps to make sure that
Elasticsearch really died. And to be extra paranoid, we parse the logs
of the dead Elasticsearch process to make sure it died with
dignity. Never forget.
This commit is contained in:
Jason Tedor 2018-03-12 23:20:07 -04:00 committed by GitHub
parent 6088af5887
commit 8b6fbe2c11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 254 additions and 4 deletions

View File

@ -0,0 +1,37 @@
/*
* 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.
*/
apply plugin: 'elasticsearch.esplugin'
esplugin {
description 'Out of memory plugin'
classname 'org.elasticsearch.DieWithDignityPlugin'
}
integTestRunner {
systemProperty 'tests.security.manager', 'false'
systemProperty 'tests.system_call_filter', 'false'
systemProperty 'pidfile', "${-> integTest.getNodes().get(0).pidFile}"
systemProperty 'log', "${-> integTest.getNodes().get(0).homeDir}/logs/${-> integTest.getNodes().get(0).clusterName}.log"
systemProperty 'runtime.java.home', "${project.runtimeJavaHome}"
}
test.enabled = false
check.dependsOn integTest

View File

@ -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;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsFilter;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
public class DieWithDignityPlugin extends Plugin implements ActionPlugin {
@Override
public List<RestHandler> getRestHandlers(
final Settings settings,
final RestController restController,
final ClusterSettings clusterSettings,
final IndexScopedSettings indexScopedSettings,
final SettingsFilter settingsFilter,
final IndexNameExpressionResolver indexNameExpressionResolver,
final Supplier<DiscoveryNodes> nodesInCluster) {
return Collections.singletonList(new RestDieWithDignityAction(settings, restController));
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.http.HttpStats;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import java.io.IOException;
public class RestDieWithDignityAction extends BaseRestHandler {
RestDieWithDignityAction(final Settings settings, final RestController restController) {
super(settings);
restController.registerHandler(RestRequest.Method.GET, "/_die_with_dignity", this);
}
@Override
public String getName() {
return "die_with_dignity_action";
}
@Override
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
throw new OutOfMemoryError("die with dignity");
}
}

View File

@ -0,0 +1,98 @@
/*
* 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.qa.die_with_dignity;
import org.apache.http.ConnectionClosedException;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseListener;
import org.elasticsearch.common.io.PathUtils;
import org.elasticsearch.test.rest.ESRestTestCase;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
public class DieWithDignityIT extends ESRestTestCase {
public void testDieWithDignity() throws Exception {
// deleting the PID file prevents stopping the cluster from failing since it occurs if and only if the PID file exists
final Path pidFile = PathUtils.get(System.getProperty("pidfile"));
final List<String> pidFileLines = Files.readAllLines(pidFile);
assertThat(pidFileLines, hasSize(1));
final int pid = Integer.parseInt(pidFileLines.get(0));
Files.delete(pidFile);
expectThrows(ConnectionClosedException.class, () -> client().performRequest("GET", "/_die_with_dignity"));
// the Elasticsearch process should die and disappear from the output of jps
assertBusy(() -> {
final String jpsPath = PathUtils.get(System.getProperty("runtime.java.home"), "bin/jps").toString();
final Process process = new ProcessBuilder().command(jpsPath).start();
assertThat(process.waitFor(), equalTo(0));
try (InputStream is = process.getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
String line;
while ((line = in.readLine()) != null) {
final int currentPid = Integer.parseInt(line.split("\\s+")[0]);
assertThat(line, pid, not(equalTo(currentPid)));
}
}
});
// parse the logs and ensure that Elasticsearch died with the expected cause
final List<String> lines = Files.readAllLines(PathUtils.get(System.getProperty("log")));
final Iterator<String> it = lines.iterator();
boolean fatalErrorOnTheNetworkLayer = false;
boolean fatalErrorInThreadExiting = false;
while (it.hasNext() && (fatalErrorOnTheNetworkLayer == 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\\]"
+ " fatal error in thread \\[Thread-\\d+\\], exiting$")) {
fatalErrorInThreadExiting = true;
assertTrue(it.hasNext());
assertThat(it.next(), equalTo("java.lang.OutOfMemoryError: die with dignity"));
}
}
assertTrue(fatalErrorOnTheNetworkLayer);
assertTrue(fatalErrorInThreadExiting);
}
@Override
protected boolean preserveClusterUponCompletion() {
// as the cluster is dead its state can not be wiped successfully so we have to bypass wiping the cluster
return true;
}
}

View File

@ -78,7 +78,8 @@ public class BootstrapForTesting {
}
// just like bootstrap, initialize natives, then SM
Bootstrap.initializeNatives(javaTmpDir, true, true, true);
final boolean systemCallFilter = Booleans.parseBoolean(System.getProperty("tests.system_call_filter", "true"));
Bootstrap.initializeNatives(javaTmpDir, true, systemCallFilter, true);
// initialize probes
Bootstrap.initializeProbes();

View File

@ -145,9 +145,11 @@ public abstract class ESRestTestCase extends ESTestCase {
*/
@After
public final void cleanUpCluster() throws Exception {
wipeCluster();
waitForClusterStateUpdatesToFinish();
logIfThereAreRunningTasks();
if (preserveClusterUponCompletion() == false) {
wipeCluster();
waitForClusterStateUpdatesToFinish();
logIfThereAreRunningTasks();
}
}
@AfterClass
@ -175,6 +177,17 @@ public abstract class ESRestTestCase extends ESTestCase {
return adminClient;
}
/**
* Returns whether to preserve the state of the cluster upon completion of this test. Defaults to false. If true, overrides the value of
* {@link #preserveIndicesUponCompletion()}, {@link #preserveTemplatesUponCompletion()}, {@link #preserveReposUponCompletion()}, and
* {@link #preserveSnapshotsUponCompletion()}.
*
* @return true if the state of the cluster should be preserved
*/
protected boolean preserveClusterUponCompletion() {
return false;
}
/**
* Returns whether to preserve the indices created during this test on completion of this test.
* Defaults to {@code false}. Override this method if indices should be preserved after the test,