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

ThreadLocalScopeManager should be extensible #334

Open
mdvorak opened this issue Mar 12, 2019 · 16 comments
Open

ThreadLocalScopeManager should be extensible #334

mdvorak opened this issue Mar 12, 2019 · 16 comments

Comments

@mdvorak
Copy link

mdvorak commented Mar 12, 2019

Adding custom behavior for activating and deactivating scope is real pain - one have to copy ThreadLocalScopeManager and ThreadLocalScope into project and modify them.
It is because while activate can be overriden by ScopeManager, the actual deactivation happens in ThreadLocalScope.close() and is conditional. Even wrapping ThreadLocalScope and hooking to close() won't work. Thus, copy-paste it is.

Main use-case here is setting MDC context. Even with 0.32 and exposed spanId and traceId, there is no straightforward way to set those to MDC (unless I'm oblivious to something). While setting always MDC is a little opinionated and can be part of more automated libraries like for Spring (as stated in #202), ot-java should provide extension point for both apps and libraries to do that with ease.

Now, I'll prepare PR, just pick a solution :)

1) Replace calls to ThreadLocalScopeManager.tlsScope.set() in ThreadLocalScope with protected method in ThreadLocalScopeManager:

protected void setThreadScope(ThreadLocalScope scope) {
  this.tlsScope.set(scope);
}

This has least impact on current code, but exposes class internals. Question is, whether it is actually an issue.

2) Similar to 1), but add listener to ThreadLocalScopeManager instead of protected setter, to allow composition over inheritance:

public interface Listener {
    void onActiveSpanChanged(Span span);
}
final Listener listener;

Whenever ThreadLocalScope calls scopeManager.tlsScope.set(), it also calls scopeManager.listener.onActiveSpanChanged.

3) Create some more sophisticated and complex observer system. Problem with above solutions is, that AutoFinishScopeManager could have exactly same logic. Is it worth adding complex structures to avoid little code duplication?
Also question is, what are actually plans with AutoFinishScopeManager, since it couples app code with specific configuration, and the implementation itself is error-prone IMO (leaking spans, not closing spans).

I prefer solution 2), as it allows very simple use in Java8+. Example for Jaeger and Log4j2:

new JaegerTracer.Builder("sample")
            .withScopeManager(new ThreadLocalScopeManager(this::setThreadContext))
            .build();
// ...
private void setThreadContext(Span span) {
    if (span instanceof JaegerSpan) {
        JaegerSpan jaegerSpan = (JaegerSpan) span;
        ThreadContext.putAll(Map.of(
            "traceId", jaegerSpan.context().getTraceId(),
            "spanId", Long.toHexString(jaegerSpan.context().getSpanId())
        ));
    } else {
        ThreadContext.removeAll(asList("traceId", "spanId"));
    }
}
@mdvorak
Copy link
Author

mdvorak commented Mar 12, 2019

Also relates to opentracing-contrib/java-spring-cloud#92

@sjoerdtalsma
Copy link
Contributor

@tylerbenson
Copy link

I strongly favor the listener option, but I think there should be a onActivate/onDeactivate or onStart/onStop (or some other matching terminology) pair.

@sjoerdtalsma
Copy link
Contributor

I strongly favor the listener option, but I think there should be a onActivate/onDeactivate or onStart/onStop (or some other matching terminology) pair.

Scope: onActivate / onDeactivate ?
Span: onStart / onFinish ?

@carlosalberto
Copy link
Collaborator

I'd also vote to add an event listener layer, as it feels to me as the cleanest option ;)

@mdvorak
Copy link
Author

mdvorak commented Mar 12, 2019

@tylerbenson I was thinking about that, problem is, that as long as its single method interface, its easily usable as lambda :) onActivate/onDeactivate is probably cleanest. But again, for primary use-case, it is actually harmful to handle onDeactivate, when scope changes (e.g. for child span), since all MDC/ThreadContext internal structures are immutable, and any change causes clone of the internal HashMap. And for such listener, you cannot distinguish, whether you are leaving outer scope, or just changing it. And calling onDeactivate for only outer scope seems wrong to me. So, single callback looks most versatile, while not the cleanest.

@sjoerdtalsma This is about scopes only. Lifecycle of spans is far more complex and outside power of the ScopeManager. Span activation in the scope does not mean its start. And deactivation does not have to be its end. If you want span lifecycle observe, you will have to open another issue :)

@sjoerdtalsma
Copy link
Contributor

Lifecycle of spans is far more complex and outside power of the ScopeManager.

Agree, I think is misinterpreted the comment from @carlosalberto 😉..

@mdvorak Have you looked at the interface I use for observing? It passes both the activated / deactivated value and the previous / reactivated value. Maybe that could be of use?

🤷‍♂️ shameless plug:

BTW I intent to release a version soon with the above PR in my library. Then you can use my ScopeManager implementation and register a ContextObserver SPI for it.
As an added benefit you could also add mdc-propagation to your classpath and have that automatically propagate into background threads using the same context aware utility classes.

@mdvorak
Copy link
Author

mdvorak commented Mar 12, 2019

@sjoerdtalsma Well, I think your implementation is a bit too complex for the goal I wanted to achieve. I was more thinking about something like this:
https://github.com/mdvorak/opentracing-java/commit/734ad19a435798ff82d24b640caf24609d3f17c2
If we had Java8 minimum, it would be even shorter.

Also, from Span.finish() docs:

Future calls to {@link #finish} are defined as noops, and future calls to methods other than {@link #context} lead to undefined behavior.

So separate handling, which avoid calling finish twice, should not be necessary.
But you have nice, documented and tested code ;)

@tylerbenson
Copy link

@mdvorak interesting insight regarding:

and any change causes clone of the internal HashMap

I was not aware of that. That makes sense then why you'd want to reduce thrashing and limit it to just the changes. I assume in the case where the root scope is being closed, you'd pass in null to indicate such?

@yurishkuro
Copy link
Member

My preference is for symmetric onActivate/onClose. The latter can be used, for example, to track how much time the span was "runnable" on a given thread.

@mdvorak
Copy link
Author

mdvorak commented Mar 12, 2019

My preference is for symmetric onActivate/onClose. The latter can be used, for example, to track how much time the span was "runnable" on a given thread.

That is not actually a bad idea, dunno why it did not occur to me. This aproach does not enforce unnecessary MDC modifications, yet it provides strong contract. Passing null on deactivating is a bit ugly. But still great for simple lambda handler :)
I'll think about how to best structure it, and cook up a PR.

@mdvorak
Copy link
Author

mdvorak commented Mar 12, 2019

@yurishkuro What about AutoFinishScopeManager? Should this change be added only to ThreadLocalScopeManager, or do you prefer both?

@yurishkuro
Copy link
Member

AutoFinishScopeManager

Isn't it deprecated?

@mdvorak
Copy link
Author

mdvorak commented Mar 12, 2019

AutoFinishScopeManager

Isn't it deprecated?

Not in code, neither in master or v0.32.0: https://github.com/opentracing/opentracing-java/blob/master/opentracing-util/src/main/java/io/opentracing/util/AutoFinishScopeManager.java#L21

@yurishkuro
Copy link
Member

@carlosalberto do we still need AutoFinishScopeManager.java ?

@carlosalberto
Copy link
Collaborator

@yurishkuro It was added as an experimental/esoteric feature, but given the latest changes, guess we can deprecate it (and if any continuation-based framework like Akka needs it, they can either have their own).

Long story short: we should deprecate it, yes.

sjoerdtalsma added a commit to talsma-ict/context-propagation that referenced this issue Mar 13, 2019
Implementation of ScopeManager based on our own AbstractThreadLocalContext class with the following advantages over the 'standard' opentracing-util ThreadLocalScopeManager:

1. Close is explicitly idempotent; closing more than once has no additional side-effects
(even when finishOnClose is set to true).
2. More predictable behaviour for out-of-order closing of scopes.
Although this is explicitly unsupported by the opentracing specification,
we think having consistent and predictable behaviour is an advantage.
3. Support for {@link nl.talsmasoftware.context.observer.ContextObserver}.
See opentracing/opentracing-java#334 why this is an advantage.
sjoerdtalsma added a commit to talsma-ict/context-propagation that referenced this issue May 6, 2019
Implementation of ScopeManager based on our own AbstractThreadLocalContext class with the following advantages over the 'standard' opentracing-util ThreadLocalScopeManager:

1. Close is explicitly idempotent; closing more than once has no additional side-effects
(even when finishOnClose is set to true).
2. More predictable behaviour for out-of-order closing of scopes.
Although this is explicitly unsupported by the opentracing specification,
we think having consistent and predictable behaviour is an advantage.
3. Support for {@link nl.talsmasoftware.context.observer.ContextObserver}.
See opentracing/opentracing-java#334 why this is an advantage.

Signed-off-by: Sjoerd Talsma <[email protected]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants