0

Problem

I'm learning about Tonic. I'm trying to write an automated test where I spin up a server in the background, call it via a generated client, and then stop the server.

Attempts

What I have currently looks like this:

//...
pub fn build(inv: impl Inventory) -> Result<Router, Box<dyn Error>> {
    let reflection_service = tonic_reflection::server::Builder::configure()
        .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
        .build()?;

    let srv = Server::builder()
        .add_service(InventoryServer::new(inv))
        .add_service(reflection_service);

    Ok(srv)
}

#[tokio::test]
async fn works() -> Result<(), Box<dyn Error>> {
    let addr = "127.0.0.1:9001".parse()?;
    let inventory = InMemoryInventory::default();

    let (handle, registration) = AbortHandle::new_pair();
    _ = Abortable::new(build(inventory)?.serve(addr), registration); // does not start

    let sku = "test".to_string();
    let add_res = add::add(AddOptions { sku: sku.clone(), price: 0.0, quantity: 0, name: None, description: None }).await?;
    println!("{:?}", add_res);
    let get_res = get::get(GetOptions { sku }).await?;
    println!("{:?}", get_res);

    handle.abort();
    Ok(())
}

I understand that the problem here comes from the fact that the future for serve is not triggered, thus the server cannot start. If I append await, the server starts on the current thread and blocks it.

I tried to create a new thread with tokio::spawn:

_ = Abortable::new(tokio::spawn(async move {
    let addr = "127.0.0.1:9001".parse().unwrap();
    let inventory = InMemoryInventory::default();
    build(inventory).unwrap().serve(addr).await
}), registration);

But then I get the following error:

error: future cannot be sent between threads safely
   --> svc-store/src/main.rs:36:37
    |
36  |       _ = Abortable::new(tokio::spawn(async move {
    |  _____________________________________^
37  | |         let addr = "127.0.0.1:9001".parse().unwrap();
38  | |         let inventory = InMemoryInventory::default();
39  | |         build(inventory).unwrap().serve(addr).await
40  | |     }), registration);
    | |_____^ future created by async block is not `Send`
    |
    = help: the trait `std::marker::Send` is not implemented for `dyn std::error::Error`

Which sends me even deeper down the rabbit hole of difficult terms. I just want to spin up a background task with dependencies that can be safely stored in that background thread, no need for any thread-to-thread communication.

Questions

  • What is the easiest way to achieve what I need?
  • Why doesn't the second approach work how a "Go programmer" would expect?
bart-kosmala
  • 931
  • 1
  • 11
  • 20

1 Answers1

0

The minimal solution I came up with was to:

  1. Remove Result typing from the build function and just return the success.
pub fn build(inv: impl Inventory) -> Router {
    let reflection_service = Builder::configure()
        .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
        .build()
        .expect("reflection service could not build");

    Server::builder()
        .add_service(InventoryServer::new(inv))
        .add_service(reflection_service)
}
  1. Use tokio::spawn as tried above for the concurrent code (this works because tokio::test ensures a tokio::Runtime.

#[tokio::test]
async fn works() -> Result<(), Box<dyn Error>> {
    tokio::spawn(async move {
        let addr = "127.0.0.1:9001".parse().unwrap();
        let inventory = InMemoryInventory::default();
        build(inventory).serve(addr).await.unwrap();
    });

    // TODO: make fancier (grpc hc?)
    sleep(Duration::from_secs(3)).await;

    let sku = "test".to_string();

    add::add(AddOptions { sku: sku.clone(), price: 1.0, quantity: 0, name: None, description: None }).await?;
    let item = get::get(GetOptions { sku }).await?.into_inner();

    assert_eq!(item.identifier.expect("no item id").sku, "test");
    Ok(())
}

That being said, I would still very much enjoy someone knowledgeable explaining why dyn Error is the whole issue here.

bart-kosmala
  • 931
  • 1
  • 11
  • 20
  • `dyn Error` sais that the type implements error and nothing else. If that type needs to move between threads as in the question, then it needs to have additional bounds added. You could instead update the answer to add the required bound and return `Result>` – rydrman Jul 14 '23 at 21:37