1

I need to run an async function in actix::prelude::AsyncContext::run_interval, but I need to also pass in a struct member and return the result (not the future). This is a somewhat more complex version of this question here. As can be seen in the commented section below, I have tried a few approaches but all of them fail for one reason or another.

I have looked at a few related resources, including the AsyncContext trait and these StackOverflow questions: 3, 4.

Here is my example code (actix crate is required in Cargo.toml):

use std::time::Duration;

use actix::{Actor, Arbiter, AsyncContext, Context, System};

struct MyActor {
    id: i32
}

impl MyActor {
    fn new(id: i32) -> Self {
        Self {
            id: id,
        }
    }

    fn heartbeat(&self, ctx: &mut <Self as Actor>::Context) {
        ctx.run_interval(Duration::from_secs(1), |act, ctx| {
            //lifetime issue
            //let res = 0;
            //Arbiter::spawn(async {
            //    res = two(act.id).await;
            //});
            //future must return `()`
            //let res = Arbiter::spawn(two(act.id));
            //async closures unstable
            //let res = Arbiter::current().exec(async || {
            //    two(act.id).await
            //});
        });
    }
}

impl Actor for MyActor {
    type Context = Context<Self>;

    fn started(&mut self, ctx: &mut Self::Context) {
        self.heartbeat(ctx);
    }
}

// assume functions `one` and `two` live in another module
async fn one(id: i32) -> i32 {
    // assume something is done with id here
    let x = id;
    1
}

async fn two(id: i32) -> i32 {
    let x = id;
    // assume this may call other async functions
    one(x).await;
    2
}

fn main() {
    let mut system = System::new("test");
    system.block_on(async { MyActor::new(10).start() });
    system.run();
}

Rust version:

$ rustc --version
rustc 1.50.0 (cb75ad5db 2021-02-10)

1 Answers1

0

Using Arbiter::spawn would work, but the issue is with the data being accessed from inside the async block that's passed to Arbiter::spawn. Since you're accessing act from inside the async block, that reference will have to live longer than the closure that calls Arbiter::spawn. In fact, in will have to have a lifetime of 'static since the future produced by the async block could potentially live until the end of the program.

One way to get around this in this specific case, given that you need an i32 inside the async block, and an i32 is a Copy type, is to move it:

ctx.run_interval(Duration::from_secs(1), |act, ctx| {
    let id = act.id;
    Arbiter::spawn(async move {
        two(id).await;
    });
});

Since we're using async move, the id variable will be moved into the future, and will thus be available whenever the future is run. By assigning it to id first, we are actually copying the data, and it's the copy (id) that will be moved.

But this might not be what you want, if you're trying to get a more general solution where you can access the object inside the async function. In that case, it gets a bit tricker, and you might want to consider not using async functions if possible. If you must, it might be possible to have a separate object with the data you need which is surrounded by std::rc::Rc, which can then be moved into the async block without duplicating the underlying data.

transistor
  • 1,480
  • 1
  • 9
  • 12