-
Notifications
You must be signed in to change notification settings - Fork 0
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
async IO traits: thoughts on ecosystem splits #12
Comments
To be a little more concrete, the read traits would look something like pub trait Read {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
async fn read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result<())> { ... }
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { ... }
async fn read_buf_exact(&mut self, buf: &mut ReadBuf<'_>) -> Result<()> { ... }
async fn read_buf_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result<usize> { ... }
async fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize> { ... }
async fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { ... }
fn is_read_vectored(&self) -> bool { ... }
fn by_ref(&mut self) -> &mut Self
where
Self: Sized,
{ ... }
fn as_ready(&self) -> Option<&dyn ReadinessRead> {
None
}
}
pub trait Ready {
async fn ready(&mut self, interest: Interest) -> Result<Readiness>;
}
// Strawman name
pub trait ReadinessRead: Ready {
fn non_blocking_read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result<NonBlocking<()>>;
fn non_blocking_read_buf_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result<NonBlocking<usize>> { ... }
fn is_read_vectored(&self) -> bool { ... }
fn by_ref(&mut self) -> &mut Self
where
Self: Sized,
{ ... }
}
impl<T: ReadinessRead> Read for T {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize> { ... }
fn as_ready(&self) -> Option<&dyn ReadinessRead> {
Some(self)
}
} |
@nrc Honestly, while the trait Currently, the most efficient way to work with io-uring is to used owned buffer. Registered buffers provides better performance because:
Thus, I think it is absolutely necessary to rethink the |
I've been subscribing to this repository for a while - I think (lack of) runtime interop is a significant issue in today's async ecosystem. Instead, it mostly looks like discussions are mostly happening somewhere else with you @nrc reporting back/synthesising/interpreting the viewpoints of the different "groups". See, for example, this paragraph:
Am I missing other obvious public venues where these conversations are taking place where we can see descriptions of requirements/concerns coming first-hand by those groups of users? |
I disagree, I think |
Sorry, somewhat unspoken here, but described in the main proposal (https://github.com/nrc/portable-interoperable/blob/master/io-traits/README.md), is that to get the most out of completion based systems you would use the BufRead (or proposed OwnedRead) traits, rather than the Read trait. So that is all somewhat orthogonal to the design around
There are some discussions happening on Zulip, mostly with the async WG, but not so much. I've been having some 1:1 chats with various stakeholders, but otherwise discussion is mostly here and on Zulip. Where I'm 'reporting back' it's mostly from reading issues or code, or discussions with stakeholders, also I guess lots of my own thinking, research on existing systems, and iteration on design. Honestly, there has not been as much active discussion as I'd like. |
The In io-uring, you can specify the id of the group of the provided buffer to be used when issuing a Thus, it is possible to configure it to use a different size dynamically. Perhaps we should also have methods for probing the size of the internal buffer and requests to change that? |
Internal buffer size modification seems like it would be appropriate more as inherent methods on types like |
Fair enough. |
I don't think having async IO traits like Many crates, like There is also crates like |
Hmmm, I just notice that there is no way to pass an owning buffer for Perhaps we also need one for |
These are good questions, but they are somewhat off-topic for this issue. I have proposed adding a BufWrite trait and perhaps OwnedWrite too, though I've mostly been focussing on reading. I very much appreciate that there is lots more than just the io traits to be done to make the ecosystem more interoperable. I think the IO traits are necessary but not sufficient. |
async fn read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result<())> { ... } I was under the impression this signature had changed to |
Yes that is correct, I'm basing this off the current sync design and will update as that evolves (it is, I think, orthogonal to the design questions around async). |
I guess I'm missing too much context to formulate a proper response to this. For example:
I definitely have thoughts on ergonomics, performance, compatibility in async Rust. But in order to engage with this I need to better understand where you're coming from and which assumptions you bring. Because if we don't share a common understanding of the problem space, it's hard to come to shared solutions - especially in an unstructured medium like this. Does that make sense? |
This is definitely a problem. I think a good solution would be to make Reqwest generic over a
Just having A slightly more general API, enabling sharing a single write buffer between multiple I/O resources, is this: impl Runtime {
pub fn buffer(&self) -> Buffer;
}
impl TcpStream {
pub async fn write_from_buffer(&self, buf: &Buffer, range: Range<usize>) -> io::Result<usize>;
pub async fn read_to_buffer(&self, buf: &mut Buffer, range: Range<usize>) -> io::Result<usize>;
} Where Edit: Actually, I do see the use case for a |
I think you may be over-indexing on the grouping here. I simply mean that some users have requirements that make memory usage a very high priority and therefore it is essential for such users to be able to minimise memory allocation. For other users their requirements mean that minimising latency is very high priority and therefore it is essential to support zero-copies. These are obviously very rough groupings and there will be many other differences between members of the group, I'm just grouping users for whether a certain aspect of performance is a top priority or if ease of use is more important.
By cutting edge I mean that they don't care about every last cycle or bit of memory, they only care about orders of magnitude performance. Examples of such users are somebody replacing part of a web backend from Ruby to Rust for performance. They care about performance, but not to the same degree as somebody implementing a load balancer for AWS or something.
Why? From discussions with you and Josh among others, where simplicity and symmetry with the sync APIs seem paramount. Alternatives - the earlier proposal of Ready::ready, Read::{read, non_blocking_read} and its variations, having just ready and non_blocking_read, or using polling are the main alternatives on this axis. The other alternatives in the proposal doc are somewhat relevant too. |
Yeah, that is similar to what I thought, but I think that it is also necessary to have a Checkout #13 , a very rough scratch of what I want. |
Not sure an issue is the right place for this, but I wanted to record some thoughts on the subject of ecosystems splits in the context of the async IO traits.
The primary driver in doing this work is to fix an ecosystem split between runtimes. I think that is an important goal with many good effects beyond portability between runtimes. Any solution to the async IO traits should strive to avoid creating new ecosystem splits. However, one of the fundamental tensions in the traits design is that there are different constituencies with different needs and priorities: some users want an easy/ergonomic way to read which will be fast and efficient, but not necessarily cutting edge. Some users have stronger preferences for performance or memory usage over ergonomics (this is multiple groups, I think, with each group having a different requirement around performance). I think there is a really important question over how much of the ecosystem can be shared between these groups and how much we just have to accept some level of splitting.
Looking at
Read
, it feels like anything other thanasync fn read
being the primary API for users and implementers is sub-optimal for the ergonomics-first group. However, there is no way to adapt such an API into the ready/non-blocking read API which seems necessary for the group which prioritises memory usage (the reverse adaption is possible). The only way to satisfy both groups is to have two sets of traits (i.e., there is aRead
trait with a simpleasync fn read
API, then there is aReady
andReadinessRead: Ready
set of traits for memory-optimal usage, with a blanket impl ofRead
forT: ReadinessRead
). In this scenario, any library which usesRead
bounds is usable by everyone, but any library which usesReady
orReadinessRead
bounds is only usable with resources which implement those traits. I think 'downcasting' for convertingdyn Read
todyn ReadinessRead
might be possible?I wonder how this would work in practice? It feels like it could be OK if most libraries used
Read
andReadinessRead
was only used where absolutely necessary, and if most leaf resources implementedReadinessRead
rather thanRead
directly. However, if the whole Tokio ecosystem moves toReadinessRead
(since that is more natural given their priorities), then I think we just end up with a variation of the current ecosystem split but split by traits rather than dependencies.Anyway, @rust-lang/wg-async, @rust-lang/libs-api I'd be interested if any of you have thoughts on this
The text was updated successfully, but these errors were encountered: