├── project ├── build.properties └── plugins.sbt ├── notes ├── 0.1.markdown ├── 0.4.markdown ├── 0.3.makrdown ├── about.markdown └── 0.2.markdown ├── agent └── src │ ├── main │ ├── scala │ │ └── trackedfuture │ │ │ ├── agent │ │ │ ├── Agent.scala │ │ │ ├── ClassAdapter.scala │ │ │ ├── TrackedFutureTransformer.scala │ │ │ └── MethodAdapter.scala │ │ │ ├── runtime │ │ │ ├── ThreadTrace.scala │ │ │ ├── StackTraces.scala │ │ │ ├── TrackedAwait.scala │ │ │ └── TrackedFuture.scala │ │ │ └── util │ │ │ └── ThreadLocalIterator.scala │ └── resource │ │ └── META-INF │ │ └── MANIFEST.MF │ └── test │ └── scala │ └── trackedfuture │ ├── SimpleFutureRun.scala │ └── runtime │ └── ExceptionSpec.scala ├── publish.sbt ├── README.md └── example └── src ├── main └── scala │ └── tracedfuture │ └── example │ └── Main.scala └── test └── scala └── tracedfuture └── example └── MainCallSpec.scala /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.1 2 | -------------------------------------------------------------------------------- /notes/0.1.markdown: -------------------------------------------------------------------------------- 1 | 2 | - initial proof-of-concept revision. 3 | 4 | 5 | -------------------------------------------------------------------------------- /notes/0.4.markdown: -------------------------------------------------------------------------------- 1 | 2 | - updated compiler to 2.13.5, sbt to 1.5.0 3 | 4 | 5 | -------------------------------------------------------------------------------- /notes/0.3.makrdown: -------------------------------------------------------------------------------- 1 | 2 | - added support for `Future.` 3 | -- `foreach` 4 | -- `recover` 5 | -- `recoverWith` 6 | 7 | 8 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") 2 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1") 3 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | 2 | [trackedfuture](https://github.com/rssh/trackedfuture) is a small java agent, which bring back full 3 | stacktraces to callbacks, passed into Future methods. 4 | 5 | -------------------------------------------------------------------------------- /notes/0.2.markdown: -------------------------------------------------------------------------------- 1 | 2 | - used part of objectweb asm is packed inside agent jar with changed package. 3 | - implemented substitutions for 4 | ```apply``` 5 | ```onComplete``` 6 | ```map``` 7 | ```flatMap``` 8 | ```filter```, ```withFilter```, 9 | ```collect``` 10 | 11 | 12 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/agent/Agent.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.agent 2 | 3 | import java.lang.instrument._ 4 | 5 | object Agent 6 | { 7 | 8 | def premain(args: String, instr: Instrumentation):Unit = 9 | { 10 | System.err.println("class transformer added") 11 | instr.addTransformer(new TrackedFutureTransformer()) 12 | } 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /agent/src/main/resource/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Implementation-Title: trackedfuture 3 | Implementation-Version: 0.1 4 | Specification-Vendor: com.github.rssh 5 | Specification-Title: trackedfuture 6 | Implementation-Vendor-Id: com.github.rssh 7 | Specification-Version: 0.1 8 | Implementation-Vendor: com.github.rssh 9 | Premain-Class: trackedfuture.agent.Agent 10 | Can-Redefine-Classes: true 11 | Can-Retransform-Classes: true 12 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/agent/ClassAdapter.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.agent 2 | 3 | import org.objectweb.asm._ 4 | 5 | class ClassAdapter(up: ClassVisitor) extends ClassVisitor(Opcodes.ASM5, up) { 6 | 7 | 8 | override def visitMethod(access: Int, name: String, desc: String, 9 | signature: String, exceptions: Array[String]): MethodVisitor = { 10 | val mv = up.visitMethod(access, name, desc, signature, exceptions) 11 | if (!(mv eq null)) { 12 | new MethodAdapter(mv) 13 | } else { 14 | mv 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /agent/src/test/scala/trackedfuture/SimpleFutureRun.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture 2 | 3 | import java.util.concurrent.Executors 4 | 5 | import scala.language.postfixOps 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.concurrent._ 8 | import scala.concurrent.duration._ 9 | 10 | object SimpleFutureRun 11 | { 12 | 13 | def main(args:Array[String]):Unit = 14 | { 15 | 16 | val ec = ExecutionContext.fromExecutor( 17 | Executors.newFixedThreadPool(1) 18 | ) 19 | 20 | val f = Future { 21 | Thread.sleep(10*1000) 22 | } 23 | 24 | System.out.println("Here") 25 | Await.ready(f, 1000 millis) 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/runtime/ThreadTrace.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.runtime 2 | 3 | import scala.util._ 4 | 5 | object ThreadTrace { 6 | 7 | val prevTraces = new DynamicVariable[StackTraces](null) 8 | 9 | def retrieveCurrent(): StackTraces = { 10 | val trace = Thread.currentThread.getStackTrace 11 | new StackTraces(trace, prevTraces.value) 12 | } 13 | 14 | def setPrev(st: StackTraces): Unit = { 15 | prevTraces.value = st 16 | } 17 | 18 | def mergeWithPrev(trace: Array[StackTraceElement]): Array[StackTraceElement] = { 19 | if (prevTraces.value eq null) { 20 | trace 21 | } else { 22 | new StackTraces(trace, prevTraces.value).toTrace 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/agent/TrackedFutureTransformer.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.agent 2 | 3 | import java.lang.instrument._ 4 | import java.security._ 5 | 6 | import org.objectweb.asm._ 7 | 8 | class TrackedFutureTransformer extends ClassFileTransformer { 9 | 10 | override def transform(loader: ClassLoader, className: String, classBeingRedefined: Class[_], 11 | protectionDomain: ProtectionDomain, classfileBuffer: Array[Byte]): Array[Byte] = { 12 | if (className.startsWith("java/") 13 | || className.startsWith("com/sun/") 14 | || className.startsWith("sun/") 15 | || className.startsWith("scala/") 16 | || className.startsWith("trackedfuture/runtime/") 17 | ) { 18 | //null to prevent memory leal 19 | classfileBuffer 20 | } else { 21 | //System.err.println("transforming class:"+className) 22 | val writer = new ClassWriter(0) 23 | val adapter = new ClassAdapter(writer) 24 | val reader = new ClassReader(classfileBuffer) 25 | reader.accept(adapter, 0) 26 | writer.toByteArray 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/runtime/StackTraces.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.runtime 2 | 3 | import scala.concurrent.Future 4 | 5 | class StackTraces(elements: Array[StackTraceElement], prev: StackTraces = null) { 6 | 7 | private var currentFuture: Future[Unit] = _ 8 | 9 | def getCurrentFuture[T]: Future[Unit] = currentFuture 10 | 11 | def setCurrentFuture(f: Future[Unit]): Unit = { 12 | currentFuture = f 13 | } 14 | 15 | def depth(): Int = { 16 | val prevDepth = if (prev eq null) 0 else prev.depth() 17 | prevDepth + elements.length 18 | } 19 | 20 | def toTrace: Array[StackTraceElement] = { 21 | val trace = new Array[StackTraceElement](depth()) 22 | fill(trace, 0) 23 | trace 24 | } 25 | 26 | def fill(trace: Array[StackTraceElement], startIndex: Int): Int = { 27 | val nextIndex = startIndex + elements.length 28 | System.arraycopy(elements, 0, trace, startIndex, elements.length) 29 | //TODO: think - how it's needed, 30 | //val inserted = createSeparatorStackTraceElement(); 31 | if (prev eq null) { 32 | nextIndex 33 | } else { 34 | prev.fill(trace, nextIndex) 35 | } 36 | } 37 | 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/runtime/TrackedAwait.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.runtime 2 | 3 | import java.util.concurrent.TimeoutException 4 | 5 | import trackedfuture.util.ThreadLocalIterator 6 | 7 | import scala.concurrent.duration.Duration 8 | import scala.concurrent.{Await, Awaitable} 9 | 10 | object TrackedAwait { 11 | def ready[T](awaitOriginal: Await.type, awaitable: Awaitable[T], atMost: Duration): awaitable.type = { 12 | try { 13 | awaitOriginal.ready(awaitable, atMost) 14 | } catch { 15 | case ex: TimeoutException => { 16 | ThreadTrace.setPrev(threadLocalTrace(awaitable)) 17 | ex.setStackTrace(ThreadTrace.mergeWithPrev(ex.getStackTrace)) 18 | throw ex 19 | } 20 | } 21 | } 22 | 23 | def result[T](awaitOriginal: Await.type, awaitable: Awaitable[T], atMost: Duration): T = { 24 | try { 25 | awaitOriginal.result(awaitable, atMost) 26 | } catch { 27 | case ex: TimeoutException => { 28 | ThreadTrace.setPrev(threadLocalTrace(awaitable)) 29 | ex.setStackTrace(ThreadTrace.mergeWithPrev(ex.getStackTrace)) 30 | throw ex 31 | } 32 | } 33 | } 34 | 35 | private def threadLocalTrace[T](awaitable: Awaitable[T]): StackTraces = { 36 | val trace = new ThreadLocalIterator[StackTraces](classOf[StackTraces]).iterator.find(_.getCurrentFuture == awaitable) 37 | if (trace == null) new StackTraces(Array()) else trace.get 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /publish.sbt: -------------------------------------------------------------------------------- 1 | credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credentials") 2 | 3 | ThisBuild / organization := "com.github.rssh" 4 | ThisBuild / organizationName := "rssh" 5 | ThisBuild / organizationHomepage := Some(url("https://github.com/rssh")) 6 | 7 | ThisBuild / scmInfo := Some( 8 | ScmInfo( 9 | url("https://github.com/rssh/trackedfuture"), 10 | "scm:git@github.com:rssh/trackedfuture" 11 | ) 12 | ) 13 | 14 | 15 | ThisBuild / developers := List( 16 | Developer( 17 | id = "rssh", 18 | name = "Ruslan Shevchenko", 19 | email = "ruslan@shevchenko.kiev.ua", 20 | url = url("https://github.com/rssh") 21 | ) 22 | ) 23 | 24 | 25 | ThisBuild / description := "small runtime + java-agent, which brings back full stacktrace for exceptions, generated inside Future callbacks" 26 | ThisBuild / licenses := List("Apache 2" -> new URL("http://www.apache.org/licenses/LICENSE-2.0.txt")) 27 | ThisBuild / homepage := Some(url("https://github.com/rssh/trackedfuture")) 28 | 29 | ThisBuild / pomIncludeRepository := { _ => false } 30 | ThisBuild / publishTo := { 31 | val nexus = "https://oss.sonatype.org/" 32 | if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots") 33 | else Some("releases" at nexus + "service/local/staging/deploy/maven2") 34 | } 35 | ThisBuild / publishMavenStyle := true 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/util/ThreadLocalIterator.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.util 2 | 3 | import scala.collection.JavaConverters._ 4 | import scala.collection.mutable.ListBuffer 5 | 6 | class ThreadLocalIterator[A](clazz: Class[A]) extends Iterable[A]{ 7 | 8 | 9 | override def iterator: Iterator[A] = getInternalMap 10 | 11 | private def getInternalMap: Iterator[A] = { 12 | 13 | val bufferList = new ListBuffer[A] 14 | val threadField = classOf[Thread].getDeclaredField("inheritableThreadLocals") 15 | threadField.setAccessible(true) 16 | 17 | val threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap") 18 | val tableField = threadLocalMapClass.getDeclaredField("table") 19 | tableField.setAccessible(true) 20 | 21 | for ((thread, stacks) <- Thread.getAllStackTraces.asScala.mapValues(_.toSet); 22 | tf = threadField.get(thread) 23 | if tf != null) { 24 | val table = tableField.get(tf) 25 | 26 | for (i <- 0 until java.lang.reflect.Array.getLength(table); 27 | entry = java.lang.reflect.Array.get(table, i) 28 | if entry != null) { 29 | val valueField = entry.getClass.getDeclaredField("value") 30 | valueField.setAccessible(true) 31 | val value = valueField.get(entry) 32 | 33 | if (value != null && value.getClass.isAssignableFrom(clazz)) { 34 | bufferList += value.asInstanceOf[A] 35 | } 36 | } 37 | } 38 | 39 | bufferList.toList.iterator 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /agent/src/test/scala/trackedfuture/runtime/ExceptionSpec.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.runtime 2 | 3 | import scala.language.postfixOps 4 | import scala.concurrent._ 5 | import scala.concurrent.duration._ 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.util._ 8 | 9 | import org.scalatest._ 10 | import org.scalatest.concurrent._ 11 | import flatspec._ 12 | 13 | 14 | 15 | class ExceptionSpec extends AnyFlatSpec with Waiters 16 | { 17 | 18 | "TrackedFuture" should "show origin thread between when trowing exception" in { 19 | val callCodeLine = 25; //!!! - point of code [TODO: implement __LINE__ as macro] 20 | def middleFun(): Future[Unit] = { 21 | val x = 1 22 | TrackedFuture { //!!! here is callCodeLine 23 | val y = 2 24 | if (true) { 25 | throw new Exception("qqq") 26 | } 27 | } 28 | } 29 | val w = new Waiter 30 | middleFun() onComplete { 31 | case Failure(ex) => 32 | val checked = checkFL("ExceptionSpec.scala",callCodeLine,ex) 33 | w{ assert(checked) } 34 | w.dismiss() 35 | case _ => w{ assert(false) } 36 | w.dismiss() 37 | } 38 | w.await{timeout(10 seconds)} 39 | } 40 | 41 | private def checkFL(fname:String, line:Int, ex:Throwable): Boolean = { 42 | ex.printStackTrace() 43 | ex.getStackTrace.toSeq.find{ ste => 44 | (ste.getFileName() == fname 45 | ) && ( 46 | line == -1 || ste.getLineNumber()==line 47 | ) 48 | }.isDefined 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/agent/MethodAdapter.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.agent 2 | 3 | import org.objectweb.asm._ 4 | 5 | class MethodAdapter(up: MethodVisitor) extends MethodVisitor(Opcodes.ASM5, up) { 6 | 7 | 8 | val methodMapping = Map( 9 | (Opcodes.INVOKEVIRTUAL, 10 | "scala/concurrent/Future$", "apply", 11 | "(Lscala/Function0;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 12 | ) ->("trackedfuture/runtime/TrackedFuture", "rapply"), 13 | (Opcodes.INVOKEINTERFACE, 14 | "scala/concurrent/Future", "map", 15 | "(Lscala/Function1;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 16 | ) ->("trackedfuture/runtime/TrackedFuture", "rmap"), 17 | (Opcodes.INVOKEINTERFACE, 18 | "scala/concurrent/Future", "flatMap", 19 | "(Lscala/Function1;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 20 | ) ->("trackedfuture/runtime/TrackedFuture", "rFlatMap"), 21 | (Opcodes.INVOKEINTERFACE, 22 | "scala/concurrent/Future", "filter", 23 | "(Lscala/Function1;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 24 | ) ->("trackedfuture/runtime/TrackedFuture", "rFilter"), 25 | (Opcodes.INVOKEINTERFACE, 26 | "scala/concurrent/Future", "withFilter", 27 | "(Lscala/Function1;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 28 | ) ->("trackedfuture/runtime/TrackedFuture", "rFilter"), 29 | (Opcodes.INVOKEINTERFACE, 30 | "scala/concurrent/Future", "collect", 31 | "(Lscala/PartialFunction;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 32 | ) ->("trackedfuture/runtime/TrackedFuture", "collect"), 33 | (Opcodes.INVOKEINTERFACE, 34 | "scala/concurrent/Future", "onComplete", 35 | "(Lscala/Function1;Lscala/concurrent/ExecutionContext;)V" 36 | ) ->("trackedfuture/runtime/TrackedFuture", "onComplete"), 37 | (Opcodes.INVOKEINTERFACE, 38 | "scala/concurrent/Future", "foreach", 39 | "(Lscala/Function1;Lscala/concurrent/ExecutionContext;)V" 40 | ) ->("trackedfuture/runtime/TrackedFuture", "foreach"), 41 | (Opcodes.INVOKEINTERFACE, 42 | "scala/concurrent/Future", "transform", 43 | "(Lscala/Function1;Lscala/Function1;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 44 | ) ->("trackedfuture/runtime/TrackedFuture", "transform"), 45 | (Opcodes.INVOKEINTERFACE, 46 | "scala/concurrent/Future", "recover", 47 | "(Lscala/PartialFunction;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 48 | ) ->("trackedfuture/runtime/TrackedFuture", "recover"), 49 | (Opcodes.INVOKEINTERFACE, 50 | "scala/concurrent/Future", "recoverWith", 51 | "(Lscala/PartialFunction;Lscala/concurrent/ExecutionContext;)Lscala/concurrent/Future;" 52 | ) ->("trackedfuture/runtime/TrackedFuture", "recoverWith"), 53 | (Opcodes.INVOKEINTERFACE, 54 | "scala/concurrent/Future", "andThen", 55 | "(Lscala/PartialFunction;Lscala/concurrent/ExecutionContext;)V" 56 | ) ->("trackedfuture/runtime/TrackedFuture", "andThen"), 57 | (Opcodes.INVOKEVIRTUAL, 58 | "scala/concurrent/Await$", "ready", 59 | "(Lscala/concurrent/Awaitable;Lscala/concurrent/duration/Duration;)Lscala/concurrent/Awaitable;" 60 | ) ->("trackedfuture/runtime/TrackedAwait", "ready"), 61 | (Opcodes.INVOKEVIRTUAL, 62 | "scala/concurrent/Await$", "result", 63 | "(Lscala/concurrent/Awaitable;Lscala/concurrent/duration/Duration;)Ljava/lang/Object;" 64 | ) ->("trackedfuture/runtime/TrackedAwait", "result") 65 | ) 66 | 67 | override def visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean): Unit = { 68 | methodMapping get(opcode, owner, name, desc) match { 69 | case Some(classMethodTuple) => 70 | up.visitMethodInsn(Opcodes.INVOKESTATIC, 71 | classMethodTuple._1, 72 | classMethodTuple._2, 73 | appendParam(owner, desc), 74 | false) 75 | case None => 76 | up.visitMethodInsn(opcode, owner, name, desc, itf) 77 | } 78 | } 79 | 80 | def appendParam(ownerType: String, desc: String): String = 81 | s"(L${ownerType};${desc.substring(1)}" 82 | 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tracked Future 2 | 3 | [![Join the chat at https://gitter.im/rssh/trackedfuture](https://badges.gitter.im/rssh/trackedfuture.svg)](https://gitter.im/rssh/trackedfuture?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ## Overview 6 | 7 | 8 | Contains agent, which substitute in bytecode calls to future method wich accepts callbacks 9 | ( ```Future.apply''' ```map```, ```flatMap``` ```filter``` ... etc) to tracked versions which save origin caller stack. 10 | 11 | Ie. tracked version collect stack trace of origin thread when appropriative construction is created and then, 12 | when handle exception, merge one with stack trace of this exception; 13 | 14 | Useful for debugging. 15 | 16 | ## Usage 17 | 18 | * publishLocal tracked-future to you local repository 19 | 20 | * when debug, enable agent 21 | ~~~scala 22 | fork := true 23 | javaOptions ++= Seq( 24 | "--add-opens", 25 | "java.base/java.lang=ALL-UNNAMED", 26 | s"-javaagent:${System.getProperty("user.home")}/_//jars/trackedfuture_-assembly.jar" 27 | ) 28 | ~~~ 29 | 30 | where 31 | - repoDir - location of you local repository 32 | - scalaVersion - is a scala version as it used in artefacts suffixes 33 | - version - version of trackedfuture 34 | 35 | Use 36 | * 0.5.0 version for scala 3 37 | * 0.4.2 for scala 2.13.5 38 | 39 | ## Results 40 | 41 | Let's look at the next code: 42 | ~~~scala 43 | object Main 44 | { 45 | 46 | def main(args: Array[String]):Unit = 47 | { 48 | val f = f0("222") 49 | try { 50 | val r = Await.result(f,10 seconds) 51 | } catch { 52 | // will print with f0 when agent is enabled 53 | case ex: Throwable => ex.printStackTrace 54 | } 55 | } 56 | 57 | def f0(x:String): Future[Unit] = 58 | { 59 | System.err.print("f0:"); 60 | f1(x) 61 | } 62 | 63 | def f1(x: String): Future[Unit] = 64 | Future{ 65 | throw new RuntimeException("AAA"); 66 | } 67 | 68 | } 69 | 70 | ~~~ 71 | 72 | With tracked future agent enabled, instead traces, which ends in top-level executor: 73 | 74 | ~~~ 75 | f0:java.lang.RuntimeException: AAA 76 | at trackedfuture.example.Main$$anonfun$f1$1.apply(Main.scala:30) 77 | at trackedfuture.example.Main$$anonfun$f1$1.apply(Main.scala:30) 78 | at trackedfuture.runtime.TrackedFuture$$anon$1.run(TrackedFuture.scala:21) 79 | at scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:121) 80 | at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260) 81 | at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339) 82 | at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979) 83 | at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107) 84 | ~~~ 85 | 86 | you will see traces wich include information: from where future was started: 87 | 88 | ~~~ 89 | f0:java.lang.RuntimeException: AAA 90 | at trackedfuture.example.Main$$anonfun$f1$1.apply(Main.scala:30) 91 | at trackedfuture.example.Main$$anonfun$f1$1.apply(Main.scala:30) 92 | at trackedfuture.runtime.TrackedFuture$$anon$1.run(TrackedFuture.scala:21) 93 | at scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:121) 94 | at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260) 95 | at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339) 96 | at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979) 97 | at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107) 98 | at java.lang.Thread.getStackTrace(Thread.java:1552) 99 | at trackedfuture.runtime.TrackedFuture$.apply(TrackedFuture.scala:13) 100 | at trackedfuture.runtime.TrackedFuture$.rapply(TrackedFuture.scala:39) 101 | at trackedfuture.runtime.TrackedFuture.rapply(TrackedFuture.scala) 102 | at trackedfuture.example.Main$.f1(Main.scala:29) 103 | at trackedfuture.example.Main$.f0(Main.scala:25) 104 | at trackedfuture.example.Main$.main(Main.scala:13) 105 | at trackedfuture.example.Main.main(Main.scala) 106 | ~~~ 107 | 108 | ## Additional Notes 109 | 110 | If you want a version with more wrappend methods and with frames cleanup - don't hesitate to submit pull request ;) 111 | 112 | -------------------------------------------------------------------------------- /example/src/main/scala/tracedfuture/example/Main.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.example 2 | 3 | import scala.language.postfixOps 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import scala.concurrent._ 6 | import scala.concurrent.duration._ 7 | import scala.util._ 8 | 9 | object Main { 10 | 11 | def main(args: Array[String]): Unit = { 12 | 13 | val f = f0("222") 14 | try { 15 | val r = Await.result(f, 10 seconds) 16 | } catch { 17 | // will print with f0 when agent is enabled 18 | case ex: Throwable => ex.printStackTrace 19 | } 20 | try { 21 | val r = Await.result(f3("AAA"), 10 seconds) 22 | } catch { 23 | case ex: Throwable => ex.printStackTrace 24 | } 25 | } 26 | 27 | def f0(x: String): Future[Unit] = { 28 | f1(x) 29 | } 30 | 31 | def f1(x: String): Future[Unit] = 32 | Future { 33 | throw new RuntimeException("AAA"); 34 | } 35 | 36 | def f3(x: String): Future[Unit] = f4(x) 37 | 38 | def f4(x: String): Future[Unit] = { 39 | Future { 40 | "aaaa " 41 | } map { _ => throw new Exception("bbb-q1") } 42 | } 43 | 44 | def fFlatMap0(): Future[Unit] = fFlatMap1() 45 | 46 | def fFlatMap1(): Future[Unit] = { 47 | Future { 48 | Future { 49 | "aaaa " 50 | } 51 | } flatMap { _ => throw new Exception("bbb-q1") } 52 | } 53 | 54 | def fFilter0(): Future[Unit] = fFilter1() 55 | 56 | def fFilter1(): Future[Unit] = { 57 | Future { 58 | () 59 | } filter { _ => false } 60 | } 61 | 62 | def withFilter0(): Future[Unit] = withFilter1() 63 | 64 | def withFilter1(): Future[Unit] = { 65 | Future { 66 | () 67 | } withFilter { _ => throw new Exception("AAA") } 68 | } 69 | 70 | def fCollect0(f: PartialFunction[String, String]): Future[String] = 71 | fCollect1(f) 72 | 73 | def fCollect1(f: PartialFunction[String, String]): Future[String] = 74 | Future { 75 | "aaa" 76 | } collect f 77 | 78 | def fOnComplete0(ec: ExecutionContext): Unit = 79 | fOnComplete1(ec) 80 | 81 | def fOnComplete1(ec: ExecutionContext): Unit = { 82 | Future { 83 | "aaa" 84 | }.onComplete { _ => throw new RuntimeException("Be-Be-Be!") }(ec) 85 | } 86 | 87 | def fOnFailure0(ec: ExecutionContext): Unit = { 88 | fOnFailure1(ec) 89 | } 90 | 91 | def fOnFailure1(ec: ExecutionContext): Unit = { 92 | // onFailure is removed 93 | //Future(1 / 0).onFailure { case ex: ArithmeticException => throw new RuntimeException("from onFailure") }(ec) 94 | Future(1 / 0).onComplete { 95 | case Success(v) => 96 | case Failure(ex: ArithmeticException) => throw new RuntimeException("from onFailure") 97 | case _ => 98 | }(ec) 99 | 100 | } 101 | 102 | def fOnSuccess0(ec: ExecutionContext): Unit = { 103 | fOnSuccess1(ec) 104 | } 105 | 106 | def fOnSuccess1(ec: ExecutionContext): Unit = { 107 | // Future(1).onSuccess { case 1 => throw new RuntimeException("from onSuccess") }(ec) 108 | Future(1).onComplete { 109 | case Success(x) => if (x==1) throw new RuntimeException("from onSuccess") 110 | case Failure(ex) => ex.printStackTrace() 111 | }(ec) 112 | } 113 | 114 | def fForeach0(ec: ExecutionContext): Unit = 115 | fForeach1(ec) 116 | 117 | def fForeach1(ec: ExecutionContext): Unit = { 118 | Future { 119 | "aaa" 120 | }.foreach { _ => throw new RuntimeException("Be-Be-Be!") }(ec) 121 | } 122 | 123 | def fTransform0(): Future[Int] = 124 | fTransform1() 125 | 126 | def fTransform1(): Future[Int] = { 127 | Future { 128 | "aaa" 129 | }.transform((_.toInt), identity) 130 | } 131 | 132 | def fRecover0(): Future[Int] = 133 | fRecover1() 134 | 135 | def fRecover1(): Future[Int] = { 136 | Future { 137 | "aaa" 138 | }.map((_.toInt)).recover { 139 | case ex: NumberFormatException => throw new RuntimeException("from recover") 140 | } 141 | } 142 | 143 | def fRecoverWith0(): Future[Int] = fRecoverWith1() 144 | 145 | def fRecoverWith1(): Future[Int] = 146 | Future(1 / 0).recoverWith { 147 | case ex: ArithmeticException => throw new RuntimeException("from recoverWith") 148 | } 149 | 150 | def awaitReady0(ec: ExecutionContext) = { 151 | awaitReady1(ec) 152 | } 153 | 154 | def awaitReady1(ec: ExecutionContext) = { 155 | val f = Future { 156 | Thread.sleep(10 * 1000) 157 | }(ec) 158 | 159 | Await.ready(f, 1000 millis) 160 | } 161 | 162 | def awaitResult0(ec: ExecutionContext) = { 163 | awaitResult1(ec) 164 | } 165 | 166 | def awaitResult1(ec: ExecutionContext) = { 167 | val f = Future { 168 | Thread.sleep(10 * 1000) 169 | }(ec) 170 | 171 | Await.result(f, 1000 millis) 172 | } 173 | 174 | 175 | } 176 | -------------------------------------------------------------------------------- /example/src/test/scala/tracedfuture/example/MainCallSpec.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.example 2 | 3 | import java.util.concurrent.{Future => _, _} 4 | 5 | import scala.language.postfixOps 6 | import org.scalatest._ 7 | import org.scalatest.concurrent._ 8 | import org.scalatest.flatspec._ 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent._ 12 | import scala.concurrent.duration._ 13 | import scala.util._ 14 | 15 | 16 | 17 | class MainCallSpec extends AnyFlatSpec with Waiters 18 | { 19 | 20 | var showException=false 21 | 22 | "MainCall" should "show origin method between future " in { 23 | callAndCheckMethod( Main.f0("AAA"), "f0") 24 | } 25 | 26 | "MainCall" should "show origin method with map " in { 27 | callAndCheckMethod( Main.f3("AAA"), "f3") 28 | } 29 | 30 | "MainCall" should "show origin method with flatMap " in { 31 | callAndCheckMethod( Main.fFlatMap0(), "fFlatMap0") 32 | } 33 | 34 | "MainCall" should "show origin method with filter " in { 35 | callAndCheckMethod( Main.fFilter0(), "fFilter0") 36 | } 37 | 38 | "MainCall" should "show origin method with withFilter " in { 39 | callAndCheckMethod( Main.withFilter0(), "withFilter0") 40 | } 41 | 42 | "MainCall" should "show origin method with collect " in { 43 | callAndCheckMethod( Main.fCollect0{case "bbb" => "ccc"}, "fCollect0") 44 | } 45 | 46 | "MainCall" should "show origin method with onComplete " in { 47 | var lastError: Option[Throwable] = None 48 | val ec = ExecutionContext.fromExecutor( 49 | Executors.newFixedThreadPool(1), 50 | e=>lastError=Some(e) 51 | ) 52 | Main.fOnComplete0(ec) 53 | Thread.sleep(100) 54 | assert(lastError.isDefined) 55 | assert(checkMethod("fOnComplete0",lastError.get)) 56 | } 57 | 58 | "MainCall" should "show origin method with onFailure " in { 59 | var lastError: Option[Throwable] = None 60 | val ec = ExecutionContext.fromExecutor( 61 | Executors.newFixedThreadPool(1), 62 | e=>lastError=Some(e) 63 | ) 64 | Main.fOnFailure0(ec) 65 | Thread.sleep(100) 66 | assert(lastError.isDefined) 67 | assert(checkMethod("fOnFailure0",lastError.get)) 68 | } 69 | 70 | "MainCall" should "show origin method with onSuccess " in { 71 | var lastError: Option[Throwable] = None 72 | val ec = ExecutionContext.fromExecutor( 73 | Executors.newFixedThreadPool(1), 74 | e=>lastError=Some(e) 75 | ) 76 | Main.fOnSuccess0(ec) 77 | Thread.sleep(100) 78 | assert(lastError.isDefined) 79 | assert(checkMethod("fOnSuccess0",lastError.get)) 80 | } 81 | 82 | "MainCall" should "show origin method with foreach" in { 83 | var lastError: Option[Throwable] = None 84 | val ec = ExecutionContext.fromExecutor( 85 | Executors.newFixedThreadPool(1), 86 | e=>lastError=Some(e) 87 | ) 88 | Main.fForeach0(ec) 89 | Thread.sleep(100) 90 | assert(lastError.isDefined) 91 | assert(checkMethod("fForeach0",lastError.get)) 92 | } 93 | 94 | "MainCall" should "show origin method with transform " in { 95 | callAndCheckMethod( Main.fTransform0(), "fTransform0") 96 | } 97 | 98 | "MainCall" should "show origin method with recover " in { 99 | callAndCheckMethod( Main.fRecover0(), "fRecover0") 100 | } 101 | 102 | "MainCall" should "show origin method with recoverWith " in { 103 | callAndCheckMethod( Main.fRecoverWith0(), "fRecoverWith0") 104 | } 105 | 106 | "MainCall" should "show origin from future for AwaitReady" in { 107 | var lastError: Exception = null 108 | val ec = ExecutionContext.fromExecutor( 109 | Executors.newFixedThreadPool(1) 110 | ) 111 | try { 112 | Main.awaitReady0(ec) 113 | } catch { 114 | case ex: TimeoutException => lastError = ex 115 | } 116 | 117 | Thread.sleep(100) 118 | assert(checkMethod("awaitReady0",lastError)) 119 | } 120 | 121 | "MainCall" should "show origin from future for AwaitResult" in { 122 | var lastError: Exception = null 123 | val ec = ExecutionContext.fromExecutor( 124 | Executors.newFixedThreadPool(1) 125 | ) 126 | try { 127 | Main.awaitResult0(ec) 128 | } catch { 129 | case ex: TimeoutException => lastError = ex 130 | } 131 | 132 | Thread.sleep(100) 133 | assert(checkMethod("awaitResult0",lastError)) 134 | } 135 | 136 | private def callAndCheckMethod(body: =>Future[_],method:String): Unit = { 137 | val f = body 138 | val w = new Waiter 139 | f onComplete { 140 | case Failure(ex) => 141 | val checked = checkMethod(method,ex) 142 | w{ assert(checked) } 143 | w.dismiss() 144 | case _ => if (showException) { 145 | System.err.println("w successfull") 146 | } 147 | w{ assert(false) } 148 | w.dismiss() 149 | } 150 | w.await{timeout(10 seconds)} 151 | } 152 | 153 | private def checkMethod(method:String, ex: Throwable): Boolean = { 154 | if (showException) ex.printStackTrace() 155 | ex.getStackTrace.toSeq.find( _.getMethodName == method ).isDefined 156 | } 157 | 158 | 159 | } 160 | -------------------------------------------------------------------------------- /agent/src/main/scala/trackedfuture/runtime/TrackedFuture.scala: -------------------------------------------------------------------------------- 1 | package trackedfuture.runtime 2 | 3 | import scala.concurrent._ 4 | import scala.util._ 5 | import scala.util.control._ 6 | 7 | object TrackedFuture { 8 | 9 | /** 10 | * this method generate static method in TrackedFuture which later can be substitutued 11 | * instead Future.apply in bytecode by agent. 12 | **/ 13 | def rapply[T](unused: Future.type, body: => T, executor: ExecutionContext): Future[T] = 14 | apply(body)(executor) 15 | 16 | def apply[T](body: => T)(implicit executor: ExecutionContext): Future[T] = { 17 | //inline to avoid extra stack frame. 18 | //val prevTrace = ThreadTrace.retrieveCurrent() 19 | val trace = Thread.currentThread.getStackTrace 20 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 21 | val promise = Promise[T]() 22 | val future = promise.future 23 | val runnable = new Runnable() { 24 | override def run(): Unit = { 25 | prevTrace.setCurrentFuture(future.asInstanceOf[Future[Unit]]) 26 | ThreadTrace.setPrev(prevTrace) 27 | try { 28 | val r = body 29 | promise success r 30 | } catch { 31 | case NonFatal(ex) => 32 | ex.setStackTrace(ThreadTrace.mergeWithPrev(ex.getStackTrace)) 33 | promise failure ex 34 | } 35 | } 36 | } 37 | executor.execute(runnable) 38 | future 39 | } 40 | 41 | def onComplete[T, U](future: Future[T], f: Try[T] => U)(implicit executor: ExecutionContext): Unit = { 42 | val trace = Thread.currentThread.getStackTrace 43 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 44 | future.onComplete(x => trackedCall(f(x), prevTrace)) 45 | } 46 | 47 | def foreach[T](future: Future[T], f: T => Unit)(implicit executor: ExecutionContext): Unit = { 48 | val trace = Thread.currentThread.getStackTrace 49 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 50 | future.foreach(x => trackedCall(f(x), prevTrace)) 51 | } 52 | 53 | def transform[T, S](future: Future[T], 54 | s: T => S, f: Throwable => Throwable)(implicit executor: ExecutionContext): Future[S] = { 55 | val trace = Thread.currentThread.getStackTrace 56 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 57 | // 58 | // note, that changign x => trackedCall(x,t) to trackedCall(_,t) change bytecode 59 | future.transform(x => trackedCall(s(x), prevTrace), x => trackedCall(f(x), prevTrace))(executor) 60 | } 61 | 62 | def andThen[T,U](future: Future[T], pf: PartialFunction[Try[T], U])(implicit executor: ExecutionContext): Future[T] = { 63 | val trace = Thread.currentThread.getStackTrace 64 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 65 | future.andThen{case x => trackedCall(pf(x), prevTrace)}(executor) 66 | } 67 | 68 | def rmap[A, B](future: Future[A], function: A => B, executor: ExecutionContext): Future[B] = { 69 | val trace = Thread.currentThread.getStackTrace 70 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 71 | future.map { a => trackedCall(function(a), prevTrace) }(executor) 72 | } 73 | 74 | def rFlatMap[A, B](future: Future[A], function: A => Future[B], executor: ExecutionContext): Future[B] = { 75 | val trace = Thread.currentThread.getStackTrace 76 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 77 | future.flatMap { a => trackedCall(function(a), prevTrace) }(executor) 78 | } 79 | 80 | def rFilter[A](future: Future[A], function: A => Boolean, executor: ExecutionContext): Future[A] = { 81 | val trace = Thread.currentThread.getStackTrace 82 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 83 | future.map { a => trackedCall( 84 | if (function(a)) a 85 | else 86 | throw new NoSuchElementException("Future.filter predicate is not satisfied") 87 | , 88 | prevTrace) 89 | }(executor) 90 | } 91 | 92 | def collect[A, B](future: Future[A], pf: PartialFunction[A, B], executor: ExecutionContext): Future[B] = { 93 | val trace = Thread.currentThread.getStackTrace 94 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 95 | future.map { a => trackedCall({ 96 | pf.applyOrElse(a, (t: A) => throw new NoSuchElementException("Future.collect partial function is not defined at: " + t)) 97 | }, prevTrace) 98 | }(executor) 99 | } 100 | 101 | def recover[T, U >: T](future: Future[T], pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Future[U] = { 102 | val trace = Thread.currentThread.getStackTrace 103 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 104 | future.recover { 105 | trackedPartialFunction(pf, prevTrace) 106 | }(executor) 107 | } 108 | 109 | def recoverWith[T, U >: T](future: Future[T], pf: PartialFunction[Throwable, Future[U]])(implicit executor: ExecutionContext): Future[U] = { 110 | val trace = Thread.currentThread.getStackTrace 111 | val prevTrace = new StackTraces(trace, ThreadTrace.prevTraces.value) 112 | future.recoverWith { 113 | trackedPartialFunction(pf, prevTrace) 114 | }(executor) 115 | } 116 | 117 | private def trackedPartialFunction[A, B](pf: => PartialFunction[A, B], prevTrace: StackTraces): PartialFunction[A, B] = new PartialFunction[A, B] { 118 | override def isDefinedAt(x: A): Boolean = pf.isDefinedAt(x) 119 | 120 | override def apply(x: A): B = trackedCall(pf(x), prevTrace) 121 | } 122 | 123 | private def trackedCall[A](body: => A, prevTrace: StackTraces): A = { 124 | ThreadTrace.setPrev(prevTrace) 125 | try { 126 | body 127 | } catch { 128 | case NonFatal(ex) => 129 | ex.setStackTrace(ThreadTrace.mergeWithPrev(ex.getStackTrace)) 130 | throw ex 131 | } 132 | } 133 | 134 | 135 | 136 | } 137 | --------------------------------------------------------------------------------