[tokio-io] how to carry state along with connection?

I am writing a network client and I have a struct which represents a "logical connection", i.e. connection to server which can be recovered, produce instrumentation events and so on. One of members is tokio::net::TcpStream which is "physical" connection.

pub struct Connected {
    addr: SocketAddr,
    ...
    tcp: TcpStream,
}

When I send a request and_then decode response, I use tokio::io::write_all. In typical async fashion, it moves 2 parameters (TcpStream and buffer) and will return them in future.

    pub fn request<R>(mut self, request: R) -> impl Future<Item=(Self,u32,R::Response), Error=String>
        where R: protocol::Request
    {
        write_all(self.tcp, buff).
...
        }).and_then(|(tcp, mut buff)| {
            let len = BigEndian::read_u32(&buff);
            println!("Response len: {}", len);
            buff.resize(len as usize, 0_u8);
            read_exact(tcp, buff)
        }).map(|(tcp, buff)| {
            let mut cursor = Cursor::new(buff);
            let (correlation_id, response) = read_response::<R::Response>(&mut cursor);
            println!("CorrId: {}, Response: {:#?}", correlation_id, response);
            (tcp, corr_id, response)
        })

Now my problem. In write_all(self.tcp, ) self will be moved but in future only self.tcp will emerge, thus, self is lost to me and I kinda need it.
I've tried to implement AsyncWrite for Connected so that I could pass my "logical" connection through async pipeline and receive it back in the future, but it feels wrong in many ways (boilerpalte code, new functions introduced in tokio in future won't be overriden, etc).
I am thinking about "detaching" tcp member from Connected struct and re-attaching it when future is complete because I have vague memories I've seen it somewhere.

Another solution I can think of is to improve tokio-io itself by modifying to accept to_async_write, then I can write one-liner adapter for my Connected struct to return tcp member.
But thinking about it more, what if I want to produce periodic statistics on my "logical" connections and they are "moved" into async at the moment. It means I just can not get access of them. So "detach-attach" solution make more sense now.

This can not be unique problem. What is the best practice to associate application-specific data with tcp stream yet allow stream to move in and out of async future?

A common (I think) approach is to maintain an unbounded sender, rather than the physical TcpStream, in the logical representation of your client/connection. The receiver half of this channel is then wired up to feed the actual TcpStream but the client code doesn’t see this. This also allows your client to not require moving self on each call. Unless you want that type of API to provide one-in-flight semantics, it’s usually more annoying to deal with those types of APIs.

@jonhoo ran a good series of web streams on developing an async tokio-based zookeeper client, which you may find useful; first part is here.

2 Likes

@vitalyd thank you for @jonhoo video, he considers several different designs and talks through them. Exactly what I need.