YARR (Yet Another Runtime Refactor)

* Minimize dependencies, no longer depending on smol directly, only async-excecutor and its utility crates
* Error callback is no longer thread safe, but faster
* Improved documentation and panic messages for internal workings of runtime
* Depend on mlua exact version needed and serialize feature
* Change crate name
This commit is contained in:
Filip Tibell 2024-01-27 15:17:09 +01:00
parent 3d6bf6e80c
commit 053b85e0c1
No known key found for this signature in database
11 changed files with 180 additions and 222 deletions

156
Cargo.lock generated
View file

@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c"
dependencies = [
"concurrent-queue",
"event-listener 4.0.3",
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
@ -21,7 +21,7 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c"
dependencies = [
"async-lock 3.3.0",
"async-lock",
"async-task",
"concurrent-queue",
"fastrand",
@ -35,7 +35,7 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1f344136bad34df1f83a47f3fd7f2ab85d75cb8a940af4ccf6d482a84ea01b"
dependencies = [
"async-lock 3.3.0",
"async-lock",
"blocking",
"futures-lite",
]
@ -46,7 +46,7 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744"
dependencies = [
"async-lock 3.3.0",
"async-lock",
"cfg-if",
"concurrent-queue",
"futures-io",
@ -59,73 +59,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "async-lock"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
dependencies = [
"event-listener 2.5.3",
]
[[package]]
name = "async-lock"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b"
dependencies = [
"event-listener 4.0.3",
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-net"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-process"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15c1cd5d253ecac3d3cf15e390fd96bd92a13b1d14497d81abf077304794fb04"
dependencies = [
"async-channel",
"async-io",
"async-lock 3.3.0",
"async-signal",
"blocking",
"cfg-if",
"event-listener 4.0.3",
"futures-lite",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
name = "async-signal"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
dependencies = [
"async-io",
"async-lock 2.8.0",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.48.0",
]
[[package]]
name = "async-task"
version = "4.7.0"
@ -157,7 +101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
dependencies = [
"async-channel",
"async-lock 3.3.0",
"async-lock",
"async-task",
"fastrand",
"futures-io",
@ -206,6 +150,15 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "erased-serde"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55d05712b2d8d88102bc9868020c9e5c7a1f5527c452b9b97450a1d006140ba7"
dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.3.8"
@ -216,12 +169,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "4.0.3"
@ -239,7 +186,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
dependencies = [
"event-listener 4.0.3",
"event-listener",
"pin-project-lite",
]
@ -337,12 +284,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3561f79659ff3afad7b25e2bf2ec21507fe601ebecb7f81088669ec4bfd51e"
dependencies = [
"bstr",
"erased-serde",
"futures-util",
"libloading",
"mlua-sys",
"num-traits",
"once_cell",
"rustc-hash",
"serde",
"serde-value",
]
[[package]]
name = "mlua-luau-runtime"
version = "0.0.0"
dependencies = [
"async-executor",
"async-fs",
"async-io",
"concurrent-queue",
"event-listener",
"futures-lite",
"mlua",
]
[[package]]
@ -372,6 +335,15 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]]
name = "parking"
version = "2.2.0"
@ -467,6 +439,16 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
@ -478,15 +460,6 @@ dependencies = [
"syn",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -496,33 +469,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "smol"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e635339259e51ef85ac7aa29a1cd991b957047507288697a690e80ab97d07cad"
dependencies = [
"async-channel",
"async-executor",
"async-fs",
"async-io",
"async-lock 3.3.0",
"async-net",
"async-process",
"blocking",
"futures-lite",
]
[[package]]
name = "smol-mlua"
version = "0.0.0"
dependencies = [
"concurrent-queue",
"event-listener 4.0.3",
"mlua",
"smol",
]
[[package]]
name = "syn"
version = "2.0.48"

View file

@ -1,17 +1,28 @@
[package]
name = "smol-mlua"
name = "mlua-luau-runtime"
version = "0.0.0"
edition = "2021"
[dependencies]
concurrent-queue = "2.4"
event-listener = "4.0"
smol = "2.0"
mlua = { version = "0.9", features = ["luau", "luau-jit", "async"] }
[lib]
path = "lib/lib.rs"
[dependencies]
async-executor = "1.8"
concurrent-queue = "2.4"
event-listener = "4.0"
futures-lite = "2.2"
mlua = { version = "0.9.5", features = [
"luau",
"luau-jit",
"async",
"serialize",
] }
[dev-dependencies]
async-fs = "2.1"
async-io = "2.3"
[[example]]
name = "basic_sleep"
test = true

View file

@ -1,25 +1,22 @@
<!-- markdownlint-disable MD033 -->
<!-- markdownlint-disable MD041 -->
<h1 align="center">smol-mlua</h1>
<h1 align="center">mlua-luau-runtime</h1>
<div align="center">
<div>
<a href="https://github.com/lune-org/smol-mlua/actions">
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/lune-org/smol-mlua/ci.yaml" alt="CI status" />
<a href="https://github.com/lune-org/mlua-luau-runtime/actions">
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/lune-org/mlua-luau-runtime/ci.yaml" alt="CI status" />
</a>
<a href="https://github.com/lune-org/smol-mlua/blob/main/LICENSE.txt">
<img src="https://img.shields.io/github/license/lune-org/smol-mlua.svg?label=License&color=informational" alt="Crate license" />
<a href="https://github.com/lune-org/mlua-luau-runtime/blob/main/LICENSE.txt">
<img src="https://img.shields.io/github/license/lune-org/mlua-luau-runtime.svg?label=License&color=informational" alt="Crate license" />
</a>
</div>
</div>
<br/>
Integration between [smol] and [mlua] that provides a fully functional and asynchronous Luau runtime using smol executor(s).
[smol]: https://crates.io/crates/smol
[mlua]: https://crates.io/crates/mlua
Luau-based async runtime for [`mlua`](https://crates.io/crates/mlua), built on top of [`async-executor`](https://crates.io/crates/async-executor).
## Example Usage
@ -27,10 +24,13 @@ Integration between [smol] and [mlua] that provides a fully functional and async
```rs
use std::time::{Duration, Instant};
use std::io::ErrorKind;
use async_io::{block_on, Timer};
use async_fs::read_to_string;
use mlua::prelude::*;
use smol::{Timer, io, fs::read_to_string}
use smol_mlua::Runtime;
use mlua_luau_runtime::*;
```
### 2. Set up lua environment
@ -55,7 +55,7 @@ lua.globals().set(
let task = lua.spawn(async move {
match read_to_string(path).await {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
});
@ -64,7 +64,7 @@ lua.globals().set(
)?;
```
### 3. Run
### 3. Set up runtime, run threads
```rs
let rt = Runtime::new(&lua)?;
@ -77,7 +77,6 @@ let fileThread = lua.load("readFile(\"Cargo.toml\")");
rt.spawn_thread(sleepThread, ());
rt.spawn_thread(fileThread, ());
// ... and run either async or blocking, until they finish
rt.run_async().await;
rt.run_blocking();
// ... and run until they finish
block_on(rt.run());
```

View file

@ -1,8 +1,9 @@
use std::time::{Duration, Instant};
use async_io::{block_on, Timer};
use mlua::prelude::*;
use smol::Timer;
use smol_mlua::Runtime;
use mlua_luau_runtime::*;
const MAIN_SCRIPT: &str = include_str!("./lua/basic_sleep.luau");
@ -18,11 +19,13 @@ pub fn main() -> LuaResult<()> {
})?,
)?;
// Load the main script into a runtime and run it until completion
// Load the main script into a runtime
let rt = Runtime::new(&lua)?;
let main = lua.load(MAIN_SCRIPT);
rt.spawn_thread(main, ())?;
rt.run_blocking();
// Run until completion
block_on(rt.run());
Ok(())
}

View file

@ -1,6 +1,10 @@
use std::io::ErrorKind;
use async_fs::read_to_string;
use async_io::block_on;
use mlua::prelude::*;
use smol::{fs::read_to_string, io};
use smol_mlua::{LuaSpawnExt, Runtime};
use mlua_luau_runtime::*;
const MAIN_SCRIPT: &str = include_str!("./lua/basic_spawn.luau");
@ -14,7 +18,7 @@ pub fn main() -> LuaResult<()> {
let task = lua.spawn(async move {
match read_to_string(path).await {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
});
@ -22,11 +26,13 @@ pub fn main() -> LuaResult<()> {
})?,
)?;
// Load the main script into a runtime and run it until completion
// Load the main script into a runtime
let rt = Runtime::new(&lua)?;
let main = lua.load(MAIN_SCRIPT);
rt.spawn_thread(main, ())?;
rt.run_blocking();
// Run until completion
block_on(rt.run());
Ok(())
}

View file

@ -1,5 +1,7 @@
use mlua::prelude::*;
use smol_mlua::Runtime;
use mlua_luau_runtime::*;
use async_io::block_on;
const MAIN_SCRIPT: &str = include_str!("./lua/callbacks.luau");
@ -17,10 +19,12 @@ pub fn main() -> LuaResult<()> {
);
});
// Load and run the main script until completion
// Load the main script into a runtime
let main = lua.load(MAIN_SCRIPT);
rt.spawn_thread(main, ())?;
rt.run_blocking();
// Run until completion
block_on(rt.run());
Ok(())
}

View file

@ -1,8 +1,9 @@
use std::time::Duration;
use async_io::{block_on, Timer};
use mlua::prelude::*;
use smol::Timer;
use smol_mlua::Runtime;
use mlua_luau_runtime::*;
const MAIN_SCRIPT: &str = include_str!("./lua/lots_of_threads.luau");
@ -30,10 +31,12 @@ pub fn main() -> LuaResult<()> {
})?,
)?;
// Load the main script into the runtime and run it until completion
// Load the main script into the runtime
let main = lua.load(MAIN_SCRIPT);
rt.spawn_thread(main, ())?;
rt.run_blocking();
// Run until completion
block_on(rt.run());
Ok(())
}

View file

@ -1,8 +1,9 @@
use std::time::{Duration, Instant};
use async_io::{block_on, Timer};
use mlua::prelude::*;
use smol::Timer;
use smol_mlua::Runtime;
use mlua_luau_runtime::*;
const MAIN_SCRIPT: &str = include_str!("./lua/scheduler_ordering.luau");
@ -23,10 +24,12 @@ pub fn main() -> LuaResult<()> {
})?,
)?;
// Load the main script into a runtime and run it until completion
// Load the main script into a runtime
let main = lua.load(MAIN_SCRIPT);
rt.spawn_thread(main, ())?;
rt.run_blocking();
// Run until completion
block_on(rt.run());
Ok(())
}

View file

@ -1,42 +1,32 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::{cell::RefCell, rc::Rc};
use mlua::prelude::*;
use smol::lock::Mutex;
type ErrorCallback = Box<dyn Fn(LuaError) + Send + 'static>;
#[derive(Clone)]
pub(crate) struct ThreadErrorCallback {
exists: Arc<AtomicBool>,
inner: Arc<Mutex<Option<ErrorCallback>>>,
inner: Rc<RefCell<Option<ErrorCallback>>>,
}
impl ThreadErrorCallback {
pub fn new() -> Self {
Self {
exists: Arc::new(AtomicBool::new(false)),
inner: Arc::new(Mutex::new(None)),
inner: Rc::new(RefCell::new(None)),
}
}
pub fn replace(&self, callback: impl Fn(LuaError) + Send + 'static) {
self.exists.store(true, Ordering::Relaxed);
self.inner.lock_blocking().replace(Box::new(callback));
self.inner.borrow_mut().replace(Box::new(callback));
}
pub fn clear(&self) {
self.exists.store(false, Ordering::Relaxed);
self.inner.lock_blocking().take();
self.inner.borrow_mut().take();
}
pub fn call(&self, error: &LuaError) {
if self.exists.load(Ordering::Relaxed) {
if let Some(cb) = &*self.inner.lock_blocking() {
cb(error.clone());
}
if let Some(cb) = &*self.inner.borrow() {
cb(error.clone());
}
}
}

View file

@ -1,10 +1,9 @@
use std::sync::{Arc, Weak};
use std::time::Duration;
use futures_lite::prelude::*;
use mlua::prelude::*;
use smol::{prelude::*, Timer};
use smol::{block_on, Executor, LocalExecutor};
use async_executor::{Executor, LocalExecutor};
use super::{
error_callback::ThreadErrorCallback, queue::ThreadQueue, traits::IntoLuaThread,
@ -142,54 +141,57 @@ impl<'lua> Runtime<'lua> {
Runs the runtime until all Lua threads have completed.
Note that the given Lua state must be the same one that was
used to create this runtime, otherwise this method may panic.
used to create this runtime, otherwise this method will panic.
*/
pub async fn run_async(&self) {
// Make sure we do not already have an executor - this is a definite user error
// and may happen if the user tries to run multiple runtimes on the same lua state
if self.lua.app_data_ref::<Weak<Executor>>().is_some() {
panic!(
"Lua state already has an executor attached!\
\nOnly one runtime can be used per lua state."
);
}
pub async fn run(&self) {
/*
Create new executors to use - note that we do not need create multiple executors
for work stealing, the user may do that themselves if they want to and it will work
just fine, as long as anything async is .await-ed from within a lua async function.
// Create new executors to use - note that we do not need to create multiple executors
// for work stealing, using the `spawn` global function that smol provides will work
// just fine, as long as anything spawned by it is awaited from lua async functions
The main purpose of the two executors here is just to have one with
the Send bound, and another (local) one without it, for lua scheduling.
We also use the main executor to drive the main loop below forward,
saving a tiny bit of processing from going on the lua executor itself.
*/
let lua_exec = LocalExecutor::new();
let main_exec = Arc::new(Executor::new());
// Store the main executor in lua for spawner trait
/*
Store the main executor in lua, so that it may be used with LuaSpawnExt.
Also ensure we do not already have an executor - this is a definite user error
and may happen if the user tries to run multiple runtimes on the same lua state.
*/
if self.lua.app_data_ref::<Weak<Executor>>().is_some() {
panic!(
"Lua state already has an executor attached!\
\nThis may be caused by running multiple runtimes on the same lua state, or a call to Runtime::run being cancelled.\
\nOnly one runtime can be used per lua state at once, and runtimes must always run until completion."
);
}
self.lua.set_app_data(Arc::downgrade(&main_exec));
// Create a timer for a resumption cycle / throttling mechanism, waiting on this
// will allow us to batch more work together when the runtime is under high load,
// and adds an acceptable amount of latency for new async tasks (we run at 250hz)
let mut cycle = Timer::interval(Duration::from_millis(4));
/*
Manually tick the lua executor, while running under the main executor.
Each tick we wait for the next action to perform, in prioritized order:
// Tick local lua executor while also driving main
// executor forward, until all lua threads finish
1. A lua thread is available to run on the spawned queue
2. A lua thread is available to run on the deferred queue
3. Task(s) scheduled on the lua executor have made progress and should be polled again
This ordering is vital to ensure that we don't accidentally exit the main loop
when there are new lua threads to enqueue and potentially more work to be done.
*/
let fut = async {
loop {
// Wait for a new thread to arrive __or__ next futures step, prioritizing
// new threads, so we don't accidentally exit when there is more work to do
let fut_spawn = self.queue_spawn.wait_for_item();
let fut_defer = self.queue_defer.wait_for_item();
let fut_tick = async {
lua_exec.tick().await;
// Do as much work as possible
loop {
if !lua_exec.try_tick() {
break;
}
}
};
let fut_spawn = self.queue_spawn.wait_for_item(); // 1
let fut_defer = self.queue_defer.wait_for_item(); // 2
let fut_tick = lua_exec.tick(); // 3
fut_spawn.or(fut_defer).or(fut_tick).await;
// If a new thread was spawned onto any queue,
// we must drain them and schedule on the executor
let process_thread = |thread: LuaThread<'lua>, args| {
// NOTE: Thread may have been cancelled from lua
// before we got here, so we need to check it again
@ -199,12 +201,11 @@ impl<'lua> Runtime<'lua> {
.spawn(async move {
// Only run stream until first coroutine.yield or completion. We will
// drop it right away to clear stack space since detached tasks dont drop
// until the executor drops https://github.com/smol-rs/smol/issues/294
// until the executor drops (https://github.com/smol-rs/smol/issues/294)
let res = stream.next().await.unwrap();
if let Err(e) = &res {
self.error_callback.call(e);
}
// TODO: Figure out how to give this result to caller of spawn_thread/defer_thread
})
.detach();
}
@ -218,28 +219,17 @@ impl<'lua> Runtime<'lua> {
process_thread(thread, args);
}
// Empty executor = no remaining threads
// Empty executor = we didn't spawn any new lua tasks
// above, and there are no remaining tasks to run later
if lua_exec.is_empty() {
break;
}
// Wait for next resumption cycle
cycle.next().await;
}
};
main_exec.run(fut).await;
// Make sure we don't leave any references behind
// Clean up
self.lua.remove_app_data::<Weak<Executor>>();
}
/**
Runs the runtime until all Lua threads have completed, blocking the thread.
See [`ThreadRuntime::run_async`] for more info.
*/
pub fn run_blocking(&self) {
block_on(self.run_async())
}
}

View file

@ -1,7 +1,8 @@
use std::{future::Future, sync::Weak};
use mlua::prelude::*;
use smol::{Executor, Task};
use async_executor::{Executor, Task};
/**
Trait for any struct that can be turned into an [`LuaThread`]
@ -62,8 +63,10 @@ pub trait LuaSpawnExt<'lua> {
### Example usage
```rust
use async_io::block_on;
use mlua::prelude::*;
use smol_mlua::{Runtime, LuaSpawnExt};
use mlua_luau_runtime::*;
fn main() -> LuaResult<()> {
let lua = Lua::new();
@ -80,7 +83,7 @@ pub trait LuaSpawnExt<'lua> {
let rt = Runtime::new(&lua)?;
rt.spawn_thread(lua.load("spawnBackgroundTask()"), ());
rt.run_blocking();
block_on(rt.run());
Ok(())
}