Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify and refine async context tracking #282

Merged
merged 8 commits into from
Jan 31, 2023
Merged

Conversation

jasnell
Copy link
Member

@jasnell jasnell commented Jan 18, 2023

There are two key changes here:

  • Elimate the root context frame: It occurred to me that we really do not need a root AsyncContextFrame instance at all since there's nothing actually being stored there ever. Just have AsyncContextFrame::current() return an empty kj::Maybe in that case and treat the nullptr as equivalent to the root frame.
  • Makes use of an obscure v8 API that allows setting continuation context for promises and tasks. Allows a number of simplifications.

Also fixes a couple of minor bugs.

src/workerd/api/global-scope.c++ Show resolved Hide resolved
src/workerd/jsg/async-context.c++ Outdated Show resolved Hide resolved
@jasnell jasnell force-pushed the jsnell/refine-asynccontext branch 2 times, most recently from 06c9dfd to 9a85519 Compare January 25, 2023 01:24
@jasnell
Copy link
Member Author

jasnell commented Jan 25, 2023

Ok, now that it's clear this is definitely the direction we want to go, I've merged the two PRs and cleaned up more of the bits that are made obsolete by relying on the v8 API.

Thenables are still a challenge here but there's a possible workaround using AsyncResource and we do expect v8 to fix that limitation in the future so I'm not that concerned.

@jasnell jasnell requested a review from harrishancock January 25, 2023 01:50
@jasnell
Copy link
Member Author

jasnell commented Jan 25, 2023

@harrishancock ... given the substantial changes, I've cleared your ok. Can you give it another look over?

Copy link
Collaborator

@harrishancock harrishancock left a comment

Choose a reason for hiding this comment

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

It looks reasonable to me. I think @kentonv's reviews here are a bit more valuable, though.

@kentonv
Copy link
Member

kentonv commented Jan 25, 2023

I can't figure out how to get a diff from what I've already reviewed to the current state of the PR. It seems like several additional changes have been rebased together with stuff I've reviewed already, and the whole thing was also rebased on master in the meantime too.

@jasnell
Copy link
Member Author

jasnell commented Jan 25, 2023

There are enough additional cleanups and a number of new commits here that it's worth looking at it as a fresh review.

@jasnell jasnell force-pushed the jsnell/refine-asynccontext branch from 08ad3c4 to 31ce92a Compare January 30, 2023 15:01
src/workerd/io/io-context.h Outdated Show resolved Hide resolved
src/workerd/jsg/README.md Outdated Show resolved Hide resolved
@kentonv
Copy link
Member

kentonv commented Jan 31, 2023

looks good but needs squash

@jasnell jasnell force-pushed the jsnell/refine-asynccontext branch 2 times, most recently from 308bb63 to 0fd0aca Compare January 31, 2023 00:09
@jasnell
Copy link
Member Author

jasnell commented Jan 31, 2023

Squashed and rebased. Will land once updated internal CI comes up green

@jasnell jasnell force-pushed the jsnell/refine-asynccontext branch from 0fd0aca to 0b3bf8a Compare January 31, 2023 15:15
It occurred to me that we really do not need a root AsyncContextFrame
instance at all since there's nothing actually being stored there ever.
Just have AsyncContextFrame::current() return an empty kj::Maybe in
that case.

Also fixes a couple of minor bugs.
It turns out that v8 has a (pretty much undocumented) API for setting
continuation context for Promises:

`v8::Context::SetContinuationPreservedEmbedderData(...)`

Using is allows us to simplify (but not quite eliminate yet) the
promise hook and eliminate the use of the frame stack in favor of the
simpler kj::Maybe approach Kenton suggested previously.
Removing obsolete bits, updating docs/comments
Capture the current AsyncContextFrame when awaitIoImpl is called and
ensure that the promise continuation enters that context when resolving
the JS promise.
By default, capture the current AsyncContextFrame when the
transform() method is called such that all of the handler
functions will enter that context. This approach gives us
the most flexibility. If a user really prefers to capture
the context when `on()` or `onDocument()` is called, the
Element and Document handlers can be easily wrapped.

// Default:
```js
class ElementHandler {
  element(element) {
    if (als.getStore() !== 321) throw new Error("Incorrect context");
  }

  text(text) {
    if (als.getStore() !== 321) throw new Error("Incorrect context");
  }
}

const res = new Response(new ReadableStream({
  start(c) {
    const enc = new TextEncoder();
    c.enqueue(enc.encode('<div>text</div>'));
    c.close();
  }
}));

// In this variation, the HTMLWriter captures the async context at the
// moment transform is called.

const rewriter = new HTMLRewriter();
als.run(123, () => rewriter.on('div', new ElementHandler()));
return als.run(321, () => rewriter.transform(res));
```

// Override:
```js
class ElementHandler extends AsyncResource {
  constructor() { super('') }

  element(element) {
    this.runInAsyncScope(() => {
      if (als.getStore() !== 123) throw new Error("Incorrect context");
    });
  }

  text(text) {
    this.runInAsyncScope(() => {
      if (als.getStore() !== 123) throw new Error("Incorrect context");
    });
  }
}

const res = new Response(new ReadableStream({
  start(c) {
    const enc = new TextEncoder();
    c.enqueue(enc.encode('<div>text</div>'));
    c.close();
  }
}));

// In this variation, the ElementHandler captures the async context.

const rewriter = new HTMLRewriter();
als.run(123, () => rewriter.on('div', new ElementHandler()));
return als.run(321, () => rewriter.transform(res));
```

Alternative override:
```js
class ElementHandler {
  element = AsyncResource.bind(() => {
    if (als.getStore() !== 123) throw new Error("Incorrect context");
  });

  text = AsyncResource.bind(() => {
    if (als.getStore() !== 123) throw new Error("Incorrect context");
  });
}

const res = new Response(new ReadableStream({
  start(c) {
    const enc = new TextEncoder();
    c.enqueue(enc.encode('<div>text</div>'));
    c.close();
  }
}));

// In this variation, the ElementHandler captures the async context.

const rewriter = new HTMLRewriter();
als.run(123, () => rewriter.on('div', new ElementHandler()));
return als.run(321, () => rewriter.transform(res));
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants