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

The bane of my existence: Supporting both async and sync code in Rust #4

Open
marioortizmanero opened this issue Sep 21, 2021 · 14 comments

Comments

@marioortizmanero
Copy link
Owner

Original post: https://nullderef.com/blog/rust-async-sync

You can use this issue to leave any comments.

@marioortizmanero marioortizmanero transferred this issue from marioortizmanero/nullderef.com Jan 4, 2022
@stIncMale
Copy link

stIncMale commented Jan 19, 2024

Thank you, this was educative. I subscribed to this issue with comments, hoping that sometime in the future, if a good solution to the problem appears, you'll update the blog post and notify commenters here.

What would you do if you were a maintainer of Rspotify?

The Fixing maybe_async approach seems ideal when combined with your idea to expand with modules. It's a shame that fixing it is too complicated.

I'd likely continue using the maybe_async crate as is until it becomes an actual problem for someone. However, I would have added this helpful guard to explicitly tell a user that the features maybe_async/is_sync, client-reqwest are mutually exclusive (so are client-ureq, client-reqwest).

Just to give some perspective, in the MongoDB Java Driver we use the Good ol' copy-pasting approach, only it's much worse than what you had in RSpotify: the async implementations and even declarations have to be different from the sync ones because there is no async/.await counterpart in Java1. So everything bad you said about this approach applies, but is further aggravated. We absolutely "love" 😢 this.


1 Virtual threads were added in Java SE 21, and they make our asynchronous API more or less worthless for programmers who use Java SE 21. But we can't just get rid of this API, because there are enough clients who use Java SE 8 😭

@siancu
Copy link

siancu commented Jan 20, 2024

This might be a stupid thing, as I'm only starting now to learn Rust, but wouldn't it be possible for you to provide only the sync APIs and let the clients implement the async part on top, if they want/need to?

@stIncMale
Copy link

stIncMale commented Jan 21, 2024

@siancu It is technically possible. For example, one may turn a synchronous API into an asynchronous one using tokio::task::spawn_blocking. However, the performance characteristics of such API will at best be those of the synchronous API, with most if not all the hurdles of asynchronous code on top of that. Thus, one gets the worst of both worlds this way. Sometimes, though, one may have to do this when there is no asynchronous alternative to a particular synchronous API, and one has to use it from asynchronous code.

@scouten
Copy link

scouten commented Jan 23, 2024

FYI I wrote a crate that approaches the sync vs async challenge a slightly different way based on the likely direction of the keyword generic initiative:

https://crates.io/crates/async-generic

@stIncMale
Copy link

@scouten The documentation of your crate says

You can write if _sync or if _async blocks ...

("can", not "must"), but of the two examples it provides, both have a synchronous original function with an if _async ... else ... block. Is it possible to write an asynchronous (or synchronous) function, add the #[async_generic] attribute to it, and get it working without duplicating its implementation in an if _async ... else ... block?

@scouten
Copy link

scouten commented Jan 26, 2024

@stIncMale in the use cases I encountered, there was often a long part of the function that was shared between sync and async cases and then a tiny portion that required .await or not which meant the rest of the function body could not be shared. Avoiding that duplication was my primary goal.

That said, I would welcome a PR that patches in/out the .await syntax.

@mr0rng
Copy link

mr0rng commented Jan 29, 2024

It seems to me that I have solved all the problems described in the article, including additive.

If the topic is not yet boring, then I would be very glad to hear your comment about the synca.

Book
crates.io

@stIncMale
Copy link

@mr0rng, that's great to know. I wonder what @marioortizmanero will say if he tries to use synca instead of maybe_async in rspotify.

@gweakliem
Copy link

I'm new to Rust so maybe I'm being overly simplistic, but I think you were too quick to discard the block_on approach. I understand the concerns about overhead, but as I understand the reason behind the original request (because I frequently have it myself) is "I just want to do something quick and dirty, why do I have to screw with all this async crap?". What's unspoken in there is that I don't care about performance. It seems to me like that's the answer, yes you can have sync on async, but there's a hit to your resource usage.
That said it's a really interesting writeup, and I learned that there's a footgun hiding in compile-time flags. My own notes say maybe_async is interesting but it's not suitable for use in a library, and if you're using it in a top-level executable why not just pick one or the other?

@dmitrybalakov
Copy link

The article is really great!
My motivation to write synca was born from a crate for working with postgres; it would be strange to use a block_on approach in the presence of tokyo_postgress.
But in the same way I don’t want to duplicate and then maintain 2 versions of the code.
But I am sure that there are situations where the solution would be different

@infogulch
Copy link

infogulch commented Feb 6, 2024

The whole issue with block_on seems to be the unnecessary bloat of the futures runtime when you only need block_on.

So I lifted the futures-rs block_on implementation into a separate crate, with minimal code and dependency tree. You should be able to call it on any future from a syncronous context, with no need to create or manage a runtime. (Like futures-rs, it creates a thread local to receive completion notifications.)

Admittedly, I haven't tested it. But it compiles fine, so...

@JM4ier
Copy link

JM4ier commented Feb 8, 2024

I decided to give it a shot myself, I think I found a neat solution using path specifiers for modules.
I already published it to crates.io so you can look at the docs here.

@mathstuf
Copy link

I had a similar problem with the gitlab crate with its 100s of endpoints. Eventually I came across a solution that I wrote about here. Basically, you have a structure for the endpoint implementations and then the client that the user provides is what does the sync/async bit (and auth). This allows you to describe an endpoint completely separate from the mechanism used to actually perform it. It also ends up allowing neat combinators like ignore the response or raw to get the raw bytes (instead of parsing anything). It also means the user gets to describe what they want from the response (dropping uninteresting fields).

@marcoradocchia
Copy link

I've been struggling writing both blocking and async code in an embedded-hal driver using feature gates. I found myself in a very similar situation to the one described in the article. I really appreciated the reading, beautiful analysis! I'm really looking forward the "Keyword Generics Initiative".

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