bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
34.37k stars 3.36k forks source link

Calling .await inside async in AsyncComputeTaskPool.spawn() causes tokio panic #2532

Open chaoticgood1 opened 3 years ago

chaoticgood1 commented 3 years ago

Bevy version

0.5

Operating system & version

Ubuntu 20.04

What you did

Run the code

use bevy::{
  prelude::*,
  tasks::{AsyncComputeTaskPool, Task},
};
use futures_lite::future;
use rand::Rng;
use tokio::time::delay_for;
use std::time::{Duration, Instant};

fn main() {
  let mut app = App::build();

  app
    .add_plugins(MinimalPlugins)
    .add_system(spawn_tasks.system())
    .add_system(handle_tasks.system())
    .run();
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Result {
  pub time: f32
}

fn spawn_tasks(mut commands: Commands, thread_pool: Res<AsyncComputeTaskPool>) {
  let task = thread_pool.spawn(async move {
    delay_for(Duration::from_millis(1000)).await;
    Result { time: 1.0 }
  });
  commands.spawn().insert(task);
}

fn handle_tasks(
  mut commands: Commands,
  mut transform_tasks: Query<(Entity, &mut Task<Result>)>,
) {
  for (entity, mut task) in transform_tasks.iter_mut() {
    if let Some(res) = future::block_on(future::poll_once(&mut *task)) {
      commands.entity(entity).remove::<Task<Result>>();
    }
  }
}

What you expected to happen

Should run with no panic

What actually happened

Panics and shows this error

thread 'Async Compute Task Pool (3)' panicked at 'there is no timer running, must be called from the context of a Tokio 0.2.x runtime', .cargo/registry/src/github.com-1ecc6299db9ec823/tokio-0.2.25/src/time/driver/handle.rs:24:32
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'Async Compute Task Pool (2)' panicked at 'there is no timer running, must be called from the context of a Tokio 0.2.x runtime', .cargo/registry/src/github.com-1ecc6299db9ec823/tokio-0.2.25/src/time/driver/handle.rs:24:32
thread 'Async Compute Task Pool (1)' panicked at 'there is no timer running, must be called from the context of a Tokio 0.2.x runtime', .cargo/registry/src/github.com-1ecc6299db9ec823/tokio-0.2.25/src/time/driver/handle.rs:24:32
thread 'Async Compute Task Pool (0)' panicked at 'there is no timer running, must be called from the context of a Tokio 0.2.x runtime', .cargo/registry/src/github.com-1ecc6299db9ec823/tokio-0.2.25/src/time/driver/handle.rs:24:32
thread 'Compute Task Pool (7)' panicked at 'task has failed', .cargo/registry/src/github.com-1ecc6299db9ec823/async-task-4.0.3/src/task.rs:368:45
thread 'main' panicked at 'task has failed', .cargo/registry/src/github.com-1ecc6299db9ec823/async-task-4.0.3/src/task.rs:368:45

Bug confirmed with @ashneverdawn

bjorn3 commented 3 years ago

You are trying to use a tokio future (as returned by tokio::time::delay_for) with the async-executor runtime used by bevy. This isn't allowed. A tokio future can only be awaited within a tokio runtime, as the tokio future needs to tell the tokio runtime when to wake up again. The async-executor runtime doesn't have any timer functionality builtin and especially not a tokio flavor timer functionality.

DJMcNab commented 3 years ago

Our tasks infrastructure is not tokio based. Imo, it is a tokio bug that this future is not generically callable, however I also don't think tokio could have a better strategy - this area likely needs more work.

The easiest way to do this is to try using async_std::task::sleep, which might work for you. Otherwise you'll have to create your own tokio threadpool, i.e. a version of bevy_tasks based on tokio. A quick search suggest that https://crates.io/crates/bevy_spicy_networking might be tokio based too, so investigating that would be useful

DJMcNab commented 3 years ago

The title is misleading; I think a more accurate title would be something like "Cannot use tokio tasks within a TaskPool".

(and we could probably move this to a discussion too, would be titled 'How to use tokio tasks with bevy?')

chaoticgood1 commented 3 years ago

Oh, sorry for the vague sample, I am required to call await inside the async because we need to use sqlx to query database.

chaoticgood1 commented 3 years ago

Something like this. I just use the delay_for() to simulate the work done and use await, as we detected it to be the cause of the problem.

let task = thread_pool.spawn(async move {
    let query = sqlx::query_as!(Data,
     "query here" )
    .fetch_all(&db)
    .await;
    // Process result
    Result
  });
commands.spawn().insert(task);
mockersf commented 3 years ago

The issue is not the await, it's the async runtime used by sqlx. It seems it supports multiple runtime, which are you using?

https://github.com/launchbadge/sqlx/blob/f0d0dce8e25c40dffd1431776b6a38510a70bde0/Cargo.toml#L71-L73

chaoticgood1 commented 3 years ago

We are using tokio 0.2.22 and sqlx 0.4.2.

mockersf commented 3 years ago

As said above, Tokyo won't easily work with Bevy.

You could try with runtime async-std, it should be better supported (though I didn't try myself)

hanabi1224 commented 2 years ago

It will be great to be able to change the async execution engine, it's actually possible to achieve this with very minimal changes by using async-global-executor, which is built on top of async-executor and has the capability to switch to async-io / tokio backend. I tried locally both async-io and tokio works fine with the examples, and tokio works fine with the code above, I can put up a PR for review.

BTW, I did some performance tests before (not related to bevy tho) and found different async execution engines do have non-trivial performance gaps in dealing with massive tasks, like 20%~30% in my test scenario. (It showed tokio is much more performant)

elpiel commented 2 years ago

For anyone looking to run tokio executor compatible async code (including tokio functions like tokio::time::sleep), you can do so using the async-compat crate:

https://crates.io/crates/async-compat

ghardin1314 commented 1 year ago

For anyone looking to run tokio executor compatible async code (including tokio functions like tokio::time::sleep), you can do so using the async-compat crate:

https://crates.io/crates/async-compat

are there any performance implications for using this with multiple requests?

the future will enter the context of a global single-threaded tokio runtime started by this crate

So I think this means all futures that are passed through the compat are run on a single thread. Granted this is probably performant enough for me, just curious on anyone else's considerations