-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,8 +18,8 @@ package cats.effect.internals | |
|
||
import cats.effect.IO | ||
import cats.effect.IO.{Async, Bind, ContextSwitch, Delay, Map, Pure, RaiseError, Suspend, Trace} | ||
import cats.effect.tracing.IOEvent | ||
import cats.effect.internals.TracingPlatform.isStackTracing | ||
import cats.effect.tracing.{IOEvent, IOTrace} | ||
import cats.effect.internals.TracingPlatform.{enhancedExceptions, isStackTracing} | ||
|
||
import scala.util.control.NonFatal | ||
|
||
|
@@ -114,6 +114,9 @@ private[effect] object IORunLoop { | |
catch { case NonFatal(ex) => RaiseError(ex) } | ||
|
||
case RaiseError(ex) => | ||
if (isStackTracing && enhancedExceptions && ctx != null) { | ||
augmentException(ex, ctx) | ||
} | ||
findErrorHandler(bFirst, bRest) match { | ||
case null => | ||
cb(Left(ex)) | ||
|
@@ -244,6 +247,9 @@ private[effect] object IORunLoop { | |
} catch { case NonFatal(ex) => RaiseError(ex) } | ||
|
||
case RaiseError(ex) => | ||
if (isStackTracing && enhancedExceptions && ctx != null) { | ||
augmentException(ex, ctx) | ||
} | ||
findErrorHandler(bFirst, bRest) match { | ||
case null => | ||
return currentIO.asInstanceOf[IO[A]] | ||
|
@@ -357,6 +363,43 @@ private[effect] object IORunLoop { | |
} | ||
} | ||
|
||
/** | ||
* If stack tracing and contextual exceptions are enabled, this | ||
* function will rewrite the stack trace of a captured exception | ||
* to include the async stack trace. | ||
*/ | ||
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 commentThe 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 The consequence of this is that printed stack traces show the marker at the end. I verified that we can't insert 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 I'll benchmark this tomorrow so we have a concrete answer. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hmm, I think this is worth benchmarking. In my experience, |
||
if (!augmented) { | ||
val prefix = dropRunLoopFrames(stackTrace) | ||
val suffix = ctx | ||
.getStackTraces() | ||
.flatMap(t => IOTrace.getOpAndCallSite(t.stackTrace)) | ||
.map { | ||
case (methodSite, callSite) => | ||
new StackTraceElement(methodSite.getMethodName + " @ " + callSite.getClassName, | ||
callSite.getMethodName, | ||
callSite.getFileName, | ||
callSite.getLineNumber) | ||
} | ||
.toArray | ||
ex.setStackTrace(prefix ++ suffix ++ Array(augmentationMarker)) | ||
} | ||
} | ||
} | ||
|
||
private def dropRunLoopFrames(frames: Array[StackTraceElement]): Array[StackTraceElement] = | ||
frames.takeWhile(ste => !runLoopFilter.exists(ste.getClassName.startsWith(_))) | ||
|
||
private[this] val runLoopFilter = List( | ||
"cats.effect.", | ||
"scala." | ||
) | ||
|
||
private[this] val augmentationMarker = new StackTraceElement("", "", "", 0) | ||
|
||
/** | ||
* A `RestartCallback` gets created only once, per [[startCancelable]] | ||
* (`unsafeRunAsync`) invocation, once an `Async` state is hit, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe 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. |
||
if (options.showFullStackTraces) { | ||
val stackTraces = events.collect { case e: IOEvent.StackTrace => e } | ||
|
||
val header = s"IOTrace: $captured frames captured, $omitted omitted\n" | ||
val body = stackTraces.zipWithIndex | ||
val acc1 = stackTraces.zipWithIndex | ||
.map { | ||
case (st, index) => | ||
val tag = getOpAndCallSite(st.stackTrace) | ||
|
@@ -62,56 +62,44 @@ final case class IOTrace(events: List[IOEvent], captured: Int, omitted: Int) { | |
} | ||
.mkString("\n") | ||
|
||
header + body | ||
val acc2 = if (omitted > 0) { | ||
"\n" + TurnRight + s" ... ($omitted frames omitted)\n" | ||
} else "\n" + TurnRight + "\n" | ||
|
||
acc0 + acc1 + acc2 | ||
} else { | ||
val acc0 = s"IOTrace: $captured frames captured, $omitted omitted\n" | ||
val acc1 = events.zipWithIndex | ||
.map { | ||
case (event, index) => | ||
val junc = if (index == events.length - 1) TurnRight else Junction | ||
val junc = if (index == events.length - 1 && omitted == 0) TurnRight else Junction | ||
val message = event match { | ||
case ev: IOEvent.StackTrace => { | ||
getOpAndCallSite(ev.stackTrace) | ||
.map { | ||
case (methodSite, callSite) => | ||
val loc = renderStackTraceElement(callSite) | ||
val op = NameTransformer.decode(methodSite.getMethodName) | ||
s"$op at $loc" | ||
s"$op @ $loc" | ||
} | ||
.getOrElse("(...)") | ||
} | ||
} | ||
s" $junc $message" | ||
} | ||
.mkString(acc0, "\n", "\n") | ||
.mkString(acc0, "\n", "") | ||
|
||
val acc2 = if (omitted > 0) { | ||
acc1 + "\n " + TurnRight + s" ... ($omitted frames omitted)" | ||
RaasAhsan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else acc1 | ||
|
||
acc1 | ||
acc2 + "\n" | ||
} | ||
} | ||
} | ||
|
||
private[effect] object IOTrace { | ||
|
||
// Number of lines to drop from the top of the stack trace | ||
def stackTraceIgnoreLines = 3 | ||
|
||
private[this] val anonfunRegex = "^\\$+anonfun\\$+(.+)\\$+\\d+$".r | ||
|
||
private[this] val stackTraceFilter = List( | ||
"cats.effect.", | ||
"cats.", | ||
"sbt.", | ||
"java.", | ||
"sun.", | ||
"scala." | ||
) | ||
|
||
private def renderStackTraceElement(ste: StackTraceElement): String = { | ||
val methodName = demangleMethod(ste.getMethodName) | ||
s"${ste.getClassName}.$methodName (${ste.getFileName}:${ste.getLineNumber})" | ||
} | ||
|
||
private def getOpAndCallSite(frames: List[StackTraceElement]): Option[(StackTraceElement, StackTraceElement)] = | ||
def getOpAndCallSite(frames: List[StackTraceElement]): Option[(StackTraceElement, StackTraceElement)] = | ||
frames | ||
.sliding(2) | ||
.collect { | ||
|
@@ -121,9 +109,25 @@ private[effect] object IOTrace { | |
case (_, callSite) => !stackTraceFilter.exists(callSite.getClassName.startsWith(_)) | ||
} | ||
|
||
private def renderStackTraceElement(ste: StackTraceElement): String = { | ||
val methodName = demangleMethod(ste.getMethodName) | ||
s"${ste.getClassName}.$methodName (${ste.getFileName}:${ste.getLineNumber})" | ||
} | ||
|
||
private def demangleMethod(methodName: String): String = | ||
anonfunRegex.findFirstMatchIn(methodName) match { | ||
case Some(mat) => mat.group(1) | ||
case None => methodName | ||
} | ||
|
||
private[this] val anonfunRegex = "^\\$+anonfun\\$+(.+)\\$+\\d+$".r | ||
|
||
private[this] val stackTraceFilter = List( | ||
"cats.effect.", | ||
"cats.", | ||
"sbt.", | ||
"java.", | ||
"sun.", | ||
"scala." | ||
) | ||
} |
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 behaviorThere 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