In futures::sink::Sink why is order of calls not enforced at type system level?

In the futures::sink::Sink, there are poll_ready and start_send fns. The documentation specifies that a successful poll_ready call is required before start_send can be called.

So, with rust, we can enforce that at the type system level, by not adding the start_send to the trait itself, but instead returning a handle that implements start_send from poll_ready. This would guarantee the proper order - which is much better compared to giving users the possibility to make a mistake.

My question is - why wasn't it done? My suspicion is the runtime overhead of having a handle value was deemed to be too high. I also have other guesses, but I'd love to hear from the people that participated in the design loop of that interface!

Largely because you aren't supposed to use futures::sink::Sink directly, and to make it easier on the implementor (they only need to implement a single trait with 1 type). Your supposed to use these types,

all of which are futures that you could await that wrap the Sink trait and call into it.

This should probably go in the Sink docs?

Thanks for the response!

I feel like from an implementor point of view, that design seems a lot better too. I'm actually coming from an implementor end here.
My current issue is, I think, that I hit the design limitations.

While I can understand the idea of having a trait that's not supposed to be used directly, but, in that sense, what we have here is more of a hack. The reason I think so is if there were any performance gains from designing the trait itself without the handler values, the resulting architecture requires even more overhead with the additional features.

To do this, you would probably need to create an associated type on the sink trait that represents the type you call start_send on, but since we do not have GAT this would mean that the returned type can't borrow from the sink, so now you need reference counting inside the sink... And now you need a Mutex if that type should be able to modify something inside the sink...

Ah, yeah, we hit the GAT again there. This must be the reason. Or at least, it's the reason it can't be currently implemented as I described. Going through Mutex is obviously far worse than what we now...

I think the expected interface here would be:

fn poll_send( self: Pin<&mut Self>, cx: &mut Context, item: Item ) 

   -> Poll<Result<(), Self::Error>>

But now what happens if the buffer of the Sink is full and the underlying connection is giving back pressure? Drop the Item?

Often enough the error type will be std::io::Error, but that has no way to hold the item to give it back to you, and async API's are not supposed to return WouldBlock. They are supposed to return Pending, but if there is no place to store Item, we run into trouble.

Thus a method poll_ready is provided which can return Pending without having consumed an Item.

This is just my imagination. I didn't invent this API, nor looked up discussion about why it was made this way, but I can see this being the reason.

If you really want to safeguard here, you could return a

Poll::Ready< Result<SendNonce, Self::Error> > from poll_ready

and require the client code to pass the SendNonce back into start_send. But probably that seems like overkill since indeed more convenient API's are provided for daily use. You only need to use this API if you are using a Sink from within another poll function.

I was thinking about something like:

  fn poll_ready(
        self: Pin<&mut Self>, 
        cx: &mut Context
    ) -> Poll<Result<FnOnce(item: Item) -> Result<(), Self::Error>, Self::Error>>;

But again, see In futures::sink::Sink why is order of calls not enforced at type system level?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.