From 56b8dbdb22466c72cf8ef85056cac385a228f23b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Feb 2020 12:13:10 +0100 Subject: [PATCH] Reimplement restarting of scm-manager SCM-Manager tries now to figure out which is the best strategy for the restart. It chooses from one of the following strategies: * PosixRestartStrategy which uses native LibC * ExitRestartStrategy uses System.exit and relies on external mechanism to start again * InjectionContextRestartStrategy destroys and re initializes the injection context --- scm-webapp/pom.xml | 14 +++ .../scm/lifecycle/BootstrapContextFilter.java | 9 +- .../java/sonia/scm/lifecycle/CLibrary.java | 22 ++++ .../scm/lifecycle/ExitRestartStrategy.java | 52 +++++++++ .../InjectionContextRestartStrategy.java | 6 +- .../scm/lifecycle/PosixRestartStrategy.java | 52 +++++++++ .../RestartNotSupportedException.java | 10 ++ .../sonia/scm/lifecycle/RestartStrategy.java | 13 ++- .../scm/lifecycle/RestartStrategyFactory.java | 66 ++++++++++++ .../lifecycle/ExitRestartStrategyTest.java | 75 +++++++++++++ .../scm/lifecycle/RestartStrategyTest.java | 101 ++++++++++++++++++ 11 files changed, 412 insertions(+), 8 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java create mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 924e176024..b71d4043c2 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -295,6 +295,20 @@ 2.7.0 + + + + org.kohsuke + akuma + 1.10 + + + + net.java.dev.jna + jna + 5.5.0 + + diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java index 1d642d9c66..388ec7bb73 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java @@ -42,6 +42,7 @@ import sonia.scm.event.ScmEventBus; import javax.servlet.FilterConfig; import javax.servlet.ServletContextEvent; import javax.servlet.ServletException; +import java.util.Optional; //~--- JDK imports ------------------------------------------------------------ @@ -100,8 +101,12 @@ public class BootstrapContextFilter extends GuiceFilter { if (filterConfig == null) { LOG.error("filter config is null, scm-manager is not initialized"); } else { - RestartStrategy restartStrategy = RestartStrategy.get(webAppClassLoader); - restartStrategy.restart(new GuiceInjectionContext()); + Optional restartStrategy = RestartStrategy.get(webAppClassLoader); + if (restartStrategy.isPresent()) { + restartStrategy.get().restart(new GuiceInjectionContext()); + } else { + LOG.warn("restarting is not supported by the underlying platform"); + } } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java new file mode 100644 index 0000000000..91b82ce3d1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java @@ -0,0 +1,22 @@ +package sonia.scm.lifecycle; + +/** + * Interface for native c library. + */ +@SuppressWarnings({ + "squid:S1214", // usage as constant is common practice for jna + "squid:S1191" // use of sun.* classes is required for jna +}) +interface CLibrary extends com.sun.jna.Library { + CLibrary LIBC = com.sun.jna.Native.load("c", CLibrary.class); + + int F_GETFD = 1; + int F_SETFD = 2; + int FD_CLOEXEC = 1; + + int getdtablesize(); + int fcntl(int fd, int command); + int fcntl(int fd, int command, int flags); + int execvp(String file, com.sun.jna.StringArray args); + String strerror(int errno); +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java new file mode 100644 index 0000000000..dfa3d72f82 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java @@ -0,0 +1,52 @@ +package sonia.scm.lifecycle; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.IntConsumer; + +/** + * {@link RestartStrategy} which tears down the scm-manager context and + * then exists the java process with {@link System#exit(int)}. + *

+ * This is useful if an external mechanism is able to restart the process after it has exited. + */ +class ExitRestartStrategy implements RestartStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(ExitRestartStrategy.class); + + static final String NAME = "exit"; + + static final String PROPERTY_EXIT_CODE = "sonia.scm.restart.exit-code"; + + private IntConsumer exiter = System::exit; + + ExitRestartStrategy() { + } + + @VisibleForTesting + void setExiter(IntConsumer exiter) { + this.exiter = exiter; + } + + @Override + public void restart(InjectionContext context) { + int exitCode = determineExitCode(); + + LOG.warn("destroy injection context"); + context.destroy(); + + LOG.warn("exit scm-manager with exit code {}", exitCode); + exiter.accept(exitCode); + } + + private int determineExitCode() { + String exitCodeAsString = System.getProperty(PROPERTY_EXIT_CODE, "0"); + try { + return Integer.parseInt(exitCodeAsString); + } catch (NumberFormatException ex) { + throw new RestartNotSupportedException("invalid exit code " + exitCodeAsString, ex); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java index 2db60580b1..e78e7b615c 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java @@ -10,9 +10,11 @@ import sonia.scm.event.ShutdownEventBusEvent; import java.util.concurrent.atomic.AtomicLong; /** - * Restart strategy implementation which destroy the injection context and re initialize it. + * Restart strategy which tries to free, every resource used by the context, starts gc and re initializes the context. */ -public class InjectionContextRestartStrategy implements RestartStrategy { +class InjectionContextRestartStrategy implements RestartStrategy { + + static final String NAME = "context"; private static final String DISABLE_RESTART_PROPERTY = "sonia.scm.restart.disable"; private static final String WAIT_PROPERTY = "sonia.scm.restart.wait"; diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java new file mode 100644 index 0000000000..d7d64b9e6d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java @@ -0,0 +1,52 @@ +package sonia.scm.lifecycle; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static sonia.scm.lifecycle.CLibrary.*; + +/** + * Restart strategy which uses execvp from libc. This strategy is only supported on posix base operating systems. + */ +class PosixRestartStrategy implements RestartStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(PosixRestartStrategy.class); + + PosixRestartStrategy() { + } + + @Override + public void restart(InjectionContext context) { + LOG.warn("destroy injection context"); + context.destroy(); + + LOG.warn("restart scm-manager jvm process"); + try { + restart(); + } catch (IOException e) { + LOG.error("failed to collect java vm arguments", e); + LOG.error("we will now exit the java process"); + System.exit(1); + } + } + + @SuppressWarnings("squid:S1191") // use of sun.* classes is required for jna) + private static void restart() throws IOException { + com.sun.akuma.JavaVMArguments args = com.sun.akuma.JavaVMArguments.current(); + args.remove("--daemon"); + + int sz = LIBC.getdtablesize(); + for(int i=3; i get(ClassLoader webAppClassLoader) { + return Optional.ofNullable(RestartStrategyFactory.create(webAppClassLoader)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java new file mode 100644 index 0000000000..a6fb371aa7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java @@ -0,0 +1,66 @@ +package sonia.scm.lifecycle; + +import com.google.common.base.Strings; +import sonia.scm.PlatformType; +import sonia.scm.util.SystemUtil; + +final class RestartStrategyFactory { + + /** + * System property to load a specific restart strategy. + */ + static final String PROPERTY_STRATEGY = "sonia.scm.lifecycle.restart-strategy"; + + /** + * No restart supported. + */ + static final String STRATEGY_NONE = "none"; + + private RestartStrategyFactory() { + } + + /** + * Returns the configured strategy or {@code null} if restart is not supported. + * + * @param webAppClassLoader root webapp classloader + * @return configured strategy or {@code null} + */ + static RestartStrategy create(ClassLoader webAppClassLoader) { + String property = System.getProperty(PROPERTY_STRATEGY); + if (Strings.isNullOrEmpty(property)) { + return forPlatform(); + } + return fromProperty(webAppClassLoader, property); + } + + private static RestartStrategy fromProperty(ClassLoader webAppClassLoader, String property) { + if (STRATEGY_NONE.equalsIgnoreCase(property)) { + return null; + } else if (ExitRestartStrategy.NAME.equalsIgnoreCase(property)) { + return new ExitRestartStrategy(); + } else if (InjectionContextRestartStrategy.NAME.equalsIgnoreCase(property)) { + return new InjectionContextRestartStrategy(webAppClassLoader); + } else { + return fromClassName(property); + } + } + + private static RestartStrategy fromClassName(String property) { + try { + Class rsClass = Class.forName(property).asSubclass(RestartStrategy.class); + return rsClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new RestartNotSupportedException("failed to create restart strategy from property", e); + } + } + + private static RestartStrategy forPlatform() { + // we do not use SystemUtil here, to allow testing + String osName = System.getProperty(SystemUtil.PROPERTY_OSNAME); + PlatformType platform = PlatformType.createPlatformType(osName); + if (platform.isPosix()) { + return new PosixRestartStrategy(); + } + return null; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java new file mode 100644 index 0000000000..078e218b75 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java @@ -0,0 +1,75 @@ +package sonia.scm.lifecycle; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.function.IntConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ExitRestartStrategyTest { + + @Mock + private RestartStrategy.InjectionContext context; + + private ExitRestartStrategy strategy; + private CapturingExiter exiter; + + @BeforeEach + void setUpStrategy() { + strategy = new ExitRestartStrategy(); + exiter = new CapturingExiter(); + strategy.setExiter(exiter); + } + + @Test + void shouldTearDownContextAndThenExit() { + strategy.restart(context); + + verify(context).destroy(); + assertThat(exiter.getExitCode()).isEqualTo(0); + } + + @Test + void shouldUseExitCodeFromSystemProperty() { + System.setProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE, "42"); + try { + strategy.restart(context); + + verify(context).destroy(); + assertThat(exiter.getExitCode()).isEqualTo(42); + } finally { + System.clearProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE); + } + } + + @Test + void shouldThrowExceptionForNonNumericExitCode() { + System.setProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE, "xyz"); + try { + assertThrows(RestartNotSupportedException.class, () -> strategy.restart(context)); + } finally { + System.clearProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE); + } + } + + private static class CapturingExiter implements IntConsumer { + + private int exitCode = -1; + + public int getExitCode() { + return exitCode; + } + + @Override + public void accept(int exitCode) { + this.exitCode = exitCode; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java new file mode 100644 index 0000000000..914c506aa2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java @@ -0,0 +1,101 @@ +package sonia.scm.lifecycle; + +import com.google.common.base.Strings; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import sonia.scm.util.SystemUtil; + +import java.util.Optional; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RestartStrategyTest { + private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + @Test + void shouldReturnRestartStrategyFromSystemProperty() { + withStrategy(TestingRestartStrategy.class.getName(), (rs) -> { + assertThat(rs).containsInstanceOf(TestingRestartStrategy.class); + }); + } + + @Test + void shouldThrowExceptionForNonStrategyClass() { + withStrategy(RestartStrategyTest.class.getName(), () -> { + assertThrows(RestartNotSupportedException.class, () -> RestartStrategy.get(classLoader)); + }); + } + + @Test + void shouldReturnEmpty() { + withStrategy(RestartStrategyFactory.STRATEGY_NONE, (rs) -> { + assertThat(rs).isEmpty(); + }); + } + + @Test + void shouldReturnEmptyForUnknownOs() { + withSystemProperty(SystemUtil.PROPERTY_OSNAME, "hitchhiker-os", () -> { + Optional restartStrategy = RestartStrategy.get(classLoader); + assertThat(restartStrategy).isEmpty(); + }); + } + + @Test + void shouldReturnExitRestartStrategy() { + withStrategy(ExitRestartStrategy.NAME, (rs) -> { + assertThat(rs).containsInstanceOf(ExitRestartStrategy.class); + }); + } + + @Test + void shouldReturnInjectionContextRestartStrategy() { + withStrategy(InjectionContextRestartStrategy.NAME, (rs) -> { + assertThat(rs).containsInstanceOf(InjectionContextRestartStrategy.class); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "linux", "darwin", "solaris", "freebsd", "openbsd" }) + void shouldReturnPosixRestartStrategyForPosixBased(String os) { + withSystemProperty(SystemUtil.PROPERTY_OSNAME, os, () -> { + Optional restartStrategy = RestartStrategy.get(classLoader); + assertThat(restartStrategy).containsInstanceOf(PosixRestartStrategy.class); + }); + } + + private void withStrategy(String strategy, Consumer> consumer) { + withStrategy(strategy, () -> { + consumer.accept(RestartStrategy.get(classLoader)); + }); + } + + private void withStrategy(String strategy, Runnable runnable) { + withSystemProperty(RestartStrategyFactory.PROPERTY_STRATEGY, strategy, runnable); + } + + private void withSystemProperty(String key, String value, Runnable runnable) { + String oldValue = System.getProperty(key); + System.setProperty(key, value); + try { + runnable.run(); + } finally { + if (Strings.isNullOrEmpty(oldValue)) { + System.clearProperty(key); + } else { + System.setProperty(key, oldValue); + } + } + } + + public static class TestingRestartStrategy implements RestartStrategy { + @Override + public void restart(InjectionContext context) { + + } + } + +}