Skip to content

Commit 2bfacef

Browse files
authored
Allow multiple UncaughtExceptionHandlerIntegrations to be active (#4462)
* allow multiple UncaughtExceptionHandlerIntegrations to be active. one per GlobalScope * Add changelog entry * add comments to close and removeFromHandlerTree methods * add initial cycle detection * make recursive method more readable * add test for cycle detection when trying to remove a handler
1 parent 0ceb6bf commit 2bfacef

File tree

3 files changed

+310
-22
lines changed

3 files changed

+310
-22
lines changed

CHANGELOG.md

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

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Allow multiple UncaughtExceptionHandlerIntegrations to be active at the same time ([#4462](https://github.com/getsentry/sentry-java/pull/4462))
8+
39
## 8.17.0
410

511
### Features

sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
import io.sentry.hints.TransactionEnd;
1111
import io.sentry.protocol.Mechanism;
1212
import io.sentry.protocol.SentryId;
13+
import io.sentry.util.AutoClosableReentrantLock;
1314
import io.sentry.util.HintUtils;
1415
import io.sentry.util.Objects;
1516
import java.io.Closeable;
17+
import java.util.HashSet;
18+
import java.util.Set;
1619
import java.util.concurrent.atomic.AtomicReference;
1720
import org.jetbrains.annotations.ApiStatus;
1821
import org.jetbrains.annotations.NotNull;
@@ -28,6 +31,8 @@ public final class UncaughtExceptionHandlerIntegration
2831
/** Reference to the pre-existing uncaught exception handler. */
2932
private @Nullable Thread.UncaughtExceptionHandler defaultExceptionHandler;
3033

34+
private static final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
35+
3136
private @Nullable IScopes scopes;
3237
private @Nullable SentryOptions options;
3338

@@ -65,27 +70,33 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO
6570
this.options.isEnableUncaughtExceptionHandler());
6671

6772
if (this.options.isEnableUncaughtExceptionHandler()) {
68-
final Thread.UncaughtExceptionHandler currentHandler =
69-
threadAdapter.getDefaultUncaughtExceptionHandler();
70-
if (currentHandler != null) {
71-
this.options
72-
.getLogger()
73-
.log(
74-
SentryLevel.DEBUG,
75-
"default UncaughtExceptionHandler class='"
76-
+ currentHandler.getClass().getName()
77-
+ "'");
78-
79-
if (currentHandler instanceof UncaughtExceptionHandlerIntegration) {
80-
final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
81-
(UncaughtExceptionHandlerIntegration) currentHandler;
82-
defaultExceptionHandler = currentHandlerIntegration.defaultExceptionHandler;
83-
} else {
84-
defaultExceptionHandler = currentHandler;
73+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
74+
final Thread.UncaughtExceptionHandler currentHandler =
75+
threadAdapter.getDefaultUncaughtExceptionHandler();
76+
if (currentHandler != null) {
77+
this.options
78+
.getLogger()
79+
.log(
80+
SentryLevel.DEBUG,
81+
"default UncaughtExceptionHandler class='"
82+
+ currentHandler.getClass().getName()
83+
+ "'");
84+
if (currentHandler instanceof UncaughtExceptionHandlerIntegration) {
85+
final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
86+
(UncaughtExceptionHandlerIntegration) currentHandler;
87+
if (currentHandlerIntegration.scopes != null
88+
&& scopes.getGlobalScope() == currentHandlerIntegration.scopes.getGlobalScope()) {
89+
defaultExceptionHandler = currentHandlerIntegration.defaultExceptionHandler;
90+
} else {
91+
defaultExceptionHandler = currentHandler;
92+
}
93+
} else {
94+
defaultExceptionHandler = currentHandler;
95+
}
8596
}
86-
}
8797

88-
threadAdapter.setDefaultUncaughtExceptionHandler(this);
98+
threadAdapter.setDefaultUncaughtExceptionHandler(this);
99+
}
89100

90101
this.options
91102
.getLogger()
@@ -157,14 +168,88 @@ static Throwable getUnhandledThrowable(
157168
return new ExceptionMechanismException(mechanism, thrown, thread);
158169
}
159170

171+
/**
172+
* Remove this UncaughtExceptionHandlerIntegration from the exception handler chain.
173+
*
174+
* <p>If this integration is currently the default handler, restore the initial handler, if this
175+
* integration is not the current default call removeFromHandlerTree
176+
*/
160177
@Override
161178
public void close() {
162-
if (this == threadAdapter.getDefaultUncaughtExceptionHandler()) {
163-
threadAdapter.setDefaultUncaughtExceptionHandler(defaultExceptionHandler);
179+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
180+
if (this == threadAdapter.getDefaultUncaughtExceptionHandler()) {
181+
threadAdapter.setDefaultUncaughtExceptionHandler(defaultExceptionHandler);
182+
183+
if (options != null) {
184+
options
185+
.getLogger()
186+
.log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.");
187+
}
188+
} else {
189+
removeFromHandlerTree(threadAdapter.getDefaultUncaughtExceptionHandler());
190+
}
191+
}
192+
}
193+
194+
/**
195+
* Intermediary method before calling the actual recursive method. Used to initialize HashSet to
196+
* keep track of visited handlers to avoid infinite recursion in case of cycles in the chain.
197+
*/
198+
private void removeFromHandlerTree(@Nullable Thread.UncaughtExceptionHandler currentHandler) {
199+
removeFromHandlerTree(currentHandler, new HashSet<>());
200+
}
201+
202+
/**
203+
* Recursively traverses the chain of UncaughtExceptionHandlerIntegrations to find and remove this
204+
* specific integration instance.
205+
*
206+
* <p>Checks if this instance is the defaultExceptionHandler of the current handler, if so replace
207+
* with its own defaultExceptionHandler, thus removing it from the chain.
208+
*
209+
* <p>If not, recursively calls itself on the next handler in the chain.
210+
*
211+
* <p>Recursion stops if the current handler is not an instance of
212+
* UncaughtExceptionHandlerIntegration, the handler was found and removed or a cycle was detected.
213+
*
214+
* @param currentHandler The current handler in the chain to examine
215+
* @param visited Set of already visited handlers to detect cycles
216+
*/
217+
private void removeFromHandlerTree(
218+
@Nullable Thread.UncaughtExceptionHandler currentHandler,
219+
@NotNull Set<Thread.UncaughtExceptionHandler> visited) {
220+
221+
if (currentHandler == null) {
222+
if (options != null) {
223+
options.getLogger().log(SentryLevel.DEBUG, "Found no UncaughtExceptionHandler to remove.");
224+
}
225+
return;
226+
}
227+
228+
if (!visited.add(currentHandler)) {
229+
if (options != null) {
230+
options
231+
.getLogger()
232+
.log(
233+
SentryLevel.WARNING,
234+
"Cycle detected in UncaughtExceptionHandler chain while removing handler.");
235+
}
236+
return;
237+
}
238+
239+
if (!(currentHandler instanceof UncaughtExceptionHandlerIntegration)) {
240+
return;
241+
}
242+
243+
final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
244+
(UncaughtExceptionHandlerIntegration) currentHandler;
164245

246+
if (this == currentHandlerIntegration.defaultExceptionHandler) {
247+
currentHandlerIntegration.defaultExceptionHandler = defaultExceptionHandler;
165248
if (options != null) {
166249
options.getLogger().log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.");
167250
}
251+
} else {
252+
removeFromHandlerTree(currentHandlerIntegration.defaultExceptionHandler, visited);
168253
}
169254
}
170255

0 commit comments

Comments
 (0)