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

Dealing with Async rust behavior #409

Closed
tensor-programming opened this issue Nov 25, 2021 · 5 comments
Closed

Dealing with Async rust behavior #409

tensor-programming opened this issue Nov 25, 2021 · 5 comments

Comments

@tensor-programming
Copy link

I am using Rustler to wrap a natively async rust library (features async traits and other such async methods). I am wondering if there is a idiomatic way to deal with futures with rustler without having to pull in a rust executor. Currently, I have a tokio runtime loaded in with lazy static but the pattern feels very brittle. I know there are threads in rustler, perhaps theres something there that is useful for dealing with async blocks? or with the Dirty Schedulers? I am not too familiar with the Dirty Schedulers.

@scrogson
Copy link
Member

This is the pattern I’ve used here (but using once_cell instead of lazy_static): https://github.com/scrogson/franz

https://github.com/scrogson/franz/blob/master/native/franz/src/task.rs

I’m not sure of a better way at the moment. I think at some point it would be nice to build an abstraction around futures so that we could automatically build NIFs which yield at await points (using enif_schedule_nif). Not sure how much work this would be, but it seems neat.

@tensor-programming
Copy link
Author

tensor-programming commented Nov 29, 2021

That would be great to be honest. That's that kind of functionality that I was expecting to see but it makes sense that its not really in here yet given how rustler has changed in the past couple of months. Would also be cool to expose the async functions as something similar to tasks in the elixir/erlang side of things. I would certainly be interesting in contributing for this kind of feature in the future. Thanks for the response.

How brittle do you think this pattern is with once_cell or lazy_static globally creating a Tokio runtime? Something about it just feels a little wrong to me but thats just my take on it.

@hansihe
Copy link
Member

hansihe commented Nov 29, 2021

I’m not sure of a better way at the moment. I think at some point it would be nice to build an abstraction around futures so that we could automatically build NIFs which yield at await points (using enif_schedule_nif). Not sure how much work this would be, but it seems neat.

I remember talking about something like this, but that wouldn't actually help in this case. This would simply be an easier way to work with NIFs that reschedule without having to manually split your function into several pieces. The implementation of something like this would require a major refactor/API change in Rustler, and the advantages are arguable, so I don't think this will be done anytime soon.


I am using Rustler to wrap a natively async rust library (features async traits and other such async methods). I am wondering if there is a idiomatic way to deal with futures with rustler without having to pull in a rust executor. Currently, I have a tokio runtime loaded in with lazy static but the pattern feels very brittle. I know there are threads in rustler, perhaps theres something there that is useful for dealing with async blocks? or with the Dirty Schedulers? I am not too familiar with the Dirty Schedulers.

We would very much like to provide better integration between Rustler and async Rust code. However, we are simply users of the erl_nif API, and we don't have the functionality available to make the interaction a whole lot nicer than it already is/you could do manually with lazy_static/OnceCell. There isn't much applicability to Dirty Schedulers for making futures easier to use with Rustler.

There is no good way to get around having a future runtime, like tokio. For initializing the runtime, you have a couple of options (like you mentioned):

  • A lazy_static. This is probably the easiest, and will lazily create the runtime the first time it's used.
  • A OnceCell or equivalent which is initialized in the NIF init function. This will create the runtime when NIF is actually loaded. I think this is the best approach.

I see no reason why these approaches would be particularly brittle, as long as you avoid blocking the BEAM scheduler thread on the future (avoid block_on).

Instead, spawn your future in the runtime, and return from the NIF immediately. Then send the result back to your process with OwnedEnv::send_and_clear.


One abstraction we could and should provide, is a variant of OwnedEnv::send_and_clear that works with async code.

A function like OwnedEnv::async_send_and_clear would take a future as an argument, and return a future. This could then be spawned in a runtime like tokio. The only thing this would really get you over the sync variant though is the ability to construct and keep terms across suspend points in the inner function.

@hansihe
Copy link
Member

hansihe commented Nov 29, 2021

Closing for #411 as it seems like that's about the extent of what we can provide.

@hansihe hansihe closed this as completed Nov 29, 2021
@tensor-programming
Copy link
Author

tensor-programming commented Dec 2, 2021

A function like OwnedEnv::async_send_and_clear would take a future as an argument, and return a future. This could then be spawned in a runtime like tokio. The only thing this would really get you over the sync variant though is the ability to construct and keep terms across suspend points in the inner function.

This here, is exactly the kind of advice I was looking for. Thank you very much. I very much look forward to seeing this kind of feature in the future as my code is riddled with block_on calls. In many cases, I find myself having to write either small block on functions or very large rust NIFs where the BEAM just hands off functionality wholesale to rust. For example, I have a system right now where I have my Elixir system listens to events coming from another RPC. And since the RPC client is only a rust client, I have no real choice but to just use rust in this case. The Elixir system calls a NIF which submits an event to this other RPC and then the side rust takes control until it receives the response back. It becomes a task on the Elixir side and on the Rust side its this clunky "process/service" that is unavoidable.

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

No branches or pull requests

3 participants