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

Scope Manager should use continuation passing #126

@pauldraper

Description

@pauldraper

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

Problems

1. Scope Manager is hard to understand and easy to misuse

  1. Must I always close an activated scope?
  2. If the thread exits, is the scope deactivated (and the span closed)?
  3. What happens if I close a scope twice? (Spec answers: undefined)
  4. Can I close a scope from a different thread than I created it?
  5. 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.

  1. 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();
});
  1. Solutions require either (1) leaking memory creating Scope Managers (2) an explicit enable/disable step (3) WeakMaps of Scope Managers, which is non-trivial.

  2. 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:

  1. It typically adds at least one and probably two frames to the stack for each activated span.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions