Skip to content

Commit e13e0d7

Browse files
committed
rfc
1 parent bd1910d commit e13e0d7

File tree

1 file changed

+88
-0
lines changed

1 file changed

+88
-0
lines changed

rfc.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Possible implementations
2+
3+
## Maintaining two separate implementations
4+
5+
Pros:
6+
- Easy to implement (just copy-paste the blocking implementation and start inserting `async`/`await`)
7+
- Allows for optimal performance in both situations
8+
- *Should* be able to share at least a part of the implementation
9+
10+
Cons:
11+
- Maintenance, any bug needs to be fixed in both implementations. Same goes for testing.
12+
- Hard to onboard, new contributors will be confronted with a very large codebase (see [Good ol' copy-pasting](https://nullderef.com/blog/rust-async-sync/#good-ol-copy-pasting))
13+
- Adding new functionality means implementing it twice.
14+
15+
## Implement in async, use `block_on` for sync implementation
16+
17+
In this implementation, the core codebase is implemented asynchronously. A `blocking` module is provided which wraps
18+
the async functions/types in `block_on` calls. Recreating the runtime on every call is very slow, so to make this work
19+
it would involve spawning a thread for the runtime and using that to spawn the async functions. This is how `reqwest`
20+
implements their async/sync code.
21+
22+
Pros:
23+
- Only need to maintain/test/upgrade one implementation
24+
- Optimal performance for async code
25+
26+
Cons:
27+
- Degrades sync performance
28+
- Need to pull in a runtime when the `blocking` feature is enabled (`reqwest` use `tokio` but something like `smoll` might make more sense)
29+
30+
## Implement in async, use `maybe_async` to generate sync implementation
31+
32+
[`maybe_async`](https://crates.io/crates/maybe-async) is a proc macro that removes the `.await` from the async code and uses it to generate sync code.
33+
34+
Pros:
35+
- Only need to maintain/test/upgrade one implementation
36+
- Optimal performance for both async and sync code
37+
38+
Cons:
39+
- Crate breaks if both the `sync` and `async` features are enabled
40+
41+
## Sans I/O
42+
43+
Implement the parser as a state machine that can be driven by both async and sync code. This is how [`rc-zip`](https://lib.rs/crates/rc-zip)
44+
is implemented.
45+
46+
Pros:
47+
- Only need to maintain/test/upgrade one implementation
48+
- Optimal performance for both async and sync code
49+
50+
Cons:
51+
- Have to manually implement the state machines
52+
- In the distant future [it's possible to use coroutines/generators](https://internals.rust-lang.org/t/using-coroutines-for-a-sans-io-parser/22968), but they're currently *very* unstable.
53+
54+
## Do not provide an async implementation
55+
56+
Pros:
57+
- Easiest option, nothing has to change
58+
59+
Cons:
60+
- An async implementation is really nice for using Avro over the network
61+
62+
# Serde
63+
64+
One problem not mentioned yet, is that Serde does not have an async interface. This doesn't necessarily have to be a problem.
65+
The current deserialize implementation also first decodes a `avro::Value` and then uses that to deserialize the Serde type (reverse for serialize).
66+
The decoding to `avro::Value` can be made async, and then the serde part can be done in a sync way as it does not use any I/O.
67+
68+
Some alternative options:
69+
- [tokio-serde](https://docs.rs/tokio-serde/latest/tokio_serde/index.html)
70+
- A wrapper around Serde that requires the user to split the input into frames containing one object.
71+
- [destream](https://docs.rs/destream/0.9.0/destream/index.html)
72+
- Async versions of the Serde traits, but not compatible with serde so lacks ecosystem support.
73+
74+
# Best option?
75+
76+
I'm currently leaning towards implementing Sans I/O. It provides an (almost) optimal implementation for both async and sync code.
77+
It doesn't duplicate code (except the interfaces) and doesn't require pulling in any runtime (only parts of `futures`).
78+
79+
Care needs to be taken that the state machines are kept small and understandable.
80+
81+
The second-best option is probably using `block_on` in a separate thread. But that seems unnecessarily heavy.
82+
83+
# References
84+
85+
- [Blog post by the maintainer of `RSpotify` who tried multiple of the above options](https://nullderef.com/blog/rust-async-sync/)
86+
- [A discussion about Sans I/O](https://sdr-podcast.com/episodes/sans-io/)
87+
- [A explanation of Sans I/O by the author of `rc-zip`](https://fasterthanli.me/articles/the-case-for-sans-io)
88+
- The blog post is currently not freely available, but the video (which has the exact same content) is freely available

0 commit comments

Comments
 (0)