How to run a tonic server in the background for a test

565 Views Asked by At

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?
1

There are 1 best solutions below

1
bart-kosmala On

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.