Shutdown: Add support for Ctrl-Close event on Windows platforms to gracefully shutdown node

This commit adds the support for the Ctrl-Close event on Windows using native system calls. This way, it is possible to catch the Ctrl-Close event sent by a 'taskill /pid' command (or when the user closes the console window where elasticsearch.bat was started) and gracefully close the node. Before this commit, the node was simply killed on taskkill/window closing.
This commit is contained in:
tlrx 2014-12-17 18:00:53 +01:00
parent 90f2f1da84
commit a4133ec4a3
4 changed files with 254 additions and 17 deletions

View File

@ -19,14 +19,12 @@
package org.elasticsearch.bootstrap; package org.elasticsearch.bootstrap;
import com.google.common.base.Charsets;
import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.Version; import org.elasticsearch.Version;
import org.elasticsearch.common.PidFile; import org.elasticsearch.common.PidFile;
import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.inject.CreationException; import org.elasticsearch.common.inject.CreationException;
import org.elasticsearch.common.inject.spi.Message; import org.elasticsearch.common.inject.spi.Message;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.jna.Natives; import org.elasticsearch.common.jna.Natives;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.logging.Loggers;
@ -39,15 +37,13 @@ import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder; import org.elasticsearch.node.NodeBuilder;
import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.elasticsearch.node.internal.InternalSettingsPreparer;
import java.io.File; import java.nio.file.Paths;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import static com.google.common.collect.Sets.newHashSet; import static com.google.common.collect.Sets.newHashSet;
import static org.elasticsearch.common.jna.Kernel32Library.ConsoleCtrlHandler;
import static org.elasticsearch.common.settings.ImmutableSettings.Builder.EMPTY_SETTINGS; import static org.elasticsearch.common.settings.ImmutableSettings.Builder.EMPTY_SETTINGS;
/** /**
@ -62,11 +58,9 @@ public class Bootstrap {
private static Bootstrap bootstrap; private static Bootstrap bootstrap;
private void setup(boolean addShutdownHook, Tuple<Settings, Environment> tuple) throws Exception { private void setup(boolean addShutdownHook, Tuple<Settings, Environment> tuple) throws Exception {
// Loggers.getLogger(Bootstrap.class, tuple.v1().get("name")).info("heap_size {}/{}", JvmStats.jvmStats().mem().heapCommitted(), JvmInfo.jvmInfo().mem().heapMax());
if (tuple.v1().getAsBoolean("bootstrap.mlockall", false)) { if (tuple.v1().getAsBoolean("bootstrap.mlockall", false)) {
Natives.tryMlockall(); Natives.tryMlockall();
} }
tuple = setupJmx(tuple);
NodeBuilder nodeBuilder = NodeBuilder.nodeBuilder().settings(tuple.v1()).loadConfigSettings(false); NodeBuilder nodeBuilder = NodeBuilder.nodeBuilder().settings(tuple.v1()).loadConfigSettings(false);
node = nodeBuilder.build(); node = nodeBuilder.build();
@ -78,16 +72,22 @@ public class Bootstrap {
} }
}); });
} }
}
private static Tuple<Settings, Environment> setupJmx(Tuple<Settings, Environment> tuple) { if (tuple.v1().getAsBoolean("bootstrap.ctrlhandler", true)) {
// We disable JMX on by default, since we don't really want the overhead of RMI (and RMI GC...) Natives.addConsoleCtrlHandler(new ConsoleCtrlHandler() {
// if (tuple.v1().get(JmxService.SettingsConstants.CREATE_CONNECTOR) == null) { @Override
// // automatically create the connector if we are bootstrapping public boolean handle(int code) {
// Settings updated = settingsBuilder().put(tuple.v1()).put(JmxService.SettingsConstants.CREATE_CONNECTOR, true).build(); if (CTRL_CLOSE_EVENT == code) {
// tuple = new Tuple<Settings, Environment>(updated, tuple.v2()); ESLogger logger = Loggers.getLogger(Bootstrap.class);
// } logger.info("running graceful exit on windows");
return tuple;
System.exit(0);
return true;
}
return false;
}
});
}
} }
private static void setupLogging(Tuple<Settings, Environment> tuple) { private static void setupLogging(Tuple<Settings, Environment> tuple) {

View File

@ -0,0 +1,129 @@
/*
* 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.jna;
import com.google.common.collect.ImmutableList;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.win32.StdCallLibrary;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import java.util.ArrayList;
import java.util.List;
/**
* Library for Windows/Kernel32
*/
public class Kernel32Library {
private static ESLogger logger = Loggers.getLogger(Kernel32Library.class);
private Kernel32 internal;
private List<NativeHandlerCallback> callbacks = new ArrayList<>();
private final static class Holder {
private final static Kernel32Library instance = new Kernel32Library();
}
private Kernel32Library() {
try {
internal = (Kernel32)Native.synchronizedLibrary((Kernel32)Native.loadLibrary("kernel32", Kernel32.class));
logger.debug("windows/Kernel32 library loaded");
} catch (NoClassDefFoundError e) {
logger.warn("JNA not found. native methods and handlers will be disabled.");
} catch (UnsatisfiedLinkError e) {
logger.warn("unable to link Windows/Kernel32 library. native methods and handlers will be disabled.");
}
}
public static Kernel32Library getInstance() {
return Holder.instance;
}
public boolean addConsoleCtrlHandler(ConsoleCtrlHandler handler) {
if (internal == null) {
throw new UnsupportedOperationException("windows/Kernel32 library not loaded, console ctrl handler cannot be set");
}
boolean result = false;
if (handler != null) {
NativeHandlerCallback callback = new NativeHandlerCallback(handler);
result = internal.SetConsoleCtrlHandler(callback, true);
if (result) {
callbacks.add(callback);
}
}
return result;
}
public ImmutableList<Object> getCallbacks() {
return ImmutableList.builder().addAll(callbacks).build();
}
interface Kernel32 extends Library {
/**
* Registers a Console Ctrl Handler.
*
* @param handler
* @param add
* @return true if the handler is correctly set
*/
public boolean SetConsoleCtrlHandler(StdCallLibrary.StdCallCallback handler, boolean add);
}
/**
* Handles consoles event with WIN API
* <p/>
* See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683242%28v=vs.85%29.aspx
*/
class NativeHandlerCallback implements StdCallLibrary.StdCallCallback {
private final ConsoleCtrlHandler handler;
public NativeHandlerCallback(ConsoleCtrlHandler handler) {
this.handler = handler;
}
public boolean callback(long dwCtrlType) {
int event = (int) dwCtrlType;
if (logger.isDebugEnabled()) {
logger.debug("console control handler receives event [{}@{}]", event, dwCtrlType);
}
return handler.handle(event);
}
}
public interface ConsoleCtrlHandler {
public static final int CTRL_CLOSE_EVENT = 2;
/**
* Handles the Ctrl event.
*
* @param code the code corresponding to the Ctrl sent.
* @return true if the handler processed the event, false otherwise. If false, the next handler will be called.
*/
boolean handle(int code);
}
}

View File

@ -20,6 +20,8 @@
package org.elasticsearch.common.jna; package org.elasticsearch.common.jna;
import com.sun.jna.Native; import com.sun.jna.Native;
import org.apache.lucene.util.Constants;
import org.elasticsearch.common.jna.Kernel32Library.ConsoleCtrlHandler;
import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.logging.Loggers;
@ -59,4 +61,21 @@ public class Natives {
} }
} }
} }
public static void addConsoleCtrlHandler(ConsoleCtrlHandler handler) {
// The console Ctrl handler is necessary on Windows platforms only.
if (Constants.WINDOWS) {
try {
boolean result = Kernel32Library.getInstance().addConsoleCtrlHandler(handler);
if (result) {
logger.debug("console ctrl handler correctly set");
} else {
logger.warn("unknown error " + Native.getLastError() + " when adding console ctrl handler:");
}
} catch (UnsatisfiedLinkError e) {
// this will have already been logged by Kernel32Library, no need to repeat it
}
}
}
} }

View File

@ -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.common.jna;
import org.apache.lucene.util.Constants;
import org.elasticsearch.common.jna.Kernel32Library.ConsoleCtrlHandler;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
public class Kernel32LibraryTests extends ElasticsearchTestCase {
/**
* Those properties are set by the JNA Api and if not ignored,
* lead to tests failure (see AbstractRandomizedTest#IGNORED_INVARIANT_PROPERTIES)
*/
private static final String[] JNA_INVARIANT_PROPERTIES = {
"jna.platform.library.path",
"jnidispatch.path"
};
private Map<String, String> properties = new HashMap<>();
@Before
public void saveProperties() {
for (String p : JNA_INVARIANT_PROPERTIES) {
properties.put(p, System.getProperty(p));
}
}
@After
public void restoreProperties() {
for (String p : JNA_INVARIANT_PROPERTIES) {
if (properties.get(p) != null) {
System.setProperty(p, properties.get(p));
} else {
System.clearProperty(p);
}
}
}
@Test
public void testKernel32Library() {
ConsoleCtrlHandler handler = new ConsoleCtrlHandler() {
@Override
public boolean handle(int code) {
return false;
}
};
assertNotNull(Kernel32Library.getInstance());
assertThat(Kernel32Library.getInstance().getCallbacks().size(), equalTo(0));
if (Constants.WINDOWS) {
assertTrue(Kernel32Library.getInstance().addConsoleCtrlHandler(handler));
assertThat(Kernel32Library.getInstance().getCallbacks().size(), equalTo(1));
} else {
try {
Kernel32Library.getInstance().addConsoleCtrlHandler(handler);
fail("should have thrown an unsupported operation exception");
} catch (UnsupportedOperationException e) {
assertThat(e.getMessage(), e.getMessage().contains("console ctrl handler cannot be set"), equalTo(true));
}
}
}
}