diff --git a/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java b/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java index 289cda112..819b40600 100755 --- a/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java +++ b/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilder.java @@ -31,6 +31,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 @@ -744,6 +745,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 a2b774d04..18d9b7662 100644 --- a/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilderPrivateAPI.java +++ b/src/main/java/net/openhft/chronicle/hash/ChronicleHashBuilderPrivateAPI.java @@ -89,4 +89,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 a861c5bcb..ca4d5633f 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 dac77b2ed..316bc646e 100644 --- a/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java +++ b/src/main/java/net/openhft/chronicle/hash/impl/VanillaChronicleHash.java @@ -122,6 +122,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 d4ee4f938..dbcbf8172 100755 --- a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java +++ b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilder.java @@ -180,6 +180,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 = @@ -1589,6 +1590,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 e32fcbb10..5c942413d 100644 --- a/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilderPrivateAPI.java +++ b/src/main/java/net/openhft/chronicle/map/ChronicleMapBuilderPrivateAPI.java @@ -115,4 +115,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 d098317a2..104f347cc 100755 --- a/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilder.java +++ b/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilder.java @@ -352,6 +352,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 5f192eb57..8edb56d5c 100644 --- a/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilderPrivateAPI.java +++ b/src/main/java/net/openhft/chronicle/set/ChronicleSetBuilderPrivateAPI.java @@ -133,4 +133,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 4330a359e..26837dbf8 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(); }