-
Notifications
You must be signed in to change notification settings - Fork 33
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
Add support for tracing methods asynchronously #86
Conversation
Codecov Report
@@ Coverage Diff @@
## master #86 +/- ##
==========================================
- Coverage 90.98% 90.85% -0.14%
==========================================
Files 30 35 +5
Lines 599 645 +46
Branches 75 76 +1
==========================================
+ Hits 545 586 +41
- Misses 54 59 +5
Continue to review full report at Codecov.
|
@@ -76,6 +79,12 @@ class TraceAspectSpec extends WordSpec | |||
throw new RuntimeException("not-ignored") | |||
} | |||
|
|||
@Traced( |
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.
As opposed to adding an async flag, with aspectj anyway we can add a different joinpoint / pointcut on functions with annotations @Trace
that return a promise (I think). Have you tried that? Unsure off the top of my head if that would work
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.
That would probably work although might require some effort to avoid ambiguity with existing pointcut. The problem with that is that it locks it to supporting only specific types and there is a fairly wide ecosystem of these types out there since Java didn't add one offering a callback until Java 8. Promise[T]
is quite specific to Scala and its uncommon to see in other Java libraries.
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.
Right, I am unclear if it would work. Can we have a test where, if someone sets async to be true, on something that is not async, that everything still works?
Imagine
@Traced(value="sync", async=true)
def plusOne(x: Int): Int = x + 1
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.
Agreed, the tests are a little anemic right now. This PR is more a spike for the purpose of discussion.
I might need to approach the unit tests a little differently as they should validate that log records aren't written until the promise/future is complete and the current helper methods don't really lend themselves to that, short of waiting for a timeout to expire. I'm thinking of mocking the Tracer instead.
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.
The main test that I would like to try out is nested futures (futures with flatmaps).
When I worked with scala Futures in the past, the onComplete of a future was executed in odd ways.
Imagine you have 3 levels of functions / nested futures via flatmaps and maps. You create child spans at each level, so 3 levels deep in a span tree. The onComplete and context that was passed was "off".
Sorry I don't have more information, I have to check out your PR and try and re-create the issue I saw
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.
Also, I think your example is odd. Seems like you have a future that yields a future as a value. That doesn't seem like a valid use case. I am curious if some of the contrived tests I wrote are things that people would never write?
Certainly .flatMap { x => Future { x + 1 } }
is valid
But Future { Future { x } }
is not
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.
Which example is that? I'm trying to duplicate (or at least reinterpret) the unit tests you had around traced futures. Some of the examples are contrived intentionally but I also wouldn't discount my flubbing things up. I've not had the luxury of dedicating my attention to this recently.
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 was playing with going down a path of having TraceAspect
replace the existing Span
with a wrapper AsyncSpan
which internally would use an AtomicInteger
as a reference counter. Invoking a terminal method on Future
like onComplete
would bump that counter up. When that callback would complete it would call stop
, which would decrease the counter, as would when the instrumented future completed. Only when the counter reached zero would the span actually be stopped. I don't think that's quite what you were talking about, though.
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.
That’s a great idea!
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 was thinking that perhaps my old TraceAspect unit tests had test cases that were invalid.
@@ -24,7 +24,7 @@ object AsyncTracingService { | |||
private lazy val services = loadServices() | |||
|
|||
def findTracingService(future: AnyRef): Option[AsyncTracingService] = | |||
services.find(_.supports(future)) | |||
if (future != null) services.find(_.supports(future)) else None |
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.
nitpicky, but...
Option(future).map(f => services.find(_.supports(f))
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.
nitpicky back at you, but: 😁
Option(future).flatMap(f => services.find(_.supports(f))
I've reworked the asynchronous tracing aspect so that it behaves a bit differently. Two spans are created instead of one. The first span represents the synchronous execution of the method. The second span, which is a child of the first span, represents the asynchronous operation represents by the future returned from the method. The second span is popped from the stack of spans and is left open after the method returns. When the future is complete, that triggers the second span to be closed based on the result of the future. That second span is never closed if the following situations happen:
The success of the second span depends on the success of the future and respects the same ignored exceptions. Given that the second asynchronous span is popped from the |
I'll also note that I don't particularly care for the name |
|
||
private def traceMethodAsync(joinPoint: ProceedingJoinPoint, traceAnnotation: Traced): AnyRef = { | ||
val key = traceAnnotation.value() | ||
val asyncKey = key + "-async" |
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.
why do we append -async to the name?
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 creating two spans. I'm giving the second span which represents the async task the suffix -async
. I could change that to do something else, or make it configurable.
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.
Ok, that is where I was confused. I don't understand why we need two separate spans.
We use Money heavily with scala futures, and all tracing is declaritive in the code. We want to give the user the ability to start / stop however we want.
It is a little unclear here with the @Traced
annotation as to when the span actually starts. The way we have done this is that the span starts when the method is invoked and completes when the Future (Promise) ends. Something conceptually like this...
def doSomethingTraced(foo: String): Future[String] = {
val span = startSpan("myName")
val fut = doSomethingInTheFuture(foo)
fut.onComplete { case Success(x) => span.stop(true); case Failure(e) => span.stop(false) }
}
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 see. That is the approach I was taking in the first iteration and I have no problem going back to that model. If I do that then the code above could be reduced to:
@Traced(value = "myName", async = true)
def doSomethingTraced(foo: String): Future[String] =
doSomethingInTheFuture(foo)
…fter the method is invoked until the future is complete
Okie dokie, I've reverted the logic back to the previous implementation, where a single span is created prior to the annotated method being called and that span not being stopped if the method returns a recognized future that is not yet complete. I've updated all of the unit tests to also reflect this change. |
Thumbing this up. Thanks for the contribution!!! Let me know if there are any other pending modifications to this, and if I can merge this before your other PR. |
Great. I am sitting on a couple of additional unit tests but no modification to the current functionality. I was toying with the idea of switching from using the Java ServiceLoader to instead using the same typesafe Config mechanism that Money uses for handlers and other configuration. I reached for ServiceLoader since I was more familiar with it coming from Jersey and it's frequently used for pluggable connectors like this but I'd want it to fit in with the established paradigm used by Money. Kris didn't have an opinion either way. |
Good to merge @HaloFour ? |
|
||
When("the method is invoked") | ||
val promise = Promise[String]() | ||
val future = asyncMethodWithIgnoredException(promise) |
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.
should future
maybe be promise
. I feel like I'm being lied to
Refactored to using typesafe Config to load the Open questions:
|
|
||
object AsyncNotificationHandlerFactory { | ||
|
||
def create(config: Config): AsyncNotificationHandler = { |
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.
What about object AsyncNotificationHandler
and create
as apply
would that look bad at call site? I guess this way you can just import AsyncNotificationHandlerFactory.create
|
||
def add(log: String, message: String) = messages.addBinding(log, message) | ||
def add(log: String, message: String): Unit = { |
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 kind of like it better without the brackets but...
I ended up removing the |
Unless there are further comments regarding implementation or style I believe that this should be ready to merge. |
Okay, I lied. The complexity of Now I think I'm good to merge this PR. 😀 |
I've commited the There are three tests which fail regularly and I don't have a good answer for them. They rely on terminal/non-composed methods on The methods in question are:
|
I wonder if you would be amenable to breaking this into separate PRs. I see two orthogonal concerns here:
The former I think is pretty cut and dry. The latter gets into the weeds of implementation details around each implementation of "future", as we're seeing with |
I think that cutting this up makes sense. |
I've reset this branch back to the commit prior to diving into the Speaking of, I'd love to make that a module of this project but it looks like Money currently targets Java 6. Is that to retain compatibility with older projects or would it be possible to bump the version up to 8? |
Adds support for tracing methods that return objects indicating asynchronous completion of a given task. The span itself remains open until the task is completed as determined by an implementation of
AsyncTraceService
that can support the return type of the method and register for completion. The duration of the method execution itself is logged via a timer on the span.This PR includes support for Scala's
Future[T]
trait.