From 9eed0e326578d929527ca5c674331b77d29410bb Mon Sep 17 00:00:00 2001 From: udaysagar2177 Date: Sun, 9 Sep 2018 22:10:43 -0700 Subject: [PATCH] add pre-shutdown action to be executed inside ChronicleHashCloseOnExitHook. This is useful when we want to do an orderly shutdown. For example: To carry out graceful shutdown and explicitly control when a ChronicleHash is closed, a pre-shutdown action can be a wait on a CountDownLatch that would be released appropriately. --- .../chronicle/hash/ChronicleHashBuilder.java | 17 +++++++ .../hash/ChronicleHashBuilderPrivateAPI.java | 8 +++ .../impl/ChronicleHashCloseOnExitHook.java | 14 ++++++ .../hash/impl/VanillaChronicleHash.java | 7 +++ .../chronicle/map/ChronicleMapBuilder.java | 6 +++ .../map/ChronicleMapBuilderPrivateAPI.java | 5 ++ .../chronicle/set/ChronicleSetBuilder.java | 5 ++ .../set/ChronicleSetBuilderPrivateAPI.java | 5 ++ .../openhft/chronicle/map/ExitHookTest.java | 49 ++++++++++++++----- 9 files changed, 105 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java b/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java index 19331e46d..6c8645ea2 100755 --- a/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java +++ b/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; /** * Base interface for {@link ChronicleMapBuilder} and {@link ChronicleSetBuilder}, i. e. defines @@ -745,6 +746,22 @@ public interface ChronicleHashBuilder, File file, boolean sameBuilderConfigAndLibraryVersion, ChronicleHashCorruption.Listener corruptionListener) throws IOException; + /** + * A {@link ChronicleHash} created using this builder is closed using a JVM shutdown hook. + * This method lets you perform an action before the {@link ChronicleHash} is closed. + *

+ *

The registered action is not executed when JVM is running and a + * {@link ChronicleHash#close()} is explicitly called. + *

+ *

Example usage of this call: To carry out a graceful shutdown and explicitly + * control when the {@link ChronicleHash} is closed, the action can be a wait on a + * {@link CountDownLatch} that would be released appropriately. + * + * @param preShutdownAction action to run before closing the {@link ChronicleHash} in a + * JVM shutdown hook. + */ + void setPreShutdownAction(Runnable preShutdownAction); + /** * @deprecated don't use private API in the client code */ diff --git a/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilderPrivateAPI.java b/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilderPrivateAPI.java index 4907578f0..cc0e4c23c 100644 --- a/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilderPrivateAPI.java +++ b/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilderPrivateAPI.java @@ -90,4 +90,12 @@ public interface ChronicleHashBuilderPrivateAPI { void removedEntryCleanupTimeout(long removedEntryCleanupTimeout, TimeUnit unit); void remoteOperations(RO remoteOperations); + + /** + * Provides registered action to be executed before closing a {@link ChronicleHash} in a JVM + * shutdown hook. + * + * @return registered pre-shutdown action. + */ + Runnable getPreShutdownAction(); } diff --git a/src/main/java/net/openhft/chronicle/hash/impl/ChronicleHashCloseOnExitHook.java b/src/main/java/net/openhft/chronicle/hash/impl/ChronicleHashCloseOnExitHook.java index 9dfe04fff..87bfeb63b 100644 --- a/src/main/java/net/openhft/chronicle/hash/impl/ChronicleHashCloseOnExitHook.java +++ b/src/main/java/net/openhft/chronicle/hash/impl/ChronicleHashCloseOnExitHook.java @@ -60,6 +60,20 @@ final class ChronicleHashCloseOnExitHook { // close later added maps first orderedMaps.descendingMap().values().forEach(h -> { try { + Runnable preShutdownAction = h.getPreShutdownAction(); + if (preShutdownAction != null) { + try { + preShutdownAction.run(); + } catch (Throwable throwable) { + try { + LOG.error("Error running pre-shutdown action for " + h.toIdentityString() + + " :", throwable); + } catch (Throwable t2) { + throwable.addSuppressed(t2); + throwable.printStackTrace(); + } + } + } h.close(); } catch (Throwable throwable) { try { diff --git a/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java b/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java index 2d7daa05a..7c67199de 100644 --- a/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java +++ b/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java @@ -125,6 +125,7 @@ public abstract class VanillaChronicleHash "dataFileVersion").text(); diff --git a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java index d71fa4148..fa0826a3a 100755 --- a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java +++ b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java @@ -181,6 +181,7 @@ public final class ChronicleMapBuilder implements MapMethods methods = DefaultSpi.mapMethods(); MapEntryOperations entryOperations = mapEntryOperations(); MapRemoteOperations remoteOperations = mapRemoteOperations(); + Runnable preShutdownAction; private String name; // not final because of cloning private ChronicleMapBuilderPrivateAPI privateAPI = @@ -1587,6 +1588,11 @@ public final class ChronicleMapBuilder implements corruptionListener); } + @Override + public void setPreShutdownAction(Runnable preShutdownAction) { + this.preShutdownAction = preShutdownAction; + } + @Override public ChronicleMap create() { // clone() to make this builder instance thread-safe, because createWithoutFile() method diff --git a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilderPrivateAPI.java b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilderPrivateAPI.java index e03fabd7c..6a13fd278 100644 --- a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilderPrivateAPI.java +++ b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilderPrivateAPI.java @@ -116,4 +116,9 @@ class ChronicleMapBuilderPrivateAPI public void remoteOperations(MapRemoteOperations remoteOperations) { b.remoteOperations(remoteOperations); } + + @Override + public Runnable getPreShutdownAction() { + return b.preShutdownAction; + } } diff --git a/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilder.java b/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilder.java index a7d5d53d2..2e5688892 100755 --- a/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilder.java +++ b/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilder.java @@ -353,6 +353,11 @@ public final class ChronicleSetBuilder return new SetFromMap<>((VanillaChronicleMap) map); } + @Override + public void setPreShutdownAction(Runnable preShutdownAction) { + chronicleMapBuilder.setPreShutdownAction(preShutdownAction); + } + /** * @deprecated don't use private API in the client code */ diff --git a/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilderPrivateAPI.java b/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilderPrivateAPI.java index c168baa62..61008462f 100644 --- a/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilderPrivateAPI.java +++ b/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilderPrivateAPI.java @@ -134,4 +134,9 @@ class ChronicleSetBuilderPrivateAPI } }); } + + @Override + public Runnable getPreShutdownAction() { + return mapB.getPreShutdownAction(); + } } diff --git a/src/test/java/net/openhft/chronicle/map/ExitHookTest.java b/src/test/java/net/openhft/chronicle/map/ExitHookTest.java index 2a82b22c8..40f861c86 100644 --- a/src/test/java/net/openhft/chronicle/map/ExitHookTest.java +++ b/src/test/java/net/openhft/chronicle/map/ExitHookTest.java @@ -16,6 +16,7 @@ package net.openhft.chronicle.map; +import com.google.common.base.Preconditions; import net.openhft.chronicle.core.OS; import org.junit.Rule; import org.junit.Test; @@ -24,12 +25,18 @@ import org.junit.rules.TemporaryFolder; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; +import java.nio.file.Files; +import java.util.Iterator; +import java.util.stream.Stream; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class ExitHookTest { private static final int KEY = 1; + private static final String PRE_SHUTDOWN_ACTION_EXECUTED = "PRE_SHUTDOWN_ACTION_EXECUTED"; @Rule public TemporaryFolder folder = new TemporaryFolder(); @@ -37,18 +44,30 @@ public class ExitHookTest { public static void main(String[] args) throws IOException, InterruptedException { System.out.println("Other process started"); File mapFile = new File(args[0]); - ChronicleMap map = createMap(mapFile); + File preShutdownActionExecutionConfirmationFile = new File(args[1]); + ChronicleMapBuilder mapBuilder = createMapBuilder(); + mapBuilder.setPreShutdownAction(() -> { + try { + System.out.println("Executing pre-shutdown action"); + Preconditions.checkState(preShutdownActionExecutionConfirmationFile.exists()); + Files.write(preShutdownActionExecutionConfirmationFile.toPath(), + PRE_SHUTDOWN_ACTION_EXECUTED.getBytes()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + ChronicleMap map = mapBuilder.createPersistedTo(mapFile); try (ExternalMapQueryContext c = map.queryContext(KEY)) { c.writeLock().lock(); Thread.sleep(30_000); } } - private static ChronicleMap createMap(File mapFile) throws IOException { - return ChronicleMap + private static ChronicleMapBuilder createMapBuilder() + throws IOException { + return ChronicleMapBuilder .of(Integer.class, Integer.class) - .entries(1) - .createPersistedTo(mapFile); + .entries(1); } // http://stackoverflow.com/a/33171840/648955 @@ -73,9 +92,9 @@ public class ExitHookTest { if (!OS.isLinux() && !OS.isMacOSX()) return; // This test runs only in Unix-like OSes File mapFile = folder.newFile(); - System.out.println("map file: " + mapFile.getAbsolutePath()); + File preShutdownActionExecutionConfirmationFile = folder.newFile(); // Create a process which opens the map, acquires the lock and "hangs" for 30 seconds - Process process = startOtherProcess(mapFile); + Process process = startOtherProcess(mapFile, preShutdownActionExecutionConfirmationFile); // Let the other process actually reach the moment when it locks the map // (JVM startup and chronicle map creation are not instant) Thread.sleep(10_000); @@ -85,12 +104,19 @@ public class ExitHookTest { int actual = process.exitValue(); if (actual != 0) // clean shutdown assertEquals(130, actual); // 130 is exit code for SIGINT (interruption). - ChronicleMap map = createMap(mapFile); + ChronicleMap map = createMapBuilder().createPersistedTo(mapFile); try (ExternalMapQueryContext c = map.queryContext(KEY)) { // Test that we are able to lock the segment, i. e. the lock was released in other // process thanks to shutdown hooks c.writeLock().lock(); } + try (Stream lines = Files.lines(preShutdownActionExecutionConfirmationFile.toPath())) { + Iterator lineIterator = lines.iterator(); + assertTrue(lineIterator.hasNext()); + String line = lineIterator.next(); + assertEquals(PRE_SHUTDOWN_ACTION_EXECUTED, line); + assertFalse(lineIterator.hasNext()); + } } // http://stackoverflow.com/a/7835467/648955 @@ -99,7 +125,7 @@ public class ExitHookTest { } // http://stackoverflow.com/a/723914/648955 - private Process startOtherProcess(File mapFile) throws IOException { + private Process startOtherProcess(File mapFile, File outputFile) throws IOException { String javaHome = System.getProperty("java.home"); String javaBin = javaHome + File.separator + "bin" + @@ -108,8 +134,9 @@ public class ExitHookTest { System.out.println("Classpath: " + classpath); String className = ExitHookTest.class.getCanonicalName(); - ProcessBuilder builder = new ProcessBuilder( - javaBin, "-cp", classpath, className, mapFile.getAbsolutePath()); + String[] command = new String[] {javaBin, "-cp", classpath, className, + mapFile.getAbsolutePath(), outputFile.getAbsolutePath()}; + ProcessBuilder builder = new ProcessBuilder(command); builder.inheritIO(); return builder.start(); }