-
Notifications
You must be signed in to change notification settings - Fork 529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhanced exceptions #1077
Enhanced exceptions #1077
Conversation
core/jvm/src/main/java/cats/effect/internals/TracingPlatform.java
Outdated
Show resolved
Hide resolved
core/shared/src/main/scala/cats/effect/internals/IORunLoop.scala
Outdated
Show resolved
Hide resolved
@@ -121,9 +109,33 @@ private[effect] object IOTrace { | |||
case (_, callSite) => !stackTraceFilter.exists(callSite.getClassName.startsWith(_)) | |||
} | |||
|
|||
def dropRunLoopSuffix(frames: List[StackTraceElement]): List[StackTraceElement] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IOTrace
companion object seems like a weird place to put this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it should be in IORunLoop
, since it's technically part of the runloop's implementation details?
@@ -33,11 +33,11 @@ final case class IOTrace(events: List[IOEvent], captured: Int, omitted: Int) { | |||
val Junction = "├" | |||
val Line = "│" | |||
|
|||
val acc0 = s"IOTrace: $captured frames captured\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to reorganize this code and come up with better names.
@@ -114,6 +114,9 @@ private[effect] object IORunLoop { | |||
catch { case NonFatal(ex) => RaiseError(ex) } | |||
|
|||
case RaiseError(ex) => | |||
if (isStackTracing && contextualExceptions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still thinking about if this is the correct place to do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I keep going back and forth. On one hand, doing it here means that every exception, even those explicitly raiseError
ed and later attempt
ed by users, gets decorated. This means that if users are doing something like handleErrorWith(log.error(_))
, they'll see the augmented trace. This seems very valuable!
But conversely, this also means that we pay the penalty of rendering the trace every time we call raiseError
, which is not a small performance hit I would assume. Maybe worth it? Probably? I'm not 100% sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we also have to be careful because augmented exceptions can be re-caught or re-lifted into the runloop. For example, IO.raiseError(ex).attempt.rethrow
, so I think we might need to check if it's already been augmented. I'm hoping the benchmarks will paint a better picture, but worst case it's something users can disable.
Brilliant 👍 |
How would this interact with |
@joroKr21 if |
private def augmentException(ex: Throwable, ctx: IOContext): Unit = { | ||
val stackTrace = ex.getStackTrace | ||
if (!stackTrace.isEmpty) { | ||
val augmented = stackTrace(stackTrace.length - 1) eq augmentationMarker |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the idea here is that a reference equality is probably a lot faster than substring searches. After we augment an exception, we append the augmentation marker which can be used for comparison later. Notice that because it is declared private
, callers can't access it and therefore they can't accidentally insert a marker.
The consequence of this is that printed stack traces show the marker at the end. I verified that we can't insert null
as a marker.
java.lang.Throwable: hello world!
at org.simpleapp.examples.Main$.b(Main.scala:29)
at org.simpleapp.examples.Main$.a(Main.scala:26)
at org.simpleapp.examples.Main$.$anonfun$foo$11(Main.scala:39)
at main$ @ org.simpleapp.examples.Main$.main(Main.scala:23)
at map @ org.simpleapp.examples.Main$.$anonfun$foo$10(Main.scala:39)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$8(Main.scala:37)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$6(Main.scala:36)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$4(Main.scala:35)
at flatMap @ org.simpleapp.examples.Main$.$anonfun$foo$2(Main.scala:34)
at flatMap @ org.simpleapp.examples.Main$.foo(Main.scala:33)
at flatMap @ org.simpleapp.examples.Main$.program(Main.scala:44)
at as @ org.simpleapp.examples.Main$.run(Main.scala:50)
at main$ @ org.simpleapp.examples.Main$.main(Main.scala:23)
at .(:0)
We're also doing a lot of Array
and List
manipulation here. Without thinking about it more, I'm not sure what the most efficient approach is (do everything in Array
or List
, and when should we convert back to an array?).
I'll benchmark this tomorrow so we have a concrete answer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized that users can access an instance of the market, through the stack trace itself! I don't think it'll be a problem either way because they would have to be deliberately manipulating stack traces for something to mess up. Same deal with checking @
probably.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the idea here is that a reference equality is probably a lot faster than substring searches. After we augment an exception, we append the augmentation marker which can be used for comparison later. Notice that because it is declared private, callers can't access it and therefore they can't accidentally insert a marker.
Hmm, I think this is worth benchmarking. In my experience, indexOf
is really really fast, so I don't think there'll be that much of a difference, but I could be mistaken!
* to include the async stack trace. | ||
*/ | ||
private def augmentException(ex: Throwable, ctx: IOContext): Unit = { | ||
val stackTrace = ex.getStackTrace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will throw a NPE here if ex
is null. I think that would break existing code that happen to rely on this behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do people actually run raiseError(null)
? I think I'm pretty comfortable telling those people that they just have to turn off augmented exceptions, because they don't deserve nice things. :-P
Here is a benchmark comparison between the reference equality check, the
The reference equality check is slightly faster, but not by any significant margin. Your call @djspiewak ! :D Also, I'm positive we can improve the performance of the |
:-) Let's go with |
With asynchronous stack tracing implemented, we have the ability to make exceptions caught by the IO runloop much more useful and relevant than they were before. This PR introduces enhanced exceptions, which is a feature that augments stack traces of caught exceptions with information from an accumulated fiber trace. The feature is controlled by a system property
cats.effect.enhancedExceptions
, which is enabled by default and also requires asynchronous stack tracing to be enabled.Stack traces before:
Stack traces after:
The original idea for this feature called for a
TracedException
wrapper, but there were quite a few problems with that approach, one of which was deciding where to wrap exceptions.Relevant Gitter discussion: https://gitter.im/typelevel/cats-effect-dev?at=5f31e511e20413052e7c556e
TODO:
printFiberTrace
?