|
| 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