-
Notifications
You must be signed in to change notification settings - Fork 180
Scope Manager should use continuation passing #126
Description
https://github.com/opentracing/specification/blob/master/rfc/scope_manager.md
Problems
1. Scope Manager is hard to understand and easy to misuse
- Must I always close an activated scope?
- If the thread exits, is the scope deactivated (and the span closed)?
- What happens if I close a scope twice? (Spec answers: undefined)
- Can I close a scope from a different thread than I created it?
- Are previous scopes automatically restored? If so, what do overlapping scope calls mean?
scope1 = scope_manager.activate(span, false)
scope2 = scope_manager.activate(span, false)
scope1.close()
scope2.close()APIs should be easy to understand and hard to misuse. This is neither. opentracing-java discussion has been a barrage of variations on this idea with edge cases, competing priorities, awkward APIs, unintuitive descriptions, and general confusion. When the conversation is muddled, it's a sign the best answer lies along a different direction.
2. Scope Manager causes memory leaks where there were none before
Last I checked, Java auto-reactivates the last active span. This was a problem with opentracing-java and caused crashes at Lucid Software in a couple messy parts of the code.
void main(ExecutorService executor) {
executor.submit(new Runnable {
public void run() {
Thread.sleep(1);
main(executor);
}
})
}
// okay
main(Executors.newSingleTheadExecutor());
// memory leak
main(tracingExecutor(Executors.newSingleTheadExecutor()));Unbounded async stacks are not a new problem, but (for Java, at least), this is an entirely avoidable problem for Scope Manager.
3. In continuation-based languages, it extremely hard to understand
I've spent the past several days on implementations of the RFC for JavaScript, with both Node.js async_hooks and Zone.js.
- How long does the scope last, and when should it be closed?
const scope = scopeManager.activate(span, false);
const promise = shouldHaveActiveSpan1();
shouldHaveActiveSpan2();
// A
await promise;
// B
shouldNotHaveActiveSpan();i. Does the scope last until A? Does it automatically end, or do I have to close it?
ii. Or does it last until through B and needs to be closed there?
iii. Or the scope last until A and another second scope is implicitly created at B and that second scope needs to be closed?
I believe (iii) is the "sensible" solution .
const scope = scopeManager.activate(span, false);
const promise = shouldHaveActiveSpan1();
shouldHaveActiveSpan2();
// A
promise.then(() => {
// B
shouldNotHaveActiveSpan();
});-
Solutions require either (1) leaking memory creating Scope Managers (2) an explicit enable/disable step (3) WeakMaps of Scope Managers, which is non-trivial.
-
Every continuation local storage solution is instead is based on callbacks, where the storage is scope to a callback and it's transitive calls.
run(session => {
session.set(value);
// ...
session.get();
});https://github.com/othiym23/node-continuation-local-storage
https://github.com/angular/zone.js/
https://nodejs.org/api/domain.html
Even the Go hack for thread locals uses a CPS API: https://godoc.org/github.com/jtolds/gls.
Solution
Similar to my proposal Feb 2017, #23 (comment) in-process propagation should have a continuation-based API.
Proposal
The entire API could be essentially as simple as:
interface ScopeManger {
active(): Span;
execute(Span span, f: () => void): void;
}That's it.
There's no open pits for users to injure themselves, no complicated questions. It unambiguously shadows and restores stacks. And it lends itself to "obviously correct" implementations.
Downsides:
-
It typically adds at least one and probably two frames to the stack for each activated span.
-
Fitting in some imperative boxes become hard or even impossible.
interface Filter {
after(request: Request, response: Response);
before(request: Request, response: Response);
}
IDK how often that pattern comes up. FWIW, the Java servlet standard uses CPS.
It would be possible to have imperative exceptions in certain cases, e.g. Java supports a raw set(Span).
But for 90% of the time and most languages, CPS is a really straightforward, robust, and broadly applicable way to augment scoping/flow control.