embassy
embassy copied to clipboard
[`embassy-time`]: Provide a way to delay a function call without blocking the async flow (`setTimeout()`)
Use case
There are moments where a programmer would want to delay a function call, without blocking the flow of execution of the code following said call.
For example, here's a pseudo-code for initiating a system reset within an HTTP response:
// Will reset the board to factory settings. Be careful calling this route.
(Method::Get, "/factory-reset") => {
connection
.initiate_response(200, Some("OK"), &[("Content-Type", "text/plain")])
.await?;
// Delay reset call to let client receive response
set_timeout(reset_to_factory_defaults, 2000).await.unwrap();
connection
.write_all(b"Device is rebooting with factory defaults.")
.await
}
Here we see that immediately calling the reset function wouldn't be ideal, because the client would not receive the response. This API would be pretty much the equivalent of setTimeout in JavaScript.
Currently, I came up with the following solution to mimic this functionality:
/// Executes a function call after a specified delay.
///
/// Note: Due to the nature of embassy tasks, we can only have a timeout for the maximum amount of
/// tasks we can spawn. If more than 5 timeouts are waiting at the same time, calling this function
/// again will return a [SpawnError].
pub async fn set_timeout(f: fn(), delay_ms: u64) -> Result<(), SpawnError> {
embassy_executor::Spawner::for_current_executor()
.await
.spawn(set_timeout_task(f, delay_ms))
}
/// Task for executing a function call after a specified delay.
#[embassy_executor::task(pool_size = 5)]
async fn set_timeout_task(f: fn(), delay_ms: u64) {
embassy_time::Timer::after_millis(delay_ms).await;
f()
}
This isn't ideal because it relies on the executor, spawning a new task, meaning there's a limit to the amount of timeouts one can have at the same time (although one could argue it's a feature), and also it isn't integrated in embassy-time allowing more people to easily use it. This solution also specifically depends on embassy-executor, but I can't think of an executor agnostic alternative.
Proposed Solution
Offer a similar API as JavaScript's setTimeout in embassy-time, that allows delaying calling a callback without blocking the flow of the execution of the code, that is optimized to work with the embassy framework, and if possible, can be executor agnostic.
this isn't really possible to do no-alloc.
When the user calls setTimeout we'd have to record somewhere "closure X must run at time T". It can't be in local variables because you'd expect if you return the timeout still fires. This leaves only global state, but if we're not using alloc it must have a fixed size, which would mean setTimeouts would start failing if you enqueue too many.
I understand the allocation issue, and it's better that we use a fixed size (user-defined possibly) so that someone doesn't hit a surprise by over allocating too many timeouts and having some guards over such faults.
Can this still be done in an executor agnostic way? I've looked into Async Rust, and it doesn't seem to allow such architecture with its paradigm.
If not, I believe we should still include a similar feature, with improvements and user-friendlyness. I'm not sure where it would live tho. Considering the inter-dependency of embassy-time <=> embassy-executor.
If anything, this issue will serve as a reference for someone wanting to implement such feature in their project.