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

Added ScopeListener interface to the ThreadLocalScopeManager #336

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Added ScopeListener interface to the ThreadLocalScopeManager #336

wants to merge 3 commits into from

Conversation

mdvorak
Copy link

@mdvorak mdvorak commented Mar 15, 2019

  • Added new interface ScopeListener
  • Modified ThreadLocalScopeManager
  • Added NoopScopeListener to be used as default
  • Modified tests

Is there anything else missing?

Also, I've created signature of onActivate, thaking directly span:

void onActivate(Span span);

Because consumers should not actually need the scope, only its span - but it is a little weird, having Span in the interface called ScopeListener. What do you think? I have already prepared commit to change it to void onActivate(Scope scope);

@mdvorak mdvorak changed the title #334 - Added ScopeListener interface to the ThreadLocalScopeManager Added ScopeListener interface to the ThreadLocalScopeManager Mar 15, 2019
@mdvorak mdvorak mentioned this pull request Mar 15, 2019
@mdvorak mdvorak marked this pull request as ready for review March 15, 2019 08:58
/**
* Called when outermost scope was deactivated.
*/
void onClose();
Copy link
Member

Choose a reason for hiding this comment

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

Why not pass the span here too?

Copy link
Author

Choose a reason for hiding this comment

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

Then it would make sense to call onActivate with previous span as well (optionally null). I'd think it is better to keep the interface as simple as possible. But its no problem to implement it of course :)

Copy link
Member

Choose a reason for hiding this comment

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

I am not sure why previous span is relevant - if we're on an executor thread that span may be completely unrelated to the one being activated.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, but previous span is the one being deactivated - same situation as onClose. So, if you don't need deactivated span in onActivate, you should not need it in onClose either.

scopeManager.listener.onActivate(toRestore.wrapped);
} else {
scopeManager.tlsScope.remove();
scopeManager.listener.onClose();
Copy link
Member

Choose a reason for hiding this comment

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

Should be done earlier imo

Copy link
Author

Choose a reason for hiding this comment

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

Are you sure? Listener code will then execute in the context of the previous span, not the one passed in as argument. Same applies to onClose.

Copy link
Member

Choose a reason for hiding this comment

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

again, the "previous span" is not really relevant. This function closes the current scope, so it should invoke listener.close(currentScope.span). Then it can do the housekeeping of the stack and re-activation of the previous span.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, maybe we misunderstood each other. Were you talking about onClose or onActivate?
onClose can be called before scopeManager.tlsScope.remove(); and it makes sense. I'll update that.

Calling onActivate before scopeManager.tlsScope.set(toRestore); does not make sense - onActivate would run still with active previous span.

Copy link
Author

Choose a reason for hiding this comment

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

On the second thought, what about renaming methods to onActivated and onClosed, and really call them both after change has happened? After all, this is listener to change, not interceptor. I would naturally expect, that state transition has already happened, when I'm receiving the call.

Copy link
Member

Choose a reason for hiding this comment

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

sgtm

Copy link
Author

Choose a reason for hiding this comment

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

Renamed

@carlosalberto
Copy link
Collaborator

Because consumers should not actually need the scope, only its span - but it is a little weird, having Span in the interface called ScopeListener

I think this is fine, aligning with the recent changes on 0.32 around exposing active Span, not the Scope object itself.

@carlosalberto
Copy link
Collaborator

I'm wondering if supporting multiple listeners could be useful ;)

@yurishkuro
Copy link
Member

Multiple listeners are already possible via composition.

/**
* Called when outermost scope was deactivated.
*/
void onClosed();
Copy link
Member

Choose a reason for hiding this comment

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

I still think it needs to accept the span represented by the scope being closed.

scopeManager.listener.onActivated(toRestore.wrapped);
} else {
scopeManager.tlsScope.remove();
scopeManager.listener.onClosed();
Copy link
Member

Choose a reason for hiding this comment

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

the previous comments got lost - the onClosed must be called regardless of the previous span presence.

Copy link
Author

@mdvorak mdvorak Mar 15, 2019

Choose a reason for hiding this comment

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

And thats exactly what i don't want to do, see discussion in #334.
Most common use-case is setting MDC (or ThreadContext for log4j) - and that uses copy-on-write immutable maps.
So, having to do following sequence:

  1. onActivated(1) - set MDC (copy internal map)
  2. onClose(1) - delete MDC (copy internal map) - useless
  3. onActivated(2) - set MDC (copy internal map)
  4. onClose(2) - delete MDC (copy internal map) - useless
  5. onActivated(1) - set MDC (copy internal map)
  6. onClose(1) - delete MDC (copy internal map) - needed

wastes resources. Consumer cannot know, whether onActivated is coming right after onClose.
Thats why originally wanted only protected method for simple extension, or single method interface, so consumer can decide, what is best. This API should be simplistic, if you need something complex, you can copy whole class.

Copy link
Author

Choose a reason for hiding this comment

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

BTW thats why I originally proposed single method interface, with possible null as argument - its ugly, but efficient and versatile.

Copy link
Member

Choose a reason for hiding this comment

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

You're making assumption that MDC is the only use case. If you don't want to clear MDC from onClosed(), you don't have to, it does not negate the usefulness of the callback for other use cases. For example, see census-instrumentation/opencensus-specs#247

Copy link
Author

@mdvorak mdvorak Mar 15, 2019

Choose a reason for hiding this comment

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

You have to clear it in onClosed(), because you can't leave it set on last deactivation (updated example above, sorry I left it out, see its 6) step).
Not saying its only use case, but its use case that should be supported. Give users freedom to do what they need, don't force them to follow exact and strict pattern.

Only model, that would actually fulfill both use-cases IMO, is ugly but flexible:

interface ScopeListener {
  void onSpanChanged(@Nullable Span activeSpan, @Nullable Span deactivatedSpan);
}

Or, I would revisit protected method and simply inheritance - do everyone whatever they want.

class ThreadLocalScopeManager {
  protected void setThreadScope(@Nullable ThreadLocalScope scope) {
    this.tlsScope.set(scope);
  }
}

this can serve as "around advice". And it would make the class extensible in many ways, which I don't think is a bad thing.

Copy link
Author

Choose a reason for hiding this comment

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

Moved discussion to bottom, it will get lost here on another push.

@carlosalberto
Copy link
Collaborator

@yurishkuro sure, but was wondering if we should support this out-of-the-box ;) (not an important thing, just curious about the possibility)

@yurishkuro
Copy link
Member

We can always add it later, it's a utility class that would dispatch to 2+ underlying listeners. Nobody asked for it yet.

@carlosalberto
Copy link
Collaborator

@yurishkuro Fair enough, sounds great to me ;)

@thegreystone
Copy link
Contributor

So, with the ScopeListener, I would need to track the association between an onActivate and a close of a scope myself, most likely with a thread local. For example, if I start a JFR event in a scope, I would have to add it in a thread local, to close the right one once close is hit.

Brave, for example, uses the concept of span and scope decorators. Upon activation, the scope decorator would expect a new scope to be returned (see ScopeDecorator#decorateScope(...)), into which the event could be pushed and closed when the scope itself is closed. No thread locals required.

@sjoerdtalsma
Copy link
Contributor

@thegreystone I like the decorator idea and I think it has come up before.
However, as I understand it, each tracer can currently assume that all spans are 'their own'.
How do you propose to allow each tracer operate on decorated spans, expose an undecorate or unwrap method of sorts?
Could it be an idea work this out in a separate PR?

*
* @param span Activated span. Never null.
*/
void onActivated(Span span);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the method name should reflect whether the activation happens before or after invoking this method. I'd propose beforeActivated and afterClosed

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree on this. It's clearer having a before or after prefix.

Copy link
Member

Choose a reason for hiding this comment

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

good call

final ThreadLocal<ThreadLocalScope> tlsScope = new ThreadLocal<ThreadLocalScope>();
final ScopeListener listener;
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider making it a list or adding a CompoundScopeListener, which maintains a list of ScopeListeners

Copy link
Member

Choose a reason for hiding this comment

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

composite listener can be another PR, imo, if someone actually needs it.

@mdvorak
Copy link
Author

mdvorak commented Mar 18, 2019

Moving discussion to bottom.

We've identified 2 requirements:

  1. Perform action with span at activation and deactivation, while providing said scope in an argument, for easy handling, as needed in Extend span APIs to support callbacks and access to spancontext census-instrumentation/opencensus-specs#247
  2. Ability to handle change of span (both activation and deactivation of nested scope) in single operation, for MDC update (avoid unnecessary modifications) - this might apply to other handlers as well

Only possible solution I can think of is a bit ugly but universal

interface ScopeListener {
  void onSpanChanged(@Nullable Span activeSpan, @Nullable Span deactivatedSpan);
}

I can't think of use case, where I would need handler before action. If so, you can always replace whole ScopeManager with your own.. Which should still be normal option for anyone requesting special behavior. I just wanted simple way to add configuring MDC for logging, without need to copy-paste scope manager into every app.


import io.opentracing.Span;

public interface NoopScopeListener extends ScopeListener {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering if we really need to expose this one as public.

@yurishkuro
Copy link
Member

@mdvorak I don't think we have an agreement on (2) to have a single operation. Using lambda or 2-method class is, imo, a very minor thing compared to the cleanliness and intuitiveness of the API.

@mdvorak
Copy link
Author

mdvorak commented Mar 18, 2019

@mdvorak I don't think we have an agreement on (2) to have a single operation. Using lambda or 2-method class is, imo, a very minor thing compared to the cleanliness and intuitiveness of the API.

Sorry I mentioned lambda originally, but read carefully previous post. Problem for two-method handler is an implementation, where deactivation always called before activation creates performance (or other) problems. You force implementation to have two separate handlers, when it actually needs to handle both at one step.

@tylerbenson
Copy link

Here's a suggestion... Have the onSpanChanged method be the primary interface, but include an adapter that converts it to the style that @yurishkuro wants. Given the constraints proposed by @mdvorak I don't think it could go the other way.

What does the prior art look like? How does this compare with Brave/Census/etc?

@felixbarny
Copy link
Contributor

What about adding an argument for the previously active/now active span.

  • void beforeActivate(Span spanToActivate, @Nullable Span previouslyActive);
  • void afterDeactivate(Span deactivatedSpan, @Nullable Span nowActive);

@thegreystone This should eliminate the need for maintaining a separate ThreadLocal, right? The advantage compared to Scope wrappers is that this does not allocate additional memory when creating scopes.

@mdvorak would that also solve your MDC use case? You will only have to remove the MDC context if nowActive is null (when you deactivate the first span of that thread).

@mdvorak
Copy link
Author

mdvorak commented Mar 18, 2019

@felixbarny Yes, it would be possible to create efficient handler, but I still find it very complicated to do very simple thing. Also, relying on behavior, where that when afterDeactivate is called with nowActive!=null can be ignored, and I can expect beforeActivate right away seems error prone. From API point of view, I would be relying on specific implementation behavior.

as for 1) I already proposed to provide both previous and next spans, so I agree with you on first point.

@felixbarny
Copy link
Contributor

Also, relying on behavior, where that when afterDeactivate is called with nowActive!=null can be ignored

How is this different when having a single method?

From API point of view, I would be relying on specific implementation behavior.

Not sure what is implementation specific here - the behavior is clearly specified and does not depend on the implementation.

@thegreystone
Copy link
Contributor

@thegreystone This should eliminate the need for maintaining a separate ThreadLocal, right? The advantage compared to Scope wrappers is that this does not allocate additional memory when creating scopes.

Not sure how this would help. I would still need to propagate the event from beforeActivate to afterDeactivate somehow, and the only way I would know how is to use a thread local (in the scope case; in the SpanListener case all bets are off).

@thegreystone
Copy link
Contributor

@thegreystone I like the decorator idea and I think it has come up before.
However, as I understand it, each tracer can currently assume that all spans are 'their own'.
How do you propose to allow each tracer operate on decorated spans, expose an undecorate or unwrap method of sorts?

True, doing this in a general fashion requires a bit of extra consideration. Brave simply returns implementations of their own scope interface, and assume they are the only player in town.

Could it be an idea work this out in a separate PR?

If we have decorators, we probably do not need listeners. And if we have listeners, it will seem a bit excessive to have decorators too. It may be worth it to consider the options. Anyone from Brave involved in OpenTracing?

@thegreystone
Copy link
Contributor

Here are some Brave decorators:
https://github.com/openzipkin/brave/tree/master/context

The JFR use case is the closest to my heart.

@mdvorak
Copy link
Author

mdvorak commented Mar 18, 2019

Scope decorators are great, and that was my first idea, except with current implementation it does not work reliably, because of this if:

public class ThreadLocalScope implements Scope {
    @Override
    public void close() {
        if (scopeManager.tlsScope.get() != this) {
            // This shouldn't happen if users call methods in the expected order. Bail out.
            return;
        }

decorator can never know, whether scope was actually closed or not.

@carlosalberto
Copy link
Collaborator

@mdvorak Besides what others already commented/asked, I suggest you cooking a small case under testbed -if you have enough cycles- to show why you need a specific event design (and why the simple event @yurishkuro has in mind does not suit your needs). I know you posted a small explanation regarding MDC, but having actual code could throw additional light into it ;)

@sjoerdtalsma
Copy link
Contributor

decorator can never know, whether scope was actually closed or not

Don't let that influence the listener vs. decorator discussion please. If that's a 'defect' of the current scope manager implementation, we could address that separately from the decorator issue. We could update scope manager to handle out-of-order closing in a predictable manner for example.

@felixbarny
Copy link
Contributor

@thegreystone This should eliminate the need for maintaining a separate ThreadLocal, right? The advantage compared to Scope wrappers is that this does not allocate additional memory when creating scopes.

Not sure how this would help. I would still need to propagate the event from beforeActivate to afterDeactivate somehow, and the only way I would know how is to use a thread local (in the scope case; in the SpanListener case all bets are off).

I misunderstood what the problem is. Looking at brave's JfrScopeDecorator I understand now. You have to have a reference to the Event instance which has been created on activation when the deactivation happens.

I see two designs to achieve that:

Brave-like scope wrappers

  • Pros:
    • Prooven design
    • Simplicity
  • Cons:
    • Additional allocations for every scope, proportional to the number of scope wrappers
    • The type of the scope changes, so we will likely need a unwrap method

Context object

Introducing a span context object which allows to attach custom key/value pairs to it. You can add custom context objects in the before callback and retrieve them from the map in the after callback.

ScopeContext

  • void add(String key, Object value) (lazily allocates the underlying map)
  • <T> T get(String key)

ScopeListener

  • void beforeActivate(Span spanToActivate, ScopeContext context);

  • void afterDeactivate(Span deactivatedSpan, ScopeContext context);

  • Pros

    • The type of the scope does not change as it is not wrapped
    • Even though we have to allocate ScopeContext objects, we only have to allocate once, no matter how many ScopeListeners are registered
  • Cons

    • Allocation of the ScopeContext object for each scope event
    • Adding things to a map causes allocations (the Map itself and Map.Entry objects)
    • More complicated to work with

Example

public class ExampleScopeListener implements ScopeListener {
    @Override
    public void beforeActivate(Span spanToActivate, ScopeContext context) {
        context.add("foo", "bar");
    }

    @Override
    public void afterDeactivate(Span deactivatedSpan, ScopeContext context) {
        String foo = context.get("foo");
    }
}

@carlosalberto
Copy link
Collaborator

Introducing a span context object which allows to attach custom key/value pairs to it. You can add custom context objects in the before callback and retrieve them from the map in the after callback.

Btw, ScopeContext has been discussed a little bit in the past, so it's probably a good time to re-take it.

@felixbarny
Copy link
Contributor

Can you link to that discussion or summarize some of the discussed use cases?

@carlosalberto
Copy link
Collaborator

Hey @felixbarny

This is the Issue: opentracing/specification#114

@dschulten
Copy link

Since this has been dormant for a while, I wanted to mention that there is an opentracing-contrib module which can do sth quite similar: https://github.com/opentracing-contrib/java-api-extensions

@whiskeysierra
Copy link

whiskeysierra commented Dec 24, 2019 via email

@dschulten
Copy link

dschulten commented Dec 25, 2019

Not really. You can't observe Scopes with that.

That is true, you observe when a Span starts and finishes, and you can track when the root span finishes (e.g. when traceId==spanId). Which is why I said it is similar. At least one can cover the MDC use case with it.

Out of curiosity: What is the compelling reason why one needs to observe a ThreadLocalScope?

@mdvorak
Copy link
Author

mdvorak commented Dec 25, 2019

I believe I discussed that somewhere - Span is completely different from Scope:
Scope is technical, Span is logical
One span can "span" across different threads, especially in reactive app. Scope is activated and deactivated on every thread change, since it is thread-based.
So, when you want to observe tracing output, Span is your thing. If you need to extend tracing library, you might need Scopes.

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.

None yet

9 participants