Skip to content

Commit 5e294cf

Browse files
Merge pull request #121 from bugsnag/fix-application-hang
[PLAT-2110] Use deamon threads to start flushing sessions
2 parents 1ef85e5 + 8421d4f commit 5e294cf

6 files changed

Lines changed: 93 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 3.4.2 (2018-11-28)
4+
5+
* Prevent application hangs due to session flushing
6+
[#121](https://github.com/bugsnag/bugsnag-java/pull/121)
7+
38
## 3.4.1
49

510
(Skipped, duplicate of 3.4.0)

bugsnag/src/main/java/com/bugsnag/Bugsnag.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.bugsnag.callbacks.Callback;
44
import com.bugsnag.delivery.Delivery;
55
import com.bugsnag.delivery.HttpDelivery;
6+
import com.bugsnag.util.DaemonThreadFactory;
67

78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
@@ -12,6 +13,9 @@
1213
import java.util.Collections;
1314
import java.util.Date;
1415
import java.util.Set;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.LinkedBlockingQueue;
1519
import java.util.concurrent.RejectedExecutionHandler;
1620
import java.util.concurrent.ScheduledThreadPoolExecutor;
1721
import java.util.concurrent.ThreadPoolExecutor;
@@ -23,8 +27,17 @@ public class Bugsnag {
2327
private static final int SESSION_TRACKING_PERIOD_MS = 60000;
2428
private static final int CORE_POOL_SIZE = 1;
2529

30+
// Create an executor service which keeps idle threads alive for a maximum of SHUTDOWN_TIMEOUT.
31+
// This should avoid blocking an application that doesn't call shutdown from exiting.
32+
private ExecutorService sessionFlusherService =
33+
new ThreadPoolExecutor(0, 1,
34+
SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS,
35+
new LinkedBlockingQueue<Runnable>());
36+
2637
private ScheduledThreadPoolExecutor sessionExecutorService =
27-
new ScheduledThreadPoolExecutor(CORE_POOL_SIZE, new RejectedExecutionHandler() {
38+
new ScheduledThreadPoolExecutor(CORE_POOL_SIZE,
39+
new DaemonThreadFactory(),
40+
new RejectedExecutionHandler() {
2841
@Override
2942
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
3043
LOGGER.error("Rejected execution for sessionExecutorService");
@@ -81,7 +94,14 @@ private void scheduleSessionFlushes() {
8194
sessionExecutorService.scheduleAtFixedRate(new Runnable() {
8295
@Override
8396
public void run() {
84-
sessionTracker.flushSessions(new Date());
97+
// Use a different thread which is not a daemon thread
98+
// to actually flush the sessions
99+
sessionFlusherService.submit(new Runnable() {
100+
@Override
101+
public void run() {
102+
sessionTracker.flushSessions(new Date());
103+
}
104+
});
85105
}
86106
}, SESSION_TRACKING_PERIOD_MS, SESSION_TRACKING_PERIOD_MS, TimeUnit.MILLISECONDS);
87107
}

bugsnag/src/main/java/com/bugsnag/Notifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
class Notifier {
66

77
private static final String NOTIFIER_NAME = "Bugsnag Java";
8-
private static final String NOTIFIER_VERSION = "3.4.0";
8+
private static final String NOTIFIER_VERSION = "3.4.2";
99
private static final String NOTIFIER_URL = "https://github.com/bugsnag/bugsnag-java";
1010

1111
private String notifierName = NOTIFIER_NAME;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.bugsnag.util;
2+
3+
import java.util.concurrent.Executors;
4+
import java.util.concurrent.ThreadFactory;
5+
6+
/**
7+
* Wraps {@link Executors#defaultThreadFactory()} to return daemon threads
8+
* This is to prevent applications from hanging waiting for the sessions scheduled task
9+
* because daemon threads will be terminated on application shutdown
10+
*/
11+
public class DaemonThreadFactory implements ThreadFactory {
12+
private final ThreadFactory defaultThreadFactory;
13+
14+
/**
15+
* Constructor
16+
*/
17+
public DaemonThreadFactory() {
18+
defaultThreadFactory = Executors.defaultThreadFactory();
19+
}
20+
21+
@Override
22+
public Thread newThread(Runnable runner) {
23+
Thread daemonThread = defaultThreadFactory.newThread(runner);
24+
25+
// Set the threads to daemon to allow the app to shutdown properly
26+
if (!daemonThread.isDaemon()) {
27+
daemonThread.setDaemon(true);
28+
}
29+
return daemonThread;
30+
}
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.bugsnag;
2+
3+
import static org.junit.Assert.assertTrue;
4+
5+
import com.bugsnag.util.DaemonThreadFactory;
6+
7+
import org.junit.Before;
8+
import org.junit.Test;
9+
10+
11+
/**
12+
* Tests for the Daemon thread factory internal logic
13+
*/
14+
public class DaemonThreadFactoryTest {
15+
16+
private DaemonThreadFactory daemonThreadFactory;
17+
18+
/**
19+
* Create the daemonThreadFactory before the tests
20+
*/
21+
@Before
22+
public void createFactory() {
23+
daemonThreadFactory = new DaemonThreadFactory();
24+
}
25+
26+
@Test
27+
public void testDaemonThreadFactory() {
28+
Thread testThread = daemonThreadFactory.newThread(null);
29+
30+
// Check that the thread is as expected
31+
assertTrue(testThread.isDaemon());
32+
}
33+
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version=3.4.1
1+
version=3.4.2
22
group=com.bugsnag
33

44
# Default properties

0 commit comments

Comments
 (0)