Skip to content

Feature Request - Non-Blocking Async Execution from within Rust #767

Description

@Leo-Aus-Berlin

The Nemo engine can be started from within Rust. To do this, we must initialize the reasoner and then await its output, meaning we are forced to use asynchronous programming:

// main function is async
let nemo_engine_input = nemo::api::Engine::initialize(
        input_program,
        ImportManager::new(ResourceProviders::default()),
    );
let output = nemo_engine_input.await

As such, one could expect nemo to support async programming utilities, such as a timeout https://docs.rs/tokio/latest/tokio/time/fn.timeout.html:

// main function is async
 let nemo_engine_input = nemo::api::Engine::initialize(
        input_program,
        ImportManager::new(ResourceProviders::default()),
    );
    let output = match timeout(Duration::from_mins(timeout_after_mins), nemo_engine_input).await {
        Err(time) => {
            error!("Timeout after {time}!");
            // Store statistics and that I timed out
            exit(1);
        }
        Ok(res) => match res {
            Err(r) => {
                error!("Failed nemo calc.");
                error!("{r}");
                exit(1);
            }
            Ok(v) => v,
        },
    };

In practice, this is not the case, because Nemo blocks the thread. This is not necessarily an issue, but perhaps non-idiomatic to asynchronous programming:

https://rust-lang.github.io/async-book/part-guide/more-async-await.html#blocking-computation

You can also block the thread by doing computation (this is not quite the same as blocking I/O, since the OS is not involved, but the effect is similar). If you have a long-running computation (with or without blocking I/O) without yielding control to the runtime, then that task will never give the runtime's scheduler a chance to schedule other tasks. Remember that async programming uses cooperative multitasking. Here a task is not cooperating, so other tasks won't get a chance to get work done. We'll discuss ways to mitigate this later.

https://rust-lang.github.io/async-book/part-guide/io.html#other-blocking-operations

Doing long-running (i.e., cpu-intensive or cpu-bound) work will prevent the scheduler from running other tasks. This is a kind of blocking, but it is not as bad as blocking on IO or waiting because at least your program is making some progress. However (without care and consideration), it is likely to be sub-optimal for performance by some measure (e.g., tail latency) and perhaps a correctness issue if the tasks that can't run needed to be run at a particular time. There is a meme that you should simply not use async Rust (or general purpose async runtimes like Tokio) for CPU-intensive work, but that is an over-simplification. What is correct is that you cannot mix IO- and CPU-bound (or more precisely, long-running and latency-sensitive) tasks without some special handling and expect to have a good time.

I believe this could be fixed by having Nemo engine spawn its own threads and await them, meaning the main Nemo thread waits occasionally, allowing the scheduler to allow the thread that spawned the Nemo engine to check whether the timeout has been reached, but I am new to asynchronous Rust programming, so I am not sure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    apipublic crate API

    Fields

    No fields configured for Feature.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions