Skip to content
This repository has been archived by the owner on May 23, 2023. It is now read-only.

Introduce ActiveSpan, ActiveSpan.Continuation, and ActiveSpanSource #115

Merged
merged 48 commits into from
May 11, 2017

Conversation

bhs
Copy link
Contributor

@bhs bhs commented Mar 28, 2017

This is a relatively small diff against what can be found in #111

I created a separate PR since – IMO – it can be difficult to discuss two alternatives in Github PRs unless they're separate, well, PRs.

Delta from #111

  • There is no more activate()-time parameter about finish() behavior. Instead, ActiveSpanHolder.Continuation has internal refcounting. This leads to terser code and (hopefully) fewer bugs: either from forgetting to finish(), or from not-knowing what to pass to activate(), especially in Runnable/Callable wrappers (which was a real problem in my explorations with "real" codebases).
  • Per the above, ActiveSpanHolder and ActiveSpanHolder.Continuation are abstract classes rather than pure interfaces, and thus the refcounting is actually part of OT-Java rather than OT-Java impls.
  • I also addressed a variety of naming and minor semantic concerns from WIP: Add the notion of an "active" Span and io.opentracing.Scheduler #111

About the reference-counting...

IMO the relationship in #111 between Span.finish() and Continuation.deactivate() was borderline unacceptable. While that's addressed (syntactically) in this PR, the result is that Continuation manages an AtomicInt and core opentracing-java gets further into the business of implementation than it has to date.

Desired feedback

I'd love to hear about structural/API-level concerns, see structural/API-level alternatives, and perhaps learn about naming preferences.

Let's not nitpick this review in too much detail; or at least not until the "WIP" prefix is removed. All in due time. :)

@bhs
Copy link
Contributor Author

bhs commented Mar 28, 2017

... you can see how ActiveSpanHolder and MDC can be bound together with the code here: bhs#4

@bhs
Copy link
Contributor Author

bhs commented Mar 28, 2017

@sjoerdtalsma
Copy link
Contributor

A comment about propagating MDC: bhs#4 (comment)

@objectiser
Copy link
Contributor

@bhs Think the ref counting approach is definitely better.

However this approach wouldn't work for something like the camel-opentracing, where there is an event handler called at the beginning and end of a scoped unit of work, and each handler may be called in a different thread. So no guarantee to be able to activate/deactivate a Continuation in the same thread - and therefore the stack wouldn't get unwound.

Its possible that the requirements of camel-opentracing are rare, so not suggesting we change the current PR to accommodate this if not considered a general requirement - just outlining the scenario in case more widely applicable.

README.md Outdated
1. In the method/function that *allocates* the closure/`Runnable`/`Future`/etc, call `ActiveSpanHolder#captureActive()` to obtain a `ActiveSpanHolder.Continuation`
2. In the closure/`Runnable`/`Future`/etc itself, pair calls to `ActiveSpanHolder.Continuation#activate` and `ActiveSpanHolder.Continuation#deactivate`

In practice, the latter is most fluently accomplished through the use of an OpenTracing-aware `ExecutorService` and/or `Runnable`/`Callable` adapter; they can factor all of the above.
Copy link
Member

Choose a reason for hiding this comment

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

might be useful to have an actual code example, as this is arguably one of the most critical (difficult to get right) use cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@yurishkuro no argument – this is what I've done in https://github.com/bhs/opentracing-java/tree/bhs/ash_demo / https://github.com/bhs/opentracing-java/blob/bhs/ash_demo/opentracing-mdc-demo/src/main/java/io/opentracing/mdcdemo/TracedExecutorService.java

If we proceed with this direction, something like the above should be part of OT-contrib (independent of MDC, btw), and we can link to it here.

@yurishkuro
Copy link
Member

One structural comment - I think it would be a lot easier to comprehend this if we defined the Holder and Continuation as interfaces only. The abstract classes are, after all, implementations. And I would expect the abstract classes to use generics for the concrete types of Span, Continuation, etc.

@bhs
Copy link
Contributor Author

bhs commented Mar 28, 2017

@objectiser

However this approach wouldn't work for something like the camel-opentracing, where there is an event handler called at the beginning and end of a scoped unit of work, and each handler may be called in a different thread. So no guarantee to be able to activate/deactivate a Continuation in the same thread - and therefore the stack wouldn't get unwound.

Is there any guarantee that the thread calling the notifier with ExchangeSendingEvent will be the thread that invokes the actual application (handler) code? If not, I'm not sure how anything using TLS would work well here.

Assuming that ^^^ is not a concern, the remaining question is how to deal with the ExchangeSentEvent and the distinct thread it might arrive on. I looked at docs and saw https://camel.apache.org/maven/camel-2.15.0/camel-core/apidocs/org/apache/camel/CamelContext.html#setUseMDCLogging(java.lang.Boolean) ... I wonder if they thread the MDC context through everywhere. If so, that might be a reasonable solution to the context propagation problem more generally.

I also saw https://camel.apache.org/maven/camel-2.15.0/camel-core/apidocs/org/apache/camel/spi/InterceptStrategy.html#wrapProcessorInInterceptors(org.apache.camel.CamelContext,%20org.apache.camel.model.ProcessorDefinition,%20org.apache.camel.Processor,%20org.apache.camel.Processor) – would that work here? At first glance it seems like it would be easier from a threading perspective.

*
* @return a new {@link Continuation} to {@link Continuation#activate()} at the appropriate time.
*/
public final Continuation capture() {
Copy link
Member

Choose a reason for hiding this comment

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

Not quite following this method. Isn't the main use case of "capture" done via tracer.holder().capture(span) which returns Continuation? Why would one need to re-capture an already captured continuation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is to make code like this easier to write:

https://github.com/bhs/opentracing-java/blob/bhs/ash_demo/opentracing-mdc-demo/src/main/java/io/opentracing/mdcdemo/TracedRunnable.java#L18

It's def just a convenience, though.

"Capture" is when the incRef happens (rather than activate), which is really important for async stuff to work correctly.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Need to think about this a bit more. On the surface it seems unnecessary if we minimize the number of ways to get to that point. I.e. when TracerRunnable is created, the continuation can be captured from the top, via holder.captureActive(). Otherwise I would call this method explicitly "increment reference count", because "capture" seems misleading.

@bhs
Copy link
Contributor Author

bhs commented Mar 28, 2017

@yurishkuro

I think it would be a lot easier to comprehend this if we defined the Holder and Continuation as interfaces only

I started off that way, but had these comments in the interface definitions that basically read like code... "Implementations are expected to check X, call Y, then do Z". Esp around the refcounting. YMMV.

I haven't formed an opinion about the generics, but no objection assuming there's no downside (and I can't think of one off-hand).

* remain.
*/
final void decRef() {
if (0 == refCount.decrementAndGet()) {
Copy link
Member

Choose a reason for hiding this comment

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

just wondering - is there a race condition here, if another thread simultaneously re-activates this continuation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

incRef happens at capture-time, not activation-time.

Re the race condition: I don't think so, at least in practice, as

  1. if deactivate() hasn't been called for any Continuation, that means the refcount is > 0
  2. any capture()ing must happen before deactivate() is called
  3. thus, the capture/incRef will always happen before the deactivate/decRef

@yurishkuro
Copy link
Member

there seems to be a bit of an overlap in the APIs of Holder and Continuation. Maybe we could start without helper methods and define an absolutely minimal interface first?

@bhs
Copy link
Contributor Author

bhs commented Mar 28, 2017

@yurishkuro

Maybe we could start without helper methods and define an absolutely minimal interface first?

This may just be a restriction of my feeble brain, but I have been mainly focused on making the calling code look and feel reasonable, and I can't do that without the helpers. I can create a branch (for diffing purposes) with helpers stripped out if you'd like. Thoughts?

@yurishkuro
Copy link
Member

I have been mainly focused on making the calling code look and feel reasonable, and I can't do that without the helpers

maybe it's a sign of gaps in the API. For example, holder.activeSpan() should be equivalent to holder.active().span(), so holder.active() may need to return some singleton, non-nil, no-span continuation. And if so, holder.activeSpan() become redundant.

I basically goes to my comment about redundancy in the two interfaces, which makes me think there should be a more explicit conceptual separation between the two. If we can define clear separation, then we could have a completely distinct set of methods (e.g. holder.capture() but not continuation.capture()). As it stands, I am having a hard time explaining to myself the precise roles of the two entities.

@objectiser
Copy link
Contributor

@bhs I think there are workarounds for camel, so that isn't a problem, just highlighting the scenario in case relevant more generally.

@wu-sheng
Copy link
Member

I basically goes to my comment about redundancy in the two interfaces, which makes me think there should be a more explicit conceptual separation between the two. If we can define clear separation, then we could have a completely distinct set of methods

Agree. Especially for someone like me... this is like an English word puzzle to me... That is why, for these days, I just watch, but not comment.

Hope we can provide concepts and responsibilities of the Interface ActiveSpan... first, like OT spec did. Then we can discuss the Java code details.

@bhs
Copy link
Contributor Author

bhs commented Mar 30, 2017

@yurishkuro @wu-sheng

I understand where you're coming from... I'm tinkering with different naming and different approaches.

I am reluctant to fully abstract things into interfaces for the reason I cited earlier. I tried the pure-interface approach and the comments felt a little absurd (dictating precise refcounting semantics, e.g.), esp in a language that supports abstract classes for exactly this reason. YMMV?

@pavolloffay
Copy link
Member

pavolloffay commented Mar 31, 2017

I like something about this refcounting approach, but it is also quite fancy. I would be happier if it was optional e.g. just another SpanHolder impl, so users could choose what is the best for them.

I think an active span API should:

  • honor OT spec
  • be bullet proof as much as possible

example 1: childOf when parent is finished

        tracer.buildSpan("parent")
                .startAndActivate()
                .span()
                .finish();

        tracer.buildSpan("child")
                .start()
                .finish();

        List<MockSpan> mockSpans = tracer.finishedSpans();
        Assert.assertEquals(2, mockSpans.size());
        Assert.assertEquals(mockSpans.get(0).context().spanId(), mockSpans.get(1).parentId());

parent is referenced in children as childOf, but parent has been finished before span child started.

example 2: multiple span finish:

        span parent = tracer.buildSpan("parent")
                .start();

        tracer.holder().capture(parent).deactivate();
        tracer.holder().capture(parent).deactivate();

@bhs bhs changed the title WIP: Introduce ActiveSpanHolder and ActiveSpanHolder.Continuation WIP: Introduce ActiveSpanSource, Continuation, and Handle Apr 1, 2017
@bhs
Copy link
Contributor Author

bhs commented Apr 1, 2017

@pavolloffay @yurishkuro @sjoerdtalsma @objectiser @wu-sheng thanks for your comments.

I did a major rev of this (really could be its own PR, but having three WIP PRs for the same issue seems absurd). The diff, basically:

  • Moved to interfaces for the, well, interfaces... I still have a helper impl just to show myself how it could work, but that's no longer important to review.
  • I separated the Continuation interface (which was doing too much work) into two pieces: more details below

Here's a simple usage example from the README: https://github.com/opentracing/opentracing-java/pull/115/files#diff-04c6e90faac2675aa89e2176d2eec7d8R107

Here's the updated MDC demo code: https://github.com/bhs/opentracing-java/pull/4/files#diff-885b8e99541d8b2fee5d686273d0e199R47

And here's the core API with "real" comments removed, and quick notes about refcounting added:

public interface ActiveSpanSource {

    interface Handle extends Closeable {
        Span span();

	// On return: refcount--
        void deactivate();

	// On return: refcount++
        Continuation defer();
    }

    interface Continuation {
	// On return: refcount unchanged
        Handle activate();
    }

    // On return: refcount unchanged
    Handle active();

    // On return: refcount = 1
    Handle adopt(Span span);
}

PTAL.

@bhs
Copy link
Contributor Author

bhs commented Apr 1, 2017

@pavolloffay re your concerns from the previous rev of this:

example 1: childOf when parent is finished

        tracer.buildSpan("parent")
                .startAndActivate()
                .span()
                .finish();

        tracer.buildSpan("child")
                .start()
                .finish();

        List<MockSpan> mockSpans = tracer.finishedSpans();
        Assert.assertEquals(2, mockSpans.size());
        Assert.assertEquals(mockSpans.get(0).context().spanId(), mockSpans.get(1).parentId());

parent is referenced in children as childOf, but parent has been finished before span child started.

A few thoughts...

  • In my revised API, try-with-resources would be strongly encouraged. I guess I'm not concerned about this example because it's literally using undefined behavior (i.e., accessing a Span after finish() is invoked).
  • That said, we could always require that finish() causes all relevant Handles to deactivate immediately. It would just be more bookkeeping and cost for impls in a case that's, again, intentionally undefined

example 2: multiple span finish:

        span parent = tracer.buildSpan("parent")
                .start();

        tracer.holder().capture(parent).deactivate();
        tracer.holder().capture(parent).deactivate();

Again, this is undefined behavior. If we had to define it, I would say that the N ≥ 2 calls to deactivate() are guaranteed to be noops... that will create cost for impls but would protect against this.

@yurishkuro
Copy link
Member

I have a couple of suggestions that might make this API a bit more fluent.

  1. Instead of tracer.spanSource(), how about tracer.activeSpan()?

  2. I wonder if ActiveSpanSource + Holder can be combined into a single interface

public interface ActiveSpan extends Span, Closable {
    Continuation defer();
    ActiveSpan replace(Span); // instead of adopt()
    void deactivate(); // == finish() when refcount==1

    interface Continuation {
        // invariant: 
        //    ActiveSpan span = cont.activate();
        //    span == tracer.activeSpan()
        ActiveSpan activate();
    }
}

I haven't checked all the use cases, but this seems to simplify the usage quite a bit, and does away with some methods that feel redundant, e.g. instead of tracer.spanSource().active().span() we just have tracer.activeSpan().

@bhs
Copy link
Contributor Author

bhs commented Apr 2, 2017

@yurishkuro thanks for the suggestions. Some commentary:

Instead of tracer.spanSource(), how about tracer.activeSpan()?

If this is an "in addition to" rather than an "instead of": sure, I guess? Various people were asking that the PR restrict itself to the bare interfaces, hence the lack of helpers in the PR. That said, I actually don't envision people calling active* in application code very often... probably more common to start new spans with implicit parents and/or to have active* calls focused on things like logging library bridges.

If the suggestion is truly "instead of": I wanted the ActiveSpanSource impl to be independent of the Tracer impl... i.e., the ActiveSpanSource impl might be MDC-specific, but it wouldn't be Zipkin-specific. If we remove an accessor for ActiveSpanSource, then there's no OT-standard API for creating Handle instances, which is vitally important for the decoupling requirement above.

I wonder if ActiveSpanSource + HolderHandler can be combined into a single interface

The arrangement of the inner/outer classes might be weird/bad, but IMO there really are three concepts here that warrant separate objects:

  1. A long-lived object that wraps/adopts Span instances: ActiveSpanSource in this PR
  2. A short-lived object that marks an "active" period for a Span: ActiveSpanSource.Handle in this PR
  3. A short-lived object that represents a continuation: obviously ActiveSpanSource.Continuation in this PR

Combining the first and second into a single API seems like a weird design due to the lifetime differences... we kind of want a "static abstract" method which is of course impossible.

We could restructure things like so:

public interface SpanHandle {
    Span span();
    void deactivate();
    Continuation defer();
    interface Continuation {
        SpanHandle activate();
    }

    interface ActiveHandleSource {
        SpanHandle adopt/replace/whatever(Span);
        SpanHandle active();
    }
}

BTW, regarding inheriting from Span: that can definitely be done, and I tried as much. Having done so, though, I'm not very enthusiastic... try-with-resources for a Handle and a Span both seem reasonable but are fundamentally different. By keeping the interfaces separate I was hoping that it'd be harder to get this wrong as a user.

@yurishkuro
Copy link
Member

yurishkuro commented Apr 2, 2017

I meant truly "instead of". The only method on the current ActiveSpanSource that requires some notion of the "manager" is adopt(), which I was calling replace(). The other two methods, defer() and deactivate() can be just as easily defined on the "active span". In my version the Tracer becomes "active span source", so that we don't need to have an extra delegation. In fact, if we don't like adopt/replace on the span, it can move to the Tracer (could be called something else, setActiveSpan, withActiveSpan, etc.)

Note that my proposal does not preclude custom implementations of ActiveSpanSource, the objection is simply to remove additional objects/handles that the user needs to deal with. I.e rather than getting a Handle and then a Span from it, just get ActiveSpan that implements both.

public interface ActiveSpanSource { // can be implemented by Tracer
    ActiveSpan activeSpan();
    ActiveSpan withActiveSpan(Span span); // aka adopt(), replace()

    // helper, could be dropped
    ActiveSpan startAndActivate(...); // == withActiveSpan(startSpan(...))
}

public interface ActiveSpan extends Span, Closable {
    Continuation defer();
    void deactivate(); // == finish() when refcount==1

    interface Continuation {
        // invariant: 
        //    ActiveSpan span = cont.activate();
        //    span == tracer.activeSpan()
        ActiveSpan activate();
    }
}

This ^^^ whole framework can be a completely standalone add-on, in fact, i.e. it can be implemented as a wrapper tracer that implements ActiveSpanSource, or a specific Tracer implementation can implement ActiveSpanSource interface by taking real ActiveSpanSource (although there's a bit of a circular dependency in startAndActivate - the problem with helper methods. If this makes it to OT API it won't be a problem).

Another thought - if we want ActiveSpanSource to be pluggable, then it probably needs another SPI that is not directly exposed to OT user.

try-with-resources for a Handle and a Span both seem reasonable but are fundamentally different

My assumption was that if you get ActiveSpan instance, try-with-resources would work differently on it than if it was a plain Span. Because in practice it's likely going to be a wrapper around a Span, it can override the close -> deactivate(), instead of close -> finish on the underlying Span.

Anyway, I agree that inheritance may create a different mental overhead, I just like having to write tracer.activeSpan() instead of tracer.spanSource().active().span(). I still think it's beneficial to have ActiveSpanSource merely as an interface that can be directly implemented by the Tracer, rather than exposing it via composition.

@bhs
Copy link
Contributor Author

bhs commented Apr 2, 2017

The only method on the current ActiveSpanSource that requires some notion of the "manager" is adopt(), which I was calling replace(). The other two methods, defer() and deactivate() can be just as easily defined on the "active span".

Yes, exactly. That's why there are two interfaces: one for the manager (adopt) and one for the instance (defer and deactivate). IMO things like this "end badly" when one squints about object lifetimes and convolves these separate concepts into a single API.

This is certainly not to say that this PR represents the only reasonable way forward... I would just argue against putting all three of the above into one interface.

Putting "adopt" (or whatever it's called) on Tracer is not unreasonable, though again some sort of SPI-like ActiveSpanSource still has separation-of-responsibilities benefits. (Basically what you're saying with I still think it's beneficial to have ActiveSpanSource merely as an interface that can be directly implemented by the Tracer, rather than exposing it via composition. – no strong objection from my end on this)

Per your point about how almost all of this can be built outside of the core API: I agree with that at a literal level, though the benefit for users is lost if OT callers (esp in 3rd-party instrumentation code) can't assume that buildSpan() will automatically find an implicit parent Span/SpanContext. For that reason I still like the idea of pushing this into the OT-api artifact as soon as we feel like we've converged, as only then can the "virtuous cycle" of this API start to take hold.

To sum up: I think we both agree that the "manager" aspects could be shared with Tracer in various ways (composition, extension, etc). I think we also both agree that having ActiveSpan extend Span is plausible though has some tradeoffs. What are we actively disagreeing about at this point that "matters"? :) I just want to be sure to focus the discussion where we need to.

@yurishkuro
Copy link
Member

What are we actively disagreeing about at this point that "matters"?

Don't think we do, I like the current approach. There are some naming questions we can address later, but overall I think a separation of "active span source/manager", "active span (holder)", and "continuation" has been very useful, and the API has become very lean.

README.md Outdated
1. Start the `Span` via `Tracer.startAndActivate()` rather than via `Tracer.start()`; or, if the `Span` was already `start()`ed, call `ActiveSpanSource#adopt(span)`. Either route will yield an `ActiveSpanSource.Handle` instance that's "adopted" the `Span`.
2. In the method/function that *allocates* the closure/`Runnable`/`Future`/etc, call `ActiveSpanSource.Handle#defer()` to obtain an `ActiveSpanSource.Continuation`
3. In the closure/`Runnable`/`Future`/etc itself, invoke `ActiveSpanSource.Continuation#activate` to re-activate the `ActiveSpanSource.Handle`, then `deactivate()` it (or use try-with-resources for less typing).

Copy link
Contributor

Choose a reason for hiding this comment

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

So many verbs in a small bit of text... start, activate, adopt, allocate, defer, re-activate and deactivate ...
By the way, I think they're all much better chosen than previous iterations, but I think a reader trying to discover what OpenTracing is will get absolutely overwhelmed by these couple of bullets 😉

@bhs
Copy link
Contributor Author

bhs commented Apr 3, 2017

@yurishkuro 🆒. I did some refactoring and ended up with https://github.com/bhs/opentracing-java/compare/bhs/ash_demo...bhs:bhs/outer_active_span?expand=1#diff-5e4b55691403ed8b93d2209691a930d3R16 . I'll leave those commits out of this PR's branch until I get some positive feedback.

Here's the important part:

// Note the "Span" interface parent. Def cleans up some calling code.
public interface ActiveSpan extends Closeable, Span {
    void deactivate();
    Continuation defer();

    // I like how Continuation is inside ActiveSpan... makes the relationship clear.
    interface Continuation {
        ActiveSpan activate();
    }

    // I'm less sure about Source being inside ActiveSpan, since the outer class is now
    // generaeted by the inner (admittedly static) class. We could always split Source
    // out into its own file/etc.
    interface Source {
        // This can move to Tracer (as a convenience) once the rest of the dust has settled.
        ActiveSpan active();

        ActiveSpan adopt(Span span);
    }
}

... IMO we are getting closer to something I actually like.

*
* @return the {@link ActiveSpan active span}, or null if none could be found.
*/
ActiveSpan activeSpan();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if this has already been discussed, but from a user's standpoint I'd prefer returning a NoopSpan over returning null so that they don't have to do null checks just to add a simple tag.

If the tag is expensive to compute, it would however be beneficial if the user could differ between proper and noop spans. But rather than checking for nulls or a global NOOP constant I propose to introduce a isSampled() method on BaseSpan. From the user's perspective, it does not really matter wether there is no active span or the active span is not sampled. They only want to avoid computing a expensive tag if not necessary.

I think this is something we should decide on before merging, or at least before releasing a new version as this would be a somewhat breaking change (although remaining binary compatibility) to introduce later (check for null vs check for isSampled()).

Copy link
Member

Choose a reason for hiding this comment

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

Are there no valid use-cases where you need the null value? E.g. only create a span if there's no active one etc?

the general isSampled() functionality is tracked by opentracing/specification#8

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably there are use cases for that. But I think the default use case is to set simple tags or add logs. IMHO an API should be optimized for the default use case. So maybe add ActiveSpanSource#hasActiveSpan() and let #activeSpan() return noops?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@felixbarny this has already been discussed, yes. Unfortunately the github thread here is so convoluted that I can't even find the relevant comment. :-(

I am 💯% happy to add an issue about this (and block release of a new version of OT-Java accordingly), but IMO it can be decoupled from this PR. Just to prep you for my point-of-view: I generally think instrumentation will either (a) create a new Span, in which case there's no need for a null check regardless, or (b) be at the layer of a logging library or similar where the verbosity of a null check is fine.

If we actually think that it's going to be commonplace to use the OT interface as the logging API (rather than a registrant of the logging API), then I would be in favor of what you propose. That seems like the essential question.

(I'm less convinced about arguments involving tags: they are almost always set at start()-time in my experience)

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for the separate issue

I think this would actually be the main api end users would use.

The use case that I have in mind is when ot is used by an agent, users should still be able to add custon tags. For example, I've implemented a search analytics Kibana dashboard on top of stagemonitor and OT for a customer. It works by adding meta info about the search via tags like no of hits and the search term. For these kinds of things it would be nice if null checks could be avoided.

* Set a key:value tag on the Span.
*/
// overloaded 3x to support the BasicType concern
BaseSpan setTag(String key, String value);
Copy link
Contributor

Choose a reason for hiding this comment

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

We could do:

public interface BaseSpan<S extends BaseSpan> {
    [...]
    S setTag(String key, String value);
    [...]
}

public interface Span extends BaseSpan<Span> {
    [...]
}

That way, chaining would still be possible in a type safe way.


In practice, all of this is most fluently accomplished through the use of an OpenTracing-aware `ExecutorService` and/or `Runnable`/`Callable` adapter; they can factor most of the typing.

#### Automatic `finish()`ing via `ActiveSpan` reference counts
Copy link

Choose a reason for hiding this comment

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

I'm happy with ThreadLocalActiveSpan being moved to opentracing-util, but this section still suggests that automatic finishing via ref counting will happen all the time. IMHO this section should be explicit on whether all tracers must implement ref counting (via ThreadLocalActiveSpan or any other means) or if it is a optional behavior that the tracer implementations can decide to support or not.

Copy link
Member

Choose a reason for hiding this comment

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

If the behavior of active span is different between implementations, then frameworks wouldn't know what the right way is to instrument their code. So the spec must be unambiguous that de-activating the last instance of the active span always finishes the span. Is there another way to achieve that without ref counting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whether there's an AtomicInteger is an implementation detail, but I don't see how this spec could be implemented without reference counting of some sort.

@bhs
Copy link
Contributor Author

bhs commented May 3, 2017

Fair warning: I'm planning to do a thorough round of nitpick-removal and merge this sometime tomorrow.

Copy link
Contributor

@objectiser objectiser left a comment

Choose a reason for hiding this comment

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

Only minor points - good job!

/**
* Set a key:value tag on the Span.
*/
// overloaded 3x to support the BasicType concern
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove?

* {@code span.log(timestampMicroseconds, Map.of("event", "exception", "payload", stackTrace))}
**/
S log(String eventName, /* @Nullable */ Object payload);
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: line gap

<artifactId>opentracing-noop</artifactId>
</dependency>
<dependency>
<groupId>io.opentracing</groupId>
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicate dependency.

Copy link
Member

Choose a reason for hiding this comment

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

Could this module depend only on util? We would like to use ThreadLocalActiveSpanSource in MockTracer and maybe as a default.

If you do the change then there will be a cyclic dependency because util depends on ot-mock, but that can be removed MockTracer is not used in ot-util.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

btw, to follow up on this one point @pavolloffay: ot-util has tests that depend on MockTracer.

public void deactivate() {
if (source.tlsSnapshot.get() != this) {
// This shouldn't happen if users call methods in the expected order. Bail out.
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be logged to make finding such issues?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's a good question... I am uneasy about adding logging statements at this layer. I could perhaps add a private boolean strict with an overloaded constructor and have it throw an exception here in strict mode?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure - needs further thought, but not a blocker.


// We can't use 1.7 features like try-with-resources in this repo -- argh.
//
// F*** IT, WE'LL DO IT LIVE!
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove?

/**
* @deprecated use {@link #startManual} or {@link #startActive} instead.
*/
Span start();
Copy link
Member

Choose a reason for hiding this comment

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

annotate with @Deprecated

<artifactId>opentracing-noop</artifactId>
</dependency>
<dependency>
<groupId>io.opentracing</groupId>
Copy link
Member

Choose a reason for hiding this comment

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

Could this module depend only on util? We would like to use ThreadLocalActiveSpanSource in MockTracer and maybe as a default.

If you do the change then there will be a cyclic dependency because util depends on ot-mock, but that can be removed MockTracer is not used in ot-util.

/**
* Create a new MockTracer that passes through any calls to inject() and/or extract().
*/
public MockTracer(Propagator propagator) {
this.propagator = propagator;
this(NoopActiveSpanSource.INSTANCE, propagator);
Copy link
Member

Choose a reason for hiding this comment

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

As mentioned above an actual impl would be better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@pavolloffay you mean instead of a noop? This runs into problems with circular dependencies since the ThreadLocalActiveSpanSourceTest depends on the MockTracer. We allow the programmer to manually specify an ActiveSpanSource via an overloaded constructor; I thought that was sufficient.

public void makeActiveSpan() throws Exception {
Span span = mock(Span.class);

// We can't use 1.7 features like try-with-resources in this repo -- argh.
Copy link
Member

Choose a reason for hiding this comment

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

Tests can use java 8. It could be set in the root pom.

if (this.startMicros == 0) {
this.startMicros = MockSpan.nowMicros();
}
return new MockSpan(MockTracer.this, this.operationName, this.startMicros, initialTags, this.firstParent);
if (firstParent == null && !ignoringActiveSpan) {
firstParent = (MockSpan.MockContext)activeSpanContext();
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, I thought we decided to do startSpan-time?

* or
* {@code span.log(timestampMicroseconds, Map.of("event", "exception", "payload", stackTrace))}
**/
S log(String eventName, /* @Nullable */ Object payload);
Copy link
Member

Choose a reason for hiding this comment

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

add @Deprecated on the method below too.

*/
SpanBuilder asChildOf(Span parent);
SpanBuilder asChildOf(BaseSpan parent);
Copy link

Choose a reason for hiding this comment

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

any particular reason for using a raw type here instead of something like <T extends BaseSpan<?>> SpanBuilder asChildOf(BaseSpan<T> parent)?

Copy link
Contributor

Choose a reason for hiding this comment

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

SpanBuilder asChildOf(BaseSpan<?> parent) should be good enough as the return type is independent from the actual generic type of BaseSpan.

Copy link
Contributor

@mabn mabn left a comment

Choose a reason for hiding this comment

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

I have a questions regarding one usage category. To make the example concrete let's assume that there's a generic HTTP client, with own threadpool, all hidden under some abstraction layers.

Naturally the client is instrumented, creates client-side span and injects span context.

I use this client to execution operation "op" in service "Service". It's understandable that I want the client-side span created inside the client to have operation_name based on the service that it calls, e.g. "call Service#op".

How to do that - pass operation name (or even tags) "higher layer" ?

The client has access only to the active span and some http-level details. I could set it as a tag in the parent span, but that would pollute parent span. I could add the name to baggage temporarily but that would abuse baggage and requires a way to remove it from baggage later.

It seems that the only way is some explicit propagation of these parameters - operation name for the future child span, possibly also tags - and it needs to be supported by the API of the used library (http client in this case)

@objectiser
Copy link
Contributor

@mabn This is an interesting problem, and relevant for many of the framework integrations in the opentracing-contrib repo. However I think it would be best raised as a separate issue rather than getting mixed up with this PR.

@objectiser
Copy link
Contributor

@mabn I've created an issue related to this, as a similar question came up in the okhttp framework integration repo: opentracing-contrib/meta#25

@mabn
Copy link
Contributor

mabn commented May 8, 2017

@objectiser thanks!

@bhs
Copy link
Contributor Author

bhs commented May 9, 2017

@pavolloffay @objectiser @ivantopo @felixbarny thanks for your recent comments, I have tried to address them. I also did a full pass over the PR and cleaned up a bunch of comments/etc.

@bhs
Copy link
Contributor Author

bhs commented May 11, 2017

Ok people... Since this PR apparently broke Github (https://gitter.im/opentracing/public?at=59146a4892217cca58669dfa) I am going to merge.

I will also create an issue in this repo to track things we collectively agreed to defer until this was in.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.