Skip to content
This repository was archived by the owner on May 23, 2023. It is now read-only.
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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions