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

ScopeManager #113

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

pauldraper
Copy link

@pauldraper pauldraper commented Aug 19, 2018

Addresses #112

See https://github.com/opentracing/specification/blob/f7ca62c9/rfc/scope_manager.md

Continuation passing style

I first tried adhering to the draft RFC (somewhat working code), but it got clumsy, particularly for the Zone.js implementation. Just about every JS continuation library uses continuation passing style (Zone.js, Node.js domains, continuation-local-storage, etc.), and I believe it's the correct choice. Plus, the existing RFC IMO has a lot of ambiguity for the implementer. I added my findings to opentracing/specification#126.

Support

Unfortunately, there just isn't a great universal, out-of-the-box CLS solution.

There are two things that can be addressed by CLS: native async syntax (async/await), and common APIs (e.g. fs.readFile).

The are a surprising number of issues with current solutions. https://github.com/othiym23/node-continuation-local-storage is widely used, but can't be used with async/await which in 2018 is increasingly common. https://github.com/gms1/node-async-context can be, but (so far) doesn't have a lot of adoption and requires Node 8+. https://github.com/angular/zone.js/ has related TC39 proposal, but it's been stalled for two years.

In the end, the JS community has not figured out CLS (yet). I added a few implementations with very minimal dependencies that between them have fairly broad coverage. (See README.md for details.)

Implementations:

  • AsyncHookSpanManager - uses Node.js async_hooks
  • AsyncWrapSpanManager - uses async-hook-jl which uses Node.js AsyncWrap
  • ZoneSpanManager - uses Zone.js

In the future, I expect the situation to improve. But this seems a good start, these wrappers are rather simple (~50 lines each).

@coveralls
Copy link

coveralls commented Aug 19, 2018

Coverage Status

Coverage increased (+1.5%) to 90.019% when pulling 71e124f on rivethealth:pauldraper/scope-manager into 0f14554 on opentracing:master.

See https://github.com/opentracing/specification/blob/f7ca62c9/rfc/scope_manager.md
However, use continuation passing style which better fits JS and available APIs.

Implementations:
* AsyncHookSpanManager - uses Node.js async_hooks
* AsyncWrapSpanManager - uses async-hook-jl which uses Node.js AsyncWrap
* ZoneSpanManager - uses Zone.js
@pauldraper
Copy link
Author

pauldraper commented Aug 19, 2018

Update: looks like https://github.com/RisingStack/jaeger-node and https://github.com/RisingStack/opentracing-auto use async_hooks. I believe that would be the most common method of CLS, since if you use Node.js and a recent version of it, it has the fewest drawbacks.

@@ -88,6 +144,11 @@ export class Tracer {
options.references = [childOf];
}
delete(options.childOf);
} else if (!options.references) {

Choose a reason for hiding this comment

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

Java and Python have an "ignore active span" option to prevent this automatic reference. Any chance you'd want to follow suite?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, this should

```

JavaScript support for continuation local storage is a patchwork.
This library includes simple wrappers around a few common APIs for continuation contexts.
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be selected automatically for the user according to the environment. This will avoid the user needing extensive knowledge of all the possible ways to do context propagation in JavaScript.

I've mentioned how I think this should be selected at the bottom of #112 (comment)

tracer.activeSpan() // span
})();
});
tracer.activeSpan() // null
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these be on tracer.scopeManager() instead?

new AsyncHookSpanManager();
```

**AsyncWrapSpanManager**
Copy link
Contributor

Choose a reason for hiding this comment

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

Since AsyncWrap cannot have multiple hooks, and has many bugs, possible crashes and memory leaks in older Node versions, I would probably skip it entirely and add something based on a fixed fork of async-listener as described in #112 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, in my experience it's also slower than async-listener.

* @return {A} the return value of the executed function.
* @template A
*/
async runSpanAsync<A>(name: string, options: SpanOptions, f: () => Promise<A>): Promise<A> {
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 I understand why this is necessary as await can be used on any promise which can be returned by runSpan as well.

Copy link
Author

Choose a reason for hiding this comment

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

It's necessary if you want to stop the span once the promise is resolved.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not simply detect if the returned value is a promise in runSpan and handle it there?

* @return {A} the return value of the executed function.
* @template A
*/
runSpan<A>(name: string, options: SpanOptions, f: () => A): A {
Copy link
Contributor

Choose a reason for hiding this comment

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

This name sounds a bit weird to me, as this method doesn't run a span.

Copy link
Author

Choose a reason for hiding this comment

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

Other suggestion?

It starts and stops (runs?) the span.
It runs the function, with a new active span for its duration.

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 I would actually remove it completely for the first version, to keep the API surface as small as possible. Then more helpers can be added over time if necessary and discussed individually.

*
* @return {SpanManager} - the span manager, which may be a noop.
*/
spanManager(): SpanManager {
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 it this could be called simply scope(). It makes sense to call tracer.scope().span() and tracer.scope().run() because these actions are indeed on the current scope.

*
* @return the active Span. This is a shorthand for tracer.spanManager().active().
*/
activeSpan(): Span | null {
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to my comment below, I don't see the point of this since the alternative is not any more involved.

});

/**
* ScopeManger using Node.js {@link async_hooks|https://nodejs.org/api/async_hooks.html}
Copy link

Choose a reason for hiding this comment

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

Suggested change
* ScopeManger using Node.js {@link async_hooks|https://nodejs.org/api/async_hooks.html}
* ScopeManager using Node.js {@link async_hooks|https://nodejs.org/api/async_hooks.html}

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.

4 participants