diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d4bdf80..d7b253df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -## 3.6.1 (2019-15-08) +## 3.6.2 (2020-11-10) + +* Fix JVM hang when System.exit or bugsnag.close is not called + [#157](https://github.com/bugsnag/bugsnag-java/pull/157) + +## 3.6.1 (2019-08-15) * Prevent potential ConcurrentModificationException when adding callback [#149](https://github.com/bugsnag/bugsnag-java/pull/149) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eafceace..76d98e5d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,12 +16,23 @@ Thanks! ## Testing -Runs tests and checkstyle. +### Unit tests and checkstyle ``` ./gradlew check ``` +### End-to-end tests + +These tests are implemented with our notifier testing tool [Maze runner](https://github.com/bugsnag/maze-runner). + +End to end tests are written in cucumber-style `.feature` files, and need Ruby-backed "steps" in order to know what to run. The tests are located in the top level [`features`](/features/) directory. + +``` +bundle install +bundle exec bugsnag-maze-runner +``` + ## Installing/testing against a local maven repository Sometimes its helpful to build and install the bugsnag-java libraries into a diff --git a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java index f8e9c4d0..13846b41 100644 --- a/bugsnag/src/main/java/com/bugsnag/Bugsnag.java +++ b/bugsnag/src/main/java/com/bugsnag/Bugsnag.java @@ -3,6 +3,7 @@ import com.bugsnag.callbacks.Callback; import com.bugsnag.delivery.Delivery; import com.bugsnag.delivery.HttpDelivery; +import com.bugsnag.util.DaemonThreadFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,7 +47,7 @@ public Thread newThread(Runnable runnable) { private ScheduledThreadPoolExecutor sessionExecutorService = new ScheduledThreadPoolExecutor(CORE_POOL_SIZE, - Executors.defaultThreadFactory(), + new DaemonThreadFactory(), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) { diff --git a/bugsnag/src/main/java/com/bugsnag/Notifier.java b/bugsnag/src/main/java/com/bugsnag/Notifier.java index 7456e82c..ac8f6d6c 100644 --- a/bugsnag/src/main/java/com/bugsnag/Notifier.java +++ b/bugsnag/src/main/java/com/bugsnag/Notifier.java @@ -5,7 +5,7 @@ class Notifier { private static final String NOTIFIER_NAME = "Bugsnag Java"; - private static final String NOTIFIER_VERSION = "3.6.1"; + private static final String NOTIFIER_VERSION = "3.6.2"; private static final String NOTIFIER_URL = "https://github.com/bugsnag/bugsnag-java"; private String notifierName = NOTIFIER_NAME; diff --git a/bugsnag/src/main/java/com/bugsnag/util/DaemonThreadFactory.java b/bugsnag/src/main/java/com/bugsnag/util/DaemonThreadFactory.java new file mode 100644 index 00000000..af95dbd6 --- /dev/null +++ b/bugsnag/src/main/java/com/bugsnag/util/DaemonThreadFactory.java @@ -0,0 +1,32 @@ +package com.bugsnag.util; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Wraps {@link Executors#defaultThreadFactory()} to return daemon threads + * This is to prevent applications from hanging waiting for the sessions scheduled task + * because daemon threads will be terminated on application shutdown + */ +public class DaemonThreadFactory implements ThreadFactory { + private final ThreadFactory defaultThreadFactory; + + /** + * Constructor + */ + public DaemonThreadFactory() { + defaultThreadFactory = Executors.defaultThreadFactory(); + } + + @Override + public Thread newThread(Runnable runner) { + Thread daemonThread = defaultThreadFactory.newThread(runner); + daemonThread.setName("bugsnag-daemon-" + daemonThread.getId()); + + // Set the threads to daemon to allow the app to shutdown properly + if (!daemonThread.isDaemon()) { + daemonThread.setDaemon(true); + } + return daemonThread; + } +} diff --git a/bugsnag/src/test/java/com/bugsnag/DaemonThreadFactoryTest.java b/bugsnag/src/test/java/com/bugsnag/DaemonThreadFactoryTest.java new file mode 100644 index 00000000..1ce6fcf0 --- /dev/null +++ b/bugsnag/src/test/java/com/bugsnag/DaemonThreadFactoryTest.java @@ -0,0 +1,33 @@ +package com.bugsnag; + +import static org.junit.Assert.assertTrue; + +import com.bugsnag.util.DaemonThreadFactory; + +import org.junit.Before; +import org.junit.Test; + + +/** + * Tests for the Daemon thread factory internal logic + */ +public class DaemonThreadFactoryTest { + + private DaemonThreadFactory daemonThreadFactory; + + /** + * Create the daemonThreadFactory before the tests + */ + @Before + public void createFactory() { + daemonThreadFactory = new DaemonThreadFactory(); + } + + @Test + public void testDaemonThreadFactory() { + Thread testThread = daemonThreadFactory.newThread(null); + + // Check that the thread is as expected + assertTrue(testThread.isDaemon()); + } +} diff --git a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java index 65c216c4..ec6ba056 100644 --- a/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java +++ b/examples/simple/src/main/java/com/bugsnag/example/simple/ExampleApp.java @@ -74,7 +74,5 @@ public void run() { // Wait for unhandled exception thread to finish before exiting thread.join(); - - System.exit(0); } } diff --git a/features/fixtures/mazerunner/src/main/java/com/bugsnag/mazerunner/TestCaseRunner.java b/features/fixtures/mazerunner/src/main/java/com/bugsnag/mazerunner/TestCaseRunner.java index 88567911..507b1f0b 100644 --- a/features/fixtures/mazerunner/src/main/java/com/bugsnag/mazerunner/TestCaseRunner.java +++ b/features/fixtures/mazerunner/src/main/java/com/bugsnag/mazerunner/TestCaseRunner.java @@ -6,8 +6,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.ExitCodeGenerator; -import org.springframework.boot.SpringApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @@ -41,15 +39,6 @@ public void run(String... args) { } else { LOGGER.error("No test case found for " + System.getenv("EVENT_TYPE")); } - - // Exit the application - LOGGER.info("Exiting spring"); - System.exit(SpringApplication.exit(ctx, (ExitCodeGenerator) new ExitCodeGenerator() { - @Override - public int getExitCode() { - return 0; - } - })); } private void setupBugsnag() { diff --git a/gradle.properties b/gradle.properties index 5446d0e7..9b0305eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.6.1 +version=3.6.2 group=com.bugsnag # Default properties