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

Implement Futures RFC #874

Merged
merged 3 commits into from
Jul 7, 2022
Merged

Implement Futures RFC #874

merged 3 commits into from
Jul 7, 2022

Conversation

kjvalencik
Copy link
Member

@kjvalencik kjvalencik commented Mar 9, 2022

Implements #872

Limitation

This makes use of JsFunction::new with a closure, which requires Node-API 5. This seemed like a reasonable limitation to simplify the implementation.

However, it would also be possible by passing the state with a JsBox and using Function.prototype.bind.

src/result/mod.rs Outdated Show resolved Hide resolved
src/event/channel.rs Outdated Show resolved Hide resolved
@kjvalencik kjvalencik requested a review from dherman March 9, 2022 17:28
@dherman
Copy link
Collaborator

dherman commented Mar 16, 2022

This makes use of JsFunction::new with a closure, which requires Node-API 5. This seemed like a reasonable limitation to simplify the implementation.

This should be fine IMO. Node 14 has had support for Node-API 6 since 14.0.0, and Node 12 is EOL at the end of next month. So it's not worth the extra complexity to support older versions of Node that are about to be EOL.

@kjvalencik
Copy link
Member Author

kjvalencik commented Jun 3, 2022

@dherman This has been updated:

  • Resolving conflicts from latest main
  • Switching to tokio::sync::oneshot and allowing JoinHandle::join and .await to live side-by-side.

This introduces a side-effect that calling .join() from an asynchronous context will panic. This is nearly always a programmer error. It could theoretically be in a dependent crate that they can't control, but the user has few options to resolve:

  • Disable the futures feature (which might not be an option if they are using it)
  • PR the upstream dependency to at a futures support that toggles appropriately
  • Use rt.spawn_blocking(..) to push it to a blocking thread and outside the runtime

@kjvalencik kjvalencik force-pushed the kv/futures branch 5 times, most recently from 8fa4f33 to be994e8 Compare June 9, 2022 13:17
Copy link
Collaborator

@dherman dherman left a comment

Choose a reason for hiding this comment

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

I really like where this API has landed. I left a few suggestions for clarity, because some of the code we a little hard for me to follow.

crates/neon/src/event/channel.rs Outdated Show resolved Hide resolved
+ 'static,
{
let then = self.get::<JsFunction, _, _>(cx, "then")?;
let catch = self.get::<JsFunction, _, _>(cx, "catch")?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor semantic proposal: since getting a property could trigger arbitrary JS side effects (since, for example, it could be a getter function), I think we should perform the side effects in this order:

  1. Extract the .then property
  2. Exec the then function
  3. Extract the .catch property
  4. Exec the catch function

Copy link
Member Author

@kjvalencik kjvalencik Jul 6, 2022

Choose a reason for hiding this comment

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

I went with this order to avoid calling anything if either getter fails. This is to avoid being in a half successful state.

It's still possible to be in this state if calling .then fails, but at least it's less likely.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK, I see where you're coming from. I don't think I'm quite convinced but I also think it's not worth blocking progress.

crates/neon/src/event/channel.rs Outdated Show resolved Hide resolved

[dependencies.neon]
version = "*"
path = "../../crates/neon"
features = ["napi-8"]
features = ["futures", "napi-experimental"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

This now means we only test one path through the codebase, when the runtime is implemented differently depending on whether this flag is enabled or disabled. We should probably run our tests both with and without the "futures" flag. IMO it's fine if we do this in follow-up work, since our next release will be a beta so the risk is somewhat mitigated.

Copy link
Collaborator

@dherman dherman left a comment

Choose a reason for hiding this comment

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

🚢

+ 'static,
{
let then = self.get::<JsFunction, _, _>(cx, "then")?;
let catch = self.get::<JsFunction, _, _>(cx, "catch")?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

OK, I see where you're coming from. I don't think I'm quite convinced but I also think it's not worth blocking progress.

}
}

// Marker that a `Throw` occurred that can be sent across threads for use in `JoinError`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice!

@kjvalencik
Copy link
Member Author

kjvalencik commented Jul 7, 2022

I will follow-up with a PR to tests since it will require some refactoring to use multiple Neon modules in the test suite.

In the meantime, I tested with a small example locally:

use neon::prelude::*;

fn lazy_add(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let a = cx.argument::<JsFunction>(0)?.root(&mut cx);
    let b = cx.argument::<JsFunction>(1)?.root(&mut cx);
    let cb = cx.argument::<JsFunction>(2)?.root(&mut cx);
    let channel = cx.channel();

    std::thread::spawn(move || {
        let (a, b, cb) = channel
            .send(move |mut cx| {
                let a = a.into_inner(&mut cx);
                let b = b.into_inner(&mut cx);
                let cb = cb.into_inner(&mut cx);

                let a = a.call_with(&cx).apply::<JsNumber, _>(&mut cx)?;
                let b = b.call_with(&cx).apply::<JsNumber, _>(&mut cx)?;

                Ok((a.value(&mut cx), b.value(&mut cx), cb.root(&mut cx)))
            })
            .join()
            .unwrap();

        let c = a + b;

        channel.send(move |mut cx| {
            cb.into_inner(&mut cx)
                .call_with(&cx)
                .arg(cx.number(c))
                .exec(&mut cx)?;

            Ok(())
        });
    });

    Ok(cx.undefined())
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    cx.export_function("lazyAdd", lazy_add)?;

    Ok(())
}
const addon = require(".");

addon.lazyAdd(() => 1, () => 2, console.log);

@kjvalencik kjvalencik merged commit f21146d into main Jul 7, 2022
@kjvalencik kjvalencik deleted the kv/futures branch July 7, 2022 22:12
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.

None yet

2 participants