Skip to content

Commit 3c93c16

Browse files
committed
allow multiple UncaughtExceptionHandlerIntegrations to be active. one per GlobalScope
1 parent 604f1f4 commit 3c93c16

2 files changed

Lines changed: 181 additions & 24 deletions

File tree

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

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
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;
@@ -28,6 +29,8 @@ public final class UncaughtExceptionHandlerIntegration
2829
/** Reference to the pre-existing uncaught exception handler. */
2930
private @Nullable Thread.UncaughtExceptionHandler defaultExceptionHandler;
3031

32+
private static final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
33+
3134
private @Nullable IScopes scopes;
3235
private @Nullable SentryOptions options;
3336

@@ -65,27 +68,33 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO
6568
this.options.isEnableUncaughtExceptionHandler());
6669

6770
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;
71+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
72+
final Thread.UncaughtExceptionHandler currentHandler =
73+
threadAdapter.getDefaultUncaughtExceptionHandler();
74+
if (currentHandler != null) {
75+
this.options
76+
.getLogger()
77+
.log(
78+
SentryLevel.DEBUG,
79+
"default UncaughtExceptionHandler class='"
80+
+ currentHandler.getClass().getName()
81+
+ "'");
82+
if (currentHandler instanceof UncaughtExceptionHandlerIntegration) {
83+
final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
84+
(UncaughtExceptionHandlerIntegration) currentHandler;
85+
if (currentHandlerIntegration.scopes != null
86+
&& scopes.getGlobalScope() == currentHandlerIntegration.scopes.getGlobalScope()) {
87+
defaultExceptionHandler = currentHandlerIntegration.defaultExceptionHandler;
88+
} else {
89+
defaultExceptionHandler = currentHandler;
90+
}
91+
} else {
92+
defaultExceptionHandler = currentHandler;
93+
}
8594
}
86-
}
8795

88-
threadAdapter.setDefaultUncaughtExceptionHandler(this);
96+
threadAdapter.setDefaultUncaughtExceptionHandler(this);
97+
}
8998

9099
this.options
91100
.getLogger()
@@ -159,11 +168,36 @@ static Throwable getUnhandledThrowable(
159168

160169
@Override
161170
public void close() {
162-
if (this == threadAdapter.getDefaultUncaughtExceptionHandler()) {
163-
threadAdapter.setDefaultUncaughtExceptionHandler(defaultExceptionHandler);
171+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
172+
if (this == threadAdapter.getDefaultUncaughtExceptionHandler()) {
173+
threadAdapter.setDefaultUncaughtExceptionHandler(defaultExceptionHandler);
174+
175+
if (options != null) {
176+
options
177+
.getLogger()
178+
.log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.");
179+
}
180+
} else {
181+
removeFromHandlerTree(threadAdapter.getDefaultUncaughtExceptionHandler());
182+
}
183+
}
184+
}
164185

165-
if (options != null) {
166-
options.getLogger().log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.");
186+
private void removeFromHandlerTree(@Nullable Thread.UncaughtExceptionHandler currentHandler) {
187+
if (currentHandler instanceof UncaughtExceptionHandlerIntegration) {
188+
final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
189+
(UncaughtExceptionHandlerIntegration) currentHandler;
190+
if (this == currentHandlerIntegration.defaultExceptionHandler) {
191+
currentHandlerIntegration.defaultExceptionHandler = defaultExceptionHandler;
192+
193+
if (options != null) {
194+
options
195+
.getLogger()
196+
.log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.");
197+
}
198+
199+
} else {
200+
removeFromHandlerTree(currentHandlerIntegration.defaultExceptionHandler);
167201
}
168202
}
169203
}

sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import org.mockito.kotlin.whenever
1919
import java.io.ByteArrayOutputStream
2020
import java.io.PrintStream
2121
import java.nio.file.Files
22+
import java.util.concurrent.CompletableFuture
23+
import java.util.concurrent.Executors
2224
import kotlin.concurrent.thread
2325
import kotlin.test.Test
2426
import kotlin.test.assertEquals
@@ -313,7 +315,7 @@ class UncaughtExceptionHandlerIntegrationTest {
313315
val integration2 = UncaughtExceptionHandlerIntegration(handler)
314316
integration2.register(fixture.scopes, fixture.options)
315317

316-
assertEquals(currentDefaultHandler, integration2)
318+
assertEquals(integration2, currentDefaultHandler)
317319
integration2.close()
318320

319321
assertEquals(null, currentDefaultHandler)
@@ -344,4 +346,125 @@ class UncaughtExceptionHandlerIntegrationTest {
344346

345347
assertEquals(initialUncaughtExceptionHandler, currentDefaultHandler)
346348
}
349+
350+
@Test
351+
fun `multiple registrations with different global scopes allowed`() {
352+
val scopes2 = mock<IScopes>()
353+
val initialUncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, _ -> }
354+
355+
var currentDefaultHandler: Thread.UncaughtExceptionHandler? = initialUncaughtExceptionHandler
356+
357+
val handler = mock<UncaughtExceptionHandler>()
358+
whenever(handler.defaultUncaughtExceptionHandler).thenAnswer { currentDefaultHandler }
359+
360+
whenever(handler.setDefaultUncaughtExceptionHandler(anyOrNull<Thread.UncaughtExceptionHandler>())).then {
361+
currentDefaultHandler = it.getArgument(0)
362+
null
363+
}
364+
365+
whenever(scopes2.globalScope).thenReturn(mock<IScope>())
366+
367+
val integration1 = UncaughtExceptionHandlerIntegration(handler)
368+
integration1.register(fixture.scopes, fixture.options)
369+
370+
val integration2 = UncaughtExceptionHandlerIntegration(handler)
371+
integration2.register(scopes2, fixture.options)
372+
373+
assertEquals(currentDefaultHandler, integration2)
374+
integration2.close()
375+
376+
assertEquals(integration1, currentDefaultHandler)
377+
integration1.close()
378+
379+
assertEquals(initialUncaughtExceptionHandler, currentDefaultHandler)
380+
}
381+
382+
@Test
383+
fun `multiple registrations with different global scopes allowed, closed out of order`() {
384+
fixture.getSut()
385+
val scopes2 = mock<IScopes>()
386+
val initialUncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, _ -> }
387+
388+
var currentDefaultHandler: Thread.UncaughtExceptionHandler? = initialUncaughtExceptionHandler
389+
390+
val handler = mock<UncaughtExceptionHandler>()
391+
whenever(handler.defaultUncaughtExceptionHandler).thenAnswer { currentDefaultHandler }
392+
393+
whenever(handler.setDefaultUncaughtExceptionHandler(anyOrNull<Thread.UncaughtExceptionHandler>())).then {
394+
currentDefaultHandler = it.getArgument(0)
395+
null
396+
}
397+
398+
whenever(scopes2.globalScope).thenReturn(mock<IScope>())
399+
400+
val integration1 = UncaughtExceptionHandlerIntegration(handler)
401+
integration1.register(fixture.scopes, fixture.options)
402+
403+
val integration2 = UncaughtExceptionHandlerIntegration(handler)
404+
integration2.register(scopes2, fixture.options)
405+
406+
assertEquals(currentDefaultHandler, integration2)
407+
integration1.close()
408+
409+
assertEquals(integration2, currentDefaultHandler)
410+
integration2.close()
411+
412+
assertEquals(initialUncaughtExceptionHandler, currentDefaultHandler)
413+
}
414+
415+
@Test
416+
fun `multiple registrations async, closed async, one remains`() {
417+
val executor = Executors.newFixedThreadPool(4)
418+
fixture.getSut()
419+
val scopes2 = mock<IScopes>()
420+
val scopes3 = mock<IScopes>()
421+
val scopes4 = mock<IScopes>()
422+
val scopes5 = mock<IScopes>()
423+
424+
val scopesList = listOf(fixture.scopes, scopes2, scopes3, scopes4, scopes5)
425+
426+
val initialUncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, _ -> }
427+
428+
var currentDefaultHandler: Thread.UncaughtExceptionHandler? = initialUncaughtExceptionHandler
429+
430+
val handler = mock<UncaughtExceptionHandler>()
431+
whenever(handler.defaultUncaughtExceptionHandler).thenAnswer { currentDefaultHandler }
432+
433+
whenever(handler.setDefaultUncaughtExceptionHandler(anyOrNull<Thread.UncaughtExceptionHandler>())).then {
434+
currentDefaultHandler = it.getArgument(0)
435+
null
436+
}
437+
438+
whenever(scopes2.globalScope).thenReturn(mock<IScope>())
439+
whenever(scopes3.globalScope).thenReturn(mock<IScope>())
440+
whenever(scopes4.globalScope).thenReturn(mock<IScope>())
441+
whenever(scopes5.globalScope).thenReturn(mock<IScope>())
442+
443+
val integrations = scopesList.map { scope ->
444+
CompletableFuture.supplyAsync(
445+
{
446+
UncaughtExceptionHandlerIntegration(handler).apply {
447+
register(scope, fixture.options)
448+
}
449+
},
450+
executor
451+
)
452+
}
453+
454+
CompletableFuture.allOf(*integrations.toTypedArray()).get()
455+
456+
val futures = integrations.minus(integrations[2]).reversed().map { integration ->
457+
CompletableFuture.supplyAsync(
458+
{
459+
integration.get().close()
460+
println(Thread.currentThread().name)
461+
},
462+
executor
463+
)
464+
}
465+
466+
CompletableFuture.allOf(*futures.toTypedArray()).get()
467+
468+
assertEquals(integrations[2].get(), currentDefaultHandler)
469+
}
347470
}

0 commit comments

Comments
 (0)