From d2a91b77c0e435e864225e2981c7293649667f6e Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 21 Oct 2020 23:06:24 +0200 Subject: [PATCH] Implement a keep-alive strategy to detect stales connections, fixes #47 --- .../jboss/fuse/mvnd/client/ClientLayout.java | 41 ++++++++- .../mvnd/client/DaemonClientConnection.java | 68 ++++++++++++--- .../fuse/mvnd/client/DaemonConnector.java | 30 +++---- .../jboss/fuse/mvnd/client/DefaultClient.java | 87 ++++++++++--------- .../jboss/fuse/mvnd/common/Environment.java | 11 ++- .../org/jboss/fuse/mvnd/common/Message.java | 14 ++- .../mvnd/common/logging/ClientOutput.java | 2 + .../mvnd/common/logging/TerminalOutput.java | 12 ++- .../org/jboss/fuse/mvnd/daemon/Server.java | 29 +++++-- .../jboss/fuse/mvnd/it/DaemonCrashTest.java | 67 ++++++++++++++ .../fuse/mvnd/junit/MvndTestExtension.java | 5 +- .../org/jboss/fuse/mvnd/junit/TestLayout.java | 5 +- .../test/projects/daemon-crash/.mvn/.gitkeep | 0 .../test/projects/daemon-crash/hello/pom.xml | 61 +++++++++++++ .../mvnd/test/multi/module/hello/Hello.java | 24 +++++ .../test/multi/module/hello/HelloTest.java | 38 ++++++++ .../test/projects/daemon-crash/plugin/pom.xml | 46 ++++++++++ .../test/module/plugin/mojo/HelloMojo.java | 43 +++++++++ .../src/test/projects/daemon-crash/pom.xml | 76 ++++++++++++++++ 19 files changed, 572 insertions(+), 87 deletions(-) create mode 100644 integration-tests/src/test/java/org/jboss/fuse/mvnd/it/DaemonCrashTest.java create mode 100644 integration-tests/src/test/projects/daemon-crash/.mvn/.gitkeep create mode 100644 integration-tests/src/test/projects/daemon-crash/hello/pom.xml create mode 100644 integration-tests/src/test/projects/daemon-crash/hello/src/main/java/org/jboss/fuse/mvnd/test/multi/module/hello/Hello.java create mode 100644 integration-tests/src/test/projects/daemon-crash/hello/src/test/java/org/jboss/fuse/mvnd/test/multi/module/hello/HelloTest.java create mode 100644 integration-tests/src/test/projects/daemon-crash/plugin/pom.xml create mode 100644 integration-tests/src/test/projects/daemon-crash/plugin/src/main/java/org/jboss/fuse/mvnd/test/module/plugin/mojo/HelloMojo.java create mode 100644 integration-tests/src/test/projects/daemon-crash/pom.xml diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java b/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java index 053006ca6..febd73824 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/ClientLayout.java @@ -38,6 +38,9 @@ public class ClientLayout extends Layout { private final Path settings; private final Path javaHome; private final Path logbackConfigurationPath; + private final int idleTimeoutMs; + private final int keepAliveMs; + private final int maxLostKeepAlive; public static ClientLayout getEnvInstance() { if (ENV_INSTANCE == null) { @@ -68,6 +71,21 @@ public static ClientLayout getEnvInstance() { .orFail() .asPath() .toAbsolutePath().normalize(); + final int idleTimeoutMs = Environment.DAEMON_IDLE_TIMEOUT_MS + .systemProperty() + .orLocalProperty(mvndProperties, mvndPropertiesPath) + .orDefault(() -> Integer.toString(Environment.DEFAULT_IDLE_TIMEOUT)) + .asInt(); + final int keepAliveMs = Environment.DAEMON_KEEP_ALIVE_MS + .systemProperty() + .orLocalProperty(mvndProperties, mvndPropertiesPath) + .orDefault(() -> Integer.toString(Environment.DEFAULT_KEEP_ALIVE)) + .asInt(); + final int maxLostKeepAlive = Environment.DAEMON_MAX_LOST_KEEP_ALIVE + .systemProperty() + .orLocalProperty(mvndProperties, mvndPropertiesPath) + .orDefault(() -> Integer.toString(Environment.DEFAULT_MAX_LOST_KEEP_ALIVE)) + .asInt(); ENV_INSTANCE = new ClientLayout( mvndPropertiesPath, mvndHome, @@ -76,18 +94,23 @@ public static ClientLayout getEnvInstance() { Environment.findJavaHome(mvndProperties, mvndPropertiesPath), findLocalRepo(), null, - Environment.findLogbackConfigurationPath(mvndProperties, mvndPropertiesPath, mvndHome)); + Environment.findLogbackConfigurationPath(mvndProperties, mvndPropertiesPath, mvndHome), + idleTimeoutMs, keepAliveMs, maxLostKeepAlive); } return ENV_INSTANCE; } public ClientLayout(Path mvndPropertiesPath, Path mavenHome, Path userDir, Path multiModuleProjectDirectory, Path javaHome, - Path localMavenRepository, Path settings, Path logbackConfigurationPath) { + Path localMavenRepository, Path settings, Path logbackConfigurationPath, int idleTimeoutMs, int keepAliveMs, + int maxLostKeepAlive) { super(mvndPropertiesPath, mavenHome, userDir, multiModuleProjectDirectory); this.localMavenRepository = localMavenRepository; this.settings = settings; this.javaHome = Objects.requireNonNull(javaHome, "javaHome"); this.logbackConfigurationPath = logbackConfigurationPath; + this.idleTimeoutMs = idleTimeoutMs; + this.keepAliveMs = keepAliveMs; + this.maxLostKeepAlive = maxLostKeepAlive; } /** @@ -96,7 +119,7 @@ public ClientLayout(Path mvndPropertiesPath, Path mavenHome, Path userDir, Path */ public ClientLayout cd(Path newUserDir) { return new ClientLayout(mvndPropertiesPath, mavenHome, newUserDir, multiModuleProjectDirectory, javaHome, - localMavenRepository, settings, logbackConfigurationPath); + localMavenRepository, settings, logbackConfigurationPath, idleTimeoutMs, keepAliveMs, maxLostKeepAlive); } /** @@ -122,6 +145,18 @@ public Path getLogbackConfigurationPath() { return logbackConfigurationPath; } + public int getIdleTimeoutMs() { + return idleTimeoutMs; + } + + public int getKeepAliveMs() { + return keepAliveMs; + } + + public int getMaxLostKeepAlive() { + return maxLostKeepAlive; + } + static Path findLocalRepo() { return Environment.MAVEN_REPO_LOCAL.systemProperty().asPath(); } diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java b/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java index 0339db9e5..4a0c89921 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonClientConnection.java @@ -15,12 +15,18 @@ */ package org.jboss.fuse.mvnd.client; +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.jboss.fuse.mvnd.common.DaemonConnection; import org.jboss.fuse.mvnd.common.DaemonException; import org.jboss.fuse.mvnd.common.DaemonException.ConnectException; -import org.jboss.fuse.mvnd.common.DaemonException.MessageIOException; import org.jboss.fuse.mvnd.common.DaemonException.StaleAddressException; import org.jboss.fuse.mvnd.common.DaemonInfo; import org.jboss.fuse.mvnd.common.Message; @@ -31,21 +37,31 @@ * File origin: * https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/client/DaemonClientConnection.java */ -public class DaemonClientConnection { +public class DaemonClientConnection implements Closeable { private final static Logger LOG = LoggerFactory.getLogger(DaemonClientConnection.class); private final DaemonConnection connection; private final DaemonInfo daemon; private final StaleAddressDetector staleAddressDetector; + private final boolean newDaemon; private boolean hasReceived; private final Lock dispatchLock = new ReentrantLock(); + private final int maxKeepAliveMs; + private final BlockingQueue queue = new ArrayBlockingQueue<>(16); + private final Thread receiver; + private final AtomicBoolean running = new AtomicBoolean(true); + private final AtomicReference exception = new AtomicReference<>(); public DaemonClientConnection(DaemonConnection connection, DaemonInfo daemon, - StaleAddressDetector staleAddressDetector) { + StaleAddressDetector staleAddressDetector, boolean newDaemon, int maxKeepAliveMs) { this.connection = connection; this.daemon = daemon; this.staleAddressDetector = staleAddressDetector; + this.newDaemon = newDaemon; + this.maxKeepAliveMs = maxKeepAliveMs; + this.receiver = new Thread(this::doReceive); + this.receiver.start(); } public DaemonInfo getDaemon() { @@ -71,22 +87,48 @@ public void dispatch(Message message) throws DaemonException.ConnectException { } } - public Message receive() throws DaemonException.ConnectException { + public Message receive() throws ConnectException, StaleAddressException { + while (true) { + try { + Message m = queue.poll(maxKeepAliveMs, TimeUnit.MILLISECONDS); + Exception e = exception.get(); + if (e != null) { + throw e; + } else if (m != null) { + return m; + } else { + throw new IOException("No message received within " + maxKeepAliveMs + "ms, daemon may have crashed"); + } + } catch (Exception e) { + LOG.debug("Problem receiving message to the daemon. Performing 'on failure' operation..."); + if (!hasReceived && newDaemon) { + throw new ConnectException("Could not receive a message from the daemon.", e); + } else if (staleAddressDetector.maybeStaleAddress(e)) { + throw new StaleAddressException("Could not receive a message from the daemon.", e); + } + } finally { + hasReceived = true; + } + } + } + + protected void doReceive() { try { - return connection.receive(); - } catch (DaemonException.MessageIOException e) { - LOG.debug("Problem receiving message to the daemon. Performing 'on failure' operation..."); - if (!hasReceived && staleAddressDetector.maybeStaleAddress(e)) { - throw new DaemonException.StaleAddressException("Could not receive a message from the daemon.", e); + while (running.get()) { + Message m = connection.receive(); + queue.put(m); + } + } catch (Exception e) { + if (running.get()) { + exception.set(e); } - throw new DaemonException.ConnectException("Could not receive a message from the daemon.", e); - } finally { - hasReceived = true; } } - public void stop() { + public void close() { LOG.debug("thread {}: connection stop", Thread.currentThread().getId()); + running.set(false); + receiver.interrupt(); connection.close(); } diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonConnector.java b/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonConnector.java index 1e80f4b07..e1225cba0 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonConnector.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/DaemonConnector.java @@ -90,7 +90,7 @@ public DaemonClientConnection maybeConnect(DaemonCompatibilitySpec constraint) { public DaemonClientConnection maybeConnect(DaemonInfo daemon) { try { - return connectToDaemon(daemon, new CleanupOnStaleAddress(daemon, true)); + return connectToDaemon(daemon, new CleanupOnStaleAddress(daemon), false); } catch (DaemonException.ConnectException e) { LOGGER.debug("Cannot connect to daemon {} due to {}. Ignoring.", daemon, e); } @@ -225,7 +225,7 @@ private List getCompatibleDaemons(Iterable daemons, Daem private DaemonClientConnection findConnection(List compatibleDaemons) { for (DaemonInfo daemon : compatibleDaemons) { try { - return connectToDaemon(daemon, new CleanupOnStaleAddress(daemon, true)); + return connectToDaemon(daemon, new CleanupOnStaleAddress(daemon), false); } catch (DaemonException.ConnectException e) { LOGGER.debug("Cannot connect to daemon {} due to {}. Trying a different daemon...", daemon, e); } @@ -238,7 +238,7 @@ public DaemonClientConnection startDaemon(DaemonCompatibilitySpec constraint) { LOGGER.debug("Started Maven daemon {}", daemon); long start = System.currentTimeMillis(); do { - DaemonClientConnection daemonConnection = connectToDaemonWithId(daemon); + DaemonClientConnection daemonConnection = connectToDaemonWithId(daemon, true); if (daemonConnection != null) { return daemonConnection; } @@ -277,11 +277,8 @@ private String startDaemon() { args.add("-Dlogback.configurationFile=" + layout.getLogbackConfigurationPath()); args.add("-Ddaemon.uid=" + uid); args.add("-Xmx4g"); - final String timeout = Environment.DAEMON_IDLE_TIMEOUT.systemProperty().asString(); - if (timeout != null) { - args.add(Environment.DAEMON_IDLE_TIMEOUT.asCommandLineProperty(timeout)); - } - + args.add(Environment.DAEMON_IDLE_TIMEOUT_MS.asCommandLineProperty(Integer.toString(layout.getIdleTimeoutMs()))); + args.add(Environment.DAEMON_KEEP_ALIVE_MS.asCommandLineProperty(Integer.toString(layout.getKeepAliveMs()))); args.add(MavenDaemon.class.getName()); command = String.join(" ", args); @@ -314,13 +311,14 @@ private String findJars(Path mavenHome, Predicate... filters) { } } - private DaemonClientConnection connectToDaemonWithId(String daemon) throws DaemonException.ConnectException { + private DaemonClientConnection connectToDaemonWithId(String daemon, boolean newDaemon) + throws DaemonException.ConnectException { // Look for 'our' daemon among the busy daemons - a daemon will start in busy state so that nobody else will // grab it. DaemonInfo daemonInfo = registry.get(daemon); if (daemonInfo != null) { try { - return connectToDaemon(daemonInfo, new CleanupOnStaleAddress(daemonInfo, false)); + return connectToDaemon(daemonInfo, new CleanupOnStaleAddress(daemonInfo), newDaemon); } catch (DaemonException.ConnectException e) { DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout.daemonLog(daemon)); throw new DaemonException.ConnectException("Could not connect to the Maven daemon.\n" + diag.describe(), e); @@ -330,11 +328,13 @@ private DaemonClientConnection connectToDaemonWithId(String daemon) throws Daemo } private DaemonClientConnection connectToDaemon(DaemonInfo daemon, - DaemonClientConnection.StaleAddressDetector staleAddressDetector) throws DaemonException.ConnectException { + DaemonClientConnection.StaleAddressDetector staleAddressDetector, boolean newDaemon) + throws DaemonException.ConnectException { LOGGER.debug("Connecting to Daemon"); try { + int maxKeepAliveMs = layout.getKeepAliveMs() * layout.getMaxLostKeepAlive(); DaemonConnection connection = connect(daemon.getAddress()); - return new DaemonClientConnection(connection, daemon, staleAddressDetector); + return new DaemonClientConnection(connection, daemon, staleAddressDetector, newDaemon, maxKeepAliveMs); } catch (DaemonException.ConnectException e) { staleAddressDetector.maybeStaleAddress(e); throw e; @@ -345,11 +345,9 @@ private DaemonClientConnection connectToDaemon(DaemonInfo daemon, private class CleanupOnStaleAddress implements DaemonClientConnection.StaleAddressDetector { private final DaemonInfo daemon; - private final boolean exposeAsStale; - public CleanupOnStaleAddress(DaemonInfo daemon, boolean exposeAsStale) { + public CleanupOnStaleAddress(DaemonInfo daemon) { this.daemon = daemon; - this.exposeAsStale = exposeAsStale; } @Override @@ -360,7 +358,7 @@ public boolean maybeStaleAddress(Exception failure) { "by user or operating system"); registry.storeStopEvent(stopEvent); registry.remove(daemon.getUid()); - return exposeAsStale; + return true; } } diff --git a/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java b/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java index e2d797815..6b5978759 100644 --- a/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java +++ b/client/src/main/java/org/jboss/fuse/mvnd/client/DefaultClient.java @@ -35,6 +35,7 @@ import org.jboss.fuse.mvnd.common.Message.BuildEvent; import org.jboss.fuse.mvnd.common.Message.BuildException; import org.jboss.fuse.mvnd.common.Message.BuildMessage; +import org.jboss.fuse.mvnd.common.Message.KeepAliveMessage; import org.jboss.fuse.mvnd.common.Message.MessageSerializer; import org.jboss.fuse.mvnd.common.logging.ClientOutput; import org.jboss.fuse.mvnd.common.logging.TerminalOutput; @@ -104,8 +105,6 @@ public ExecutionResult execute(ClientOutput output, List argv) { debug = true; args.add(arg); break; - case "--install": - throw new IllegalStateException("The --install option was removed in mvnd 0.0.2"); default: if (arg.startsWith("-D")) { final int eqPos = arg.indexOf('='); @@ -184,50 +183,54 @@ public ExecutionResult execute(ClientOutput output, List argv) { final DaemonConnector connector = new DaemonConnector(layout, registry, buildProperties, new MessageSerializer()); List opts = new ArrayList<>(); - DaemonClientConnection daemon = connector.connect(new DaemonCompatibilitySpec(javaHome, opts), - s -> output.accept(null, s)); + try (DaemonClientConnection daemon = connector.connect(new DaemonCompatibilitySpec(javaHome, opts), + s -> output.accept(null, s))) { - daemon.dispatch(new Message.BuildRequest( - args, - layout.userDir().toString(), - layout.multiModuleProjectDirectory().toString(), System.getenv())); + daemon.dispatch(new Message.BuildRequest( + args, + layout.userDir().toString(), + layout.multiModuleProjectDirectory().toString(), + System.getenv())); - while (true) { - Message m = daemon.receive(); - if (m instanceof BuildException) { - final BuildException e = (BuildException) m; - output.error(e.getMessage(), e.getClassName(), e.getStackTrace()); - return new DefaultResult(argv, - new Exception(e.getClassName() + ": " + e.getMessage() + "\n" + e.getStackTrace())); - } else if (m instanceof BuildEvent) { - BuildEvent be = (BuildEvent) m; - switch (be.getType()) { - case BuildStarted: - int projects = 0; - int cores = 0; - Properties props = new Properties(); - try { - props.load(new StringReader(be.getDisplay())); - projects = Integer.parseInt(props.getProperty("projects")); - cores = Integer.parseInt(props.getProperty("cores")); - } catch (Exception e) { - // Ignore + while (true) { + Message m = daemon.receive(); + if (m instanceof BuildException) { + final BuildException e = (BuildException) m; + output.error(e.getMessage(), e.getClassName(), e.getStackTrace()); + return new DefaultResult(argv, + new Exception(e.getClassName() + ": " + e.getMessage() + "\n" + e.getStackTrace())); + } else if (m instanceof BuildEvent) { + BuildEvent be = (BuildEvent) m; + switch (be.getType()) { + case BuildStarted: + int projects = 0; + int cores = 0; + Properties props = new Properties(); + try { + props.load(new StringReader(be.getDisplay())); + projects = Integer.parseInt(props.getProperty("projects")); + cores = Integer.parseInt(props.getProperty("cores")); + } catch (Exception e) { + // Ignore + } + output.startBuild(be.getProjectId(), projects, cores); + break; + case BuildStopped: + return new DefaultResult(argv, null); + case ProjectStarted: + case MojoStarted: + output.projectStateChanged(be.getProjectId(), be.getDisplay()); + break; + case ProjectStopped: + output.projectFinished(be.getProjectId()); + break; } - output.startBuild(be.getProjectId(), projects, cores); - break; - case BuildStopped: - return new DefaultResult(argv, null); - case ProjectStarted: - case MojoStarted: - output.projectStateChanged(be.getProjectId(), be.getDisplay()); - break; - case ProjectStopped: - output.projectFinished(be.getProjectId()); - break; + } else if (m instanceof BuildMessage) { + BuildMessage bm = (BuildMessage) m; + output.accept(bm.getProjectId(), bm.getMessage()); + } else if (m instanceof KeepAliveMessage) { + output.keepAlive(); } - } else if (m instanceof BuildMessage) { - BuildMessage bm = (BuildMessage) m; - output.accept(bm.getProjectId(), bm.getMessage()); } } } diff --git a/common/src/main/java/org/jboss/fuse/mvnd/common/Environment.java b/common/src/main/java/org/jboss/fuse/mvnd/common/Environment.java index 834a10c63..a58d57acf 100644 --- a/common/src/main/java/org/jboss/fuse/mvnd/common/Environment.java +++ b/common/src/main/java/org/jboss/fuse/mvnd/common/Environment.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Optional; import java.util.Properties; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; import org.slf4j.Logger; @@ -39,9 +40,17 @@ public enum Environment { MAVEN_MULTIMODULE_PROJECT_DIRECTORY("maven.multiModuleProjectDirectory", null), MVND_PROPERTIES_PATH("mvnd.properties.path", "MVND_PROPERTIES_PATH"), DAEMON_DEBUG("daemon.debug", null), - DAEMON_IDLE_TIMEOUT("daemon.idleTimeout", null), + DAEMON_IDLE_TIMEOUT_MS("daemon.idleTimeoutMs", null), + DAEMON_KEEP_ALIVE_MS("daemon.keepAliveMs", null), + DAEMON_MAX_LOST_KEEP_ALIVE("daemon.maxLostKeepAlive", null), DAEMON_UID("daemon.uid", null); + public static final int DEFAULT_IDLE_TIMEOUT = (int) TimeUnit.HOURS.toMillis(3); + + public static final int DEFAULT_KEEP_ALIVE = (int) TimeUnit.SECONDS.toMillis(1); + + public static final int DEFAULT_MAX_LOST_KEEP_ALIVE = 3; + private static final Logger LOG = LoggerFactory.getLogger(Environment.class); static Properties properties = System.getProperties(); static Map env = System.getenv(); diff --git a/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java b/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java index b9d6536e6..bce6caaa3 100644 --- a/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java +++ b/common/src/main/java/org/jboss/fuse/mvnd/common/Message.java @@ -83,7 +83,7 @@ public BuildException(Throwable t) { this(t.getMessage(), t.getClass().getName(), getStackTrace(t)); } - static String getStackTrace(Throwable t) { + public static String getStackTrace(Throwable t) { StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw, true)); return sw.toString(); @@ -180,12 +180,20 @@ public String toString() { } } + public static class KeepAliveMessage extends Message { + @Override + public String toString() { + return "KeepAliveMessage{}"; + } + } + public static class MessageSerializer implements Serializer { final int BUILD_REQUEST = 0; final int BUILD_EVENT = 1; final int BUILD_MESSAGE = 2; final int BUILD_EXCEPTION = 3; + final int KEEP_ALIVE = 4; @Override public Message read(DataInputStream input) throws EOFException, Exception { @@ -202,6 +210,8 @@ public Message read(DataInputStream input) throws EOFException, Exception { return readBuildMessage(input); case BUILD_EXCEPTION: return readBuildException(input); + case KEEP_ALIVE: + return new KeepAliveMessage(); } throw new IllegalStateException("Unexpected message type: " + type); } @@ -220,6 +230,8 @@ public void write(DataOutputStream output, Message value) throws Exception { } else if (value instanceof BuildException) { output.write(BUILD_EXCEPTION); writeBuildException(output, (BuildException) value); + } else if (value instanceof KeepAliveMessage) { + output.write(KEEP_ALIVE); } else { throw new IllegalStateException(); } diff --git a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java index 6327f8273..0bcf4325a 100644 --- a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java +++ b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/ClientOutput.java @@ -30,4 +30,6 @@ public interface ClientOutput extends AutoCloseable { void error(String message, String className, String stackTrace); + void keepAlive(); + } diff --git a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java index 425101052..c3d364c6a 100644 --- a/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java +++ b/common/src/main/java/org/jboss/fuse/mvnd/common/logging/TerminalOutput.java @@ -76,7 +76,8 @@ enum EventType { LOG, ERROR, END_OF_STREAM, - INPUT + INPUT, + KEEP_ALIVE } static class Event { @@ -168,6 +169,15 @@ public void error(String message, String className, String stackTrace) { } } + @Override + public void keepAlive() { + try { + queue.put(new Event(EventType.KEEP_ALIVE, null, null)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + void readInputLoop() { try { while (!closing) { diff --git a/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java index 7fd464cec..a88acd638 100644 --- a/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java +++ b/daemon/src/main/java/org/jboss/fuse/mvnd/daemon/Server.java @@ -57,6 +57,7 @@ import org.jboss.fuse.mvnd.common.Message.BuildException; import org.jboss.fuse.mvnd.common.Message.BuildMessage; import org.jboss.fuse.mvnd.common.Message.BuildRequest; +import org.jboss.fuse.mvnd.common.Message.KeepAliveMessage; import org.jboss.fuse.mvnd.common.Message.MessageSerializer; import org.jboss.fuse.mvnd.daemon.DaemonExpiration.DaemonExpirationResult; import org.jboss.fuse.mvnd.daemon.DaemonExpiration.DaemonExpirationStrategy; @@ -72,7 +73,6 @@ public class Server implements AutoCloseable, Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(Server.class); public static final int CANCEL_TIMEOUT = 10 * 1000; - public static final int DEFAULT_IDLE_TIMEOUT = (int) TimeUnit.HOURS.toMillis(3); private final String uid; private final ServerSocketChannel socket; @@ -96,9 +96,9 @@ public Server(String uid) throws IOException { registry = new DaemonRegistry(layout.registry()); socket = ServerSocketChannel.open().bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); - final int idleTimeout = Environment.DAEMON_IDLE_TIMEOUT + final int idleTimeout = Environment.DAEMON_IDLE_TIMEOUT_MS .systemProperty() - .orDefault(() -> String.valueOf(DEFAULT_IDLE_TIMEOUT)) + .orDefault(() -> String.valueOf(Environment.DEFAULT_IDLE_TIMEOUT)) .asInt(); executor = Executors.newScheduledThreadPool(1); strategy = DaemonExpiration.master(); @@ -389,6 +389,8 @@ private void cancelNow() { private void handle(DaemonConnection connection, BuildRequest buildRequest) { updateState(Busy); try { + int keepAlive = Environment.DAEMON_KEEP_ALIVE_MS.systemProperty().asInt(); + LOGGER.info("Executing request"); CliRequest req = new CliRequestBuilder() .arguments(buildRequest.getArgs()) @@ -403,11 +405,22 @@ private void handle(DaemonConnection connection, BuildRequest buildRequ AbstractLoggingSpy.instance(loggingSpy); Thread pumper = new Thread(() -> { try { + boolean flushed = true; while (true) { - Message m = queue.poll(); - if (m == null) { - connection.flush(); - m = queue.take(); + Message m; + if (flushed) { + m = queue.poll(keepAlive, TimeUnit.MILLISECONDS); + if (m == null) { + m = new KeepAliveMessage(); + } + flushed = false; + } else { + m = queue.poll(); + if (m == null) { + connection.flush(); + flushed = true; + continue; + } } if (m == STOP) { connection.flush(); @@ -462,6 +475,8 @@ int getClassOrder(Message m) { return 97; } else if (m == STOP) { return 99; + } else if (m instanceof KeepAliveMessage) { + return 100; } else { throw new IllegalStateException(); } diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/DaemonCrashTest.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/DaemonCrashTest.java new file mode 100644 index 000000000..fab38dafc --- /dev/null +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/it/DaemonCrashTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed 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.jboss.fuse.mvnd.it; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import javax.inject.Inject; + +import org.assertj.core.api.Assertions; +import org.jboss.fuse.mvnd.client.Client; +import org.jboss.fuse.mvnd.client.ClientLayout; +import org.jboss.fuse.mvnd.common.DaemonException; +import org.jboss.fuse.mvnd.common.logging.ClientOutput; +import org.jboss.fuse.mvnd.junit.MvndTest; +import org.jboss.fuse.mvnd.junit.TestUtils; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@MvndTest(projectDir = "src/test/projects/daemon-crash") +public class DaemonCrashTest { + + @Inject + Client client; + + @Inject + ClientLayout layout; + + @Test + void cleanInstall() throws IOException, InterruptedException { + final Path helloPath = layout.multiModuleProjectDirectory().resolve("hello/target/hello.txt"); + try { + Files.deleteIfExists(helloPath); + } catch (IOException e) { + throw new RuntimeException("Could not delete " + helloPath); + } + + final Path localMavenRepo = layout.getLocalMavenRepository(); + TestUtils.deleteDir(localMavenRepo); + final Path[] installedJars = { + localMavenRepo.resolve( + "org/jboss/fuse/mvnd/test/daemon-crash/daemon-crash-maven-plugin/0.0.1-SNAPSHOT/daemon-crash-maven-plugin-0.0.1-SNAPSHOT.jar"), + }; + Stream.of(installedJars).forEach(jar -> Assertions.assertThat(jar).doesNotExist()); + + final ClientOutput output = Mockito.mock(ClientOutput.class); + assertThrows(DaemonException.StaleAddressException.class, + () -> client.execute(output, "clean", "install", "-e", "-Dmvnd.log.level=DEBUG").assertFailure()); + } +} diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java index c2b1ff64b..8b30209d0 100644 --- a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/MvndTestExtension.java @@ -203,7 +203,10 @@ public static MvndResource create(String className, String rawProjectDir, boolea multiModuleProjectDirectory, Paths.get(System.getProperty("java.home")).toAbsolutePath().normalize(), localMavenRepository, settingsPath, - logback); + logback, + Environment.DEFAULT_IDLE_TIMEOUT, + Environment.DEFAULT_KEEP_ALIVE, + Environment.DEFAULT_MAX_LOST_KEEP_ALIVE); final TestRegistry registry = new TestRegistry(layout.registry()); return new MvndResource(layout, registry, isNative, timeoutMs); diff --git a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/TestLayout.java b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/TestLayout.java index 92ce570c6..5353ab39f 100644 --- a/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/TestLayout.java +++ b/integration-tests/src/test/java/org/jboss/fuse/mvnd/junit/TestLayout.java @@ -22,9 +22,10 @@ public class TestLayout extends ClientLayout { private final Path testDir; public TestLayout(Path testDir, Path mvndPropertiesPath, Path mavenHome, Path userDir, Path multiModuleProjectDirectory, - Path javaHome, Path localMavenRepository, Path settings, Path logbackConfigurationPath) { + Path javaHome, Path localMavenRepository, Path settings, Path logbackConfigurationPath, + int idleTimeout, int keepAlive, int maxLostKeepAlive) { super(mvndPropertiesPath, mavenHome, userDir, multiModuleProjectDirectory, javaHome, localMavenRepository, - settings, logbackConfigurationPath); + settings, logbackConfigurationPath, idleTimeout, keepAlive, maxLostKeepAlive); this.testDir = testDir; } diff --git a/integration-tests/src/test/projects/daemon-crash/.mvn/.gitkeep b/integration-tests/src/test/projects/daemon-crash/.mvn/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/integration-tests/src/test/projects/daemon-crash/hello/pom.xml b/integration-tests/src/test/projects/daemon-crash/hello/pom.xml new file mode 100644 index 000000000..8b92a745d --- /dev/null +++ b/integration-tests/src/test/projects/daemon-crash/hello/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + org.jboss.fuse.mvnd.test.daemon-crash + daemon-crash + 0.0.1-SNAPSHOT + ../pom.xml + + + module-and-plugin-hello + + + + org.junit.jupiter + junit-jupiter-engine + 5.6.2 + test + + + + + + + org.jboss.fuse.mvnd.test.daemon-crash + daemon-crash-maven-plugin + 0.0.1-SNAPSHOT + + + hello + + hello + + compile + + ${basedir}/target/hello.txt + + + + + + + + \ No newline at end of file diff --git a/integration-tests/src/test/projects/daemon-crash/hello/src/main/java/org/jboss/fuse/mvnd/test/multi/module/hello/Hello.java b/integration-tests/src/test/projects/daemon-crash/hello/src/main/java/org/jboss/fuse/mvnd/test/multi/module/hello/Hello.java new file mode 100644 index 000000000..8e45d1aaa --- /dev/null +++ b/integration-tests/src/test/projects/daemon-crash/hello/src/main/java/org/jboss/fuse/mvnd/test/multi/module/hello/Hello.java @@ -0,0 +1,24 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed 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.jboss.fuse.mvnd.test.multi.module.hello; + +public class Hello { + + public String greet() { + return "Hello"; + } + +} diff --git a/integration-tests/src/test/projects/daemon-crash/hello/src/test/java/org/jboss/fuse/mvnd/test/multi/module/hello/HelloTest.java b/integration-tests/src/test/projects/daemon-crash/hello/src/test/java/org/jboss/fuse/mvnd/test/multi/module/hello/HelloTest.java new file mode 100644 index 000000000..099e3cce8 --- /dev/null +++ b/integration-tests/src/test/projects/daemon-crash/hello/src/test/java/org/jboss/fuse/mvnd/test/multi/module/hello/HelloTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed 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.jboss.fuse.mvnd.test.multi.module.hello; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class HelloTest { + + @Test + void greet() throws IOException { + final String actual = new Hello().greet(); + Assertions.assertEquals("Hello", actual); + + /* Make sure the plugin was run before this test */ + final String content = new String(Files.readAllBytes(Paths.get("target/hello.txt")), StandardCharsets.UTF_8); + Assertions.assertTrue("Hi".equals(content) || "Hello".equals(content)); + } + +} diff --git a/integration-tests/src/test/projects/daemon-crash/plugin/pom.xml b/integration-tests/src/test/projects/daemon-crash/plugin/pom.xml new file mode 100644 index 000000000..3f0ba9482 --- /dev/null +++ b/integration-tests/src/test/projects/daemon-crash/plugin/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.jboss.fuse.mvnd.test.daemon-crash + daemon-crash + 0.0.1-SNAPSHOT + ../pom.xml + + + daemon-crash-maven-plugin + maven-plugin + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + provided + 3.5 + + + org.apache.maven + maven-plugin-api + 3.6.0 + + + + + \ No newline at end of file diff --git a/integration-tests/src/test/projects/daemon-crash/plugin/src/main/java/org/jboss/fuse/mvnd/test/module/plugin/mojo/HelloMojo.java b/integration-tests/src/test/projects/daemon-crash/plugin/src/main/java/org/jboss/fuse/mvnd/test/module/plugin/mojo/HelloMojo.java new file mode 100644 index 000000000..92cff772f --- /dev/null +++ b/integration-tests/src/test/projects/daemon-crash/plugin/src/main/java/org/jboss/fuse/mvnd/test/module/plugin/mojo/HelloMojo.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed 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.jboss.fuse.mvnd.test.module.plugin.mojo; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +/** + */ +@Mojo(name = "hello", requiresProject = true) +public class HelloMojo extends AbstractMojo { + + @Parameter + File file; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + System.exit(-1); + } + +} diff --git a/integration-tests/src/test/projects/daemon-crash/pom.xml b/integration-tests/src/test/projects/daemon-crash/pom.xml new file mode 100644 index 000000000..aa94087da --- /dev/null +++ b/integration-tests/src/test/projects/daemon-crash/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + org.jboss.fuse.mvnd.test.daemon-crash + daemon-crash + 0.0.1-SNAPSHOT + pom + + + UTF-8 + 1.8 + 1.8 + + 2.5 + 3.8.0 + 2.4 + 2.6 + 2.22.2 + + + + hello + plugin + + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + + + \ No newline at end of file