Async Design Pattern Help

Hello Rustaceans!

I can't see to find the correct design for my system.

I've got a gRPC server responding to requests. I want the structural type implementing the server to accept a request, then process it in an async function.

use tokio::runtime::Runtime;

struct ConcreteServer {
    pool: Arc<Runtime>,
    cache: Mutex<Vec<i32>>,
}
impl Server for ConcreteServer {
    async fn ping(&self, request: Request<PingRequest>) -> Result<Response<PingResponse> {
        // Spawn a new thread to do work on.
        self.pool.spawn(async {
             // Perform work using fields and methods on the ConcreteServer type.
             // All such fields are Send and Sync.
            let mut data = self.cache.lock().unwrap();
            data.push(5);
        });
        // Reply immediately.
        let reply = PingReply {};
        Ok(Response::new(reply))
    }
}

The error I'm getting is cannot infer an appropriate lifetime for autoref due to conflicting requirements.
I think I understand the error: the lifetime of &self is not guaranteed to outlive the lifetime of the spawned thread. ping could up the stack and eventually deallocate the stack frame containing the ConcreteServer, creating a use-after-free in my spawned thread.

So I'm obviously designing my programs incorrectly. I can't figure out a way to do what I'd like, which is to offload work to a thread, using the TCP connections I'm caching on the ConcreteServer struct. How can I lend a field from a struct to another thread, promising that the lifetime of the ConcreteServer is static?

Put your cache in an Arc like this:

struct ConcreteServer {
    pool: Arc<Runtime>,
    cache: Arc<Mutex<Vec<i32>>>,
}

then move a clone of the arc into the spawned task:

let cache = Arc::clone(&self.cache);
self.pool.spawn(async move {
     // Perform work using fields and methods on the ConcreteServer type.
     // All such fields are Send and Sync.
    let mut data = cache.lock().unwrap();
    data.push(5);
});
1 Like

Uhh, wow, that was easier than I thought it was going to be. I thought I had to make my server's lifetime static. This works.

I guess the reason is that the cloned atomic reference is cleaned up when the refcount=0, so it's lifetime isn't tied to self. Since Arc is theadsafe, you can move the reference into the spawned thread, side-stepping my two issues.

This was super useful. Thank you very much @alice for your help, and your speedy reply. I really appreciate it.

2 Likes

You could also put the entire ConcreteServer struct in an Arc instead, and define a method that takes self by-arc like this:

async fn ping(self: &Arc<Self>, request: Request<PingRequest>) -> Result<Response<PingResponse> {

Then you can clone self instead and access all the fields in your spawned task.

1 Like

I like that option a lot too!

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