Futures + Stream : Lazy vs Eager Submission of Requests?

Broadly speaking, the general consensus on Futures (and Streams) in Rust is that they must be "driven to completion" to perform work, and that, unlike JavaScript, they are not executed eagerly.

I have made an example below of a client that returns a Future and a Stream, with questions on at what point a request should actually be transmitted out over the internet (independent on when we begin to try to receive/poll the response).

I would like some clarification on whether the below API behaves correctly, since I have a colleague that disagrees with me. More importantly than me "being right", if possible, a justification for why submission should occur only after a poll would be appreciated.

Please view Question 1 an Question 2 in the implementation comments.

/// Fetches data about a person's programming preferences via internet
struct ProgrammingPreferencesClient {
   // the client itself, upon receiving a request, sends it to a
   // background thread that owns a TCP connection and
   // is the actual thing transmitting and receiving over the internet
   sender_to_background_io_multiplexer_thread: mpsc::UnboundedSender<_>}
}

impl ProgrammingPreferencesClient {
   fn get_favorite_programming_language_for_person(&self, name: String) -> impl Future<Output=String> {
         let (tx_from_background, rx_of_background_result) = oneshot::channel();
         let sender = self.sender_to_background_io_multiplexer_thread.clone();
               
         async move {


           // !!!!! QUESTION 1:
           // Should the `sender.sender` below occur prior to the async move? If that is the case, then
           // the outgoing request will be eagerly submitted to the IO thread and set out via the internet
           // even though no polling on the future has occurred yet.
           // I believe the answer should be no, that the correct thing is to submit the outgoing 
           // request  lazily, only after the first poll.

            // submit the request payload to background thread, as well as a way for it
            // to communicate the result to this function
            let _ = sender.send((name, tx_from_background));


             // oneshot's receiver implements future
             rx_of_background_result.await.unwrap()
         }
   }

      // given a person's name, return their favorite libraries
      fn get_favorite_programming_libraries_for_person(&self, name: String) -> impl Stream<Item=String> {
          // ...........
      }
}

async fn main() {
      let client: ProgrammingPreferencesClient = instantiate_my_client();
      
      let fut = client.get_favorite_programming_language_for_person("Ada Lovelace".to_string());      
      // Question 1:
      // at this point, no network IO, not even on the background thread should have been submitted, correct?

     let result = fut.await;
     // now, due to .await beginning the poll, the client should have both Submitted info and Received info over the network.
   
    assert_eq!(&result, "Rust");


   // Question 2:
   // if the answer to Question 1 is that absolutely no network IO should have been submitted prior to poll,
   // then does that also apply to stream? 
   let stream = client.get_favorite_programming_libraries_for_person("Ada Lovelace");
   let mut stream = std::pin::pin!(stream);   

   // Question 2 continued:
   // would the initial submission of this request over the internet only occur on the first poll_next?
   while let Some(item) = stream.next().await {
      println!("{item}");
   }

}

I feel like a good mental framework is to keep your API as flexible as possible (not foot-gunny) for the users.
Imagine one user collects a set of futures and sinks, that they plan on polling sometime later. If you perform the initial "start sending to me" I/O in the construction of the futures/sinks, then by the time the user polls to get the results, the remote server may have timed out or the results may have overflowed some queue.
Not to mention, if the user has a massive executor setup (hundreds of threads, say), then you don't want to bottleneck the (blocking) creation of the futures. This would undermine the scheduler of their chosen runtime.

1 Like

Thank you for your quick response.

If I understand your summary correctly, as far as Question 1 is concerned, you agree that no I/O should occur at all prior to the first poll, correct?

As for Question 2, do you also agree that no outgoing IO should be submitted prior to the first .next() call on the stream?

Agree

1 Like