Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -7705,6 +7705,7 @@ public final class io/sentry/util/EventSizeLimitingUtils {
public final class io/sentry/util/ExceptionUtils {
public fun <init> ()V
public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable;
public static fun handleFatal (Ljava/lang/Throwable;)V
public static fun isIgnored (Ljava/util/Set;Ljava/lang/Throwable;)Z
}

Expand Down
20 changes: 20 additions & 0 deletions sentry/src/main/java/io/sentry/util/ExceptionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,24 @@ public static boolean isIgnored(
final @NotNull Throwable throwable) {
return ignoredExceptionsForType.contains(throwable.getClass());
}

/**
* Handles non-recoverable {@link Throwable}s that should never be swallowed. Rethrows {@link
* VirtualMachineError} (e.g. OutOfMemoryError/StackOverflowError) and {@link ThreadDeath} as-is.
* For {@link InterruptedException}, restores the thread's interrupted status instead of
* rethrowing, since it is a checked exception. All other throwables are left untouched for the
* caller to handle/log/ignore as before.
*
* @param throwable - the throwable to check
*/
public static void handleFatal(final @NotNull Throwable throwable) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Thoughts about rethrowIfFatal()?

To me, that'd make its purpose more obvious at the call site (and I'm fine with its cheating a bit w/r/t restoring the interrupted flag).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sounds good to me too, let's cheat a little 😅

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with rethrowIfFatal!

// VirtualMachineError covers OutOfMemoryError, StackOverflowError, InternalError, and
// UnknownError
if (throwable instanceof VirtualMachineError || throwable instanceof ThreadDeath) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Agree that we should omit re-throwing LinkageError b/c of the compileOnly issue illustrated here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a valid point but I think we should re-throw LinkageError and only catch it where we expect it to be thrown as you did there.

throw (Error) throwable;
}
if (throwable instanceof InterruptedException) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit torn apart if this utility method should take care of interrupts as well, and decided to do it, as it will keep all call sites more tight. Along with that change the method is now a generic handleFatal instead of a rethrowFatal. That doesn't tell a lot about the method, but I guess it will be a common pattern everywhere. - Happy for any opinions on this!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me, and definitely nice to have a common facility for restoring the interrupt flag.

The only potential concern I can think of are methods that declare throws InterruptedException and need to use handleFatal(), as sometimes clients might expect either an InterruptedException or an interrupted flag to be set, but not both. But those methods could simply re-throw the InterruptedException before calling handleFatal().

The alternative would be to have separate methods similar to what Project Reactor does:

  • ExceptionUtils.rethrowIfJvmFatal()
  • ExceptionUtils.restoreInterruptIfNeeded()
  • ExceptionUtils.rethrowIfCancellation() (for coroutine CancellationException)

But I prefer the current all-in-one approach more 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's some good input! We could improve the ergonomics slightly by introducing method chaining. E.g.

ExceptionUtils.restoreInterrupt().rethrowIfJvmFatal()

Let me task the clanker to have a look at this and the actual impact on our codebase

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends on the codebase and if we have use cases where we would need to do one and not the other. Can you ask the clanker to give you a report if we have such cases?

Thread.currentThread().interrupt();
}
}
}
35 changes: 35 additions & 0 deletions sentry/src/test/java/io/sentry/util/ExceptionUtilsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package io.sentry.util
import java.lang.RuntimeException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class ExceptionUtilsTest {
@Test
Expand All @@ -18,4 +21,36 @@ class ExceptionUtilsTest {
val ex = RuntimeException(cause)
assertEquals(rootCause, ExceptionUtils.findRootCause(ex))
}

@Test
fun `handleFatal rethrows OutOfMemoryError`() {
assertFails { ExceptionUtils.handleFatal(OutOfMemoryError()) }
}

@Test
fun `handleFatal rethrows StackOverflowError`() {
assertFails { ExceptionUtils.handleFatal(StackOverflowError()) }
}

@Test
fun `handleFatal rethrows ThreadDeath`() {
assertFails { ExceptionUtils.handleFatal(ThreadDeath()) }
}

@Test
fun `handleFatal restores interrupt flag for InterruptedException without rethrowing`() {
try {
ExceptionUtils.handleFatal(InterruptedException())
assertTrue(Thread.currentThread().isInterrupted)
} finally {
// clear the interrupt flag so it doesn't leak into other tests
Thread.interrupted()
}
}

@Test
fun `handleFatal does nothing for regular exceptions`() {
ExceptionUtils.handleFatal(RuntimeException())
assertFalse(Thread.currentThread().isInterrupted)
}
}
Loading