Refactor new task scheduler

* Use cell & refcell, we don't need mutexes

* Split out into several modules
This commit is contained in:
Filip Tibell 2023-02-15 12:18:25 +01:00
parent 8d44e1e827
commit fa14e4a02b
No known key found for this signature in database
10 changed files with 489 additions and 493 deletions

View file

@ -6,13 +6,20 @@ use crate::{
}; };
const TASK_WAIT_IMPL_LUA: &str = r#" const TASK_WAIT_IMPL_LUA: &str = r#"
resume_after(thread(), ...) local seconds = ...
local current = thread()
resumeAfter(seconds, current)
return yield() return yield()
"#; "#;
const TASK_SPAWN_IMPL_LUA: &str = r#" const TASK_SPAWN_IMPL_LUA: &str = r#"
local task = resume_first(...) -- Schedule the current thread at the front
resume_second(thread()) scheduleNext(thread())
-- Schedule the thread to spawn at the front,
-- the previous schedule now comes right after
local task = scheduleNext(...)
-- Give control over to the scheduler, which will
-- resume the above tasks in order when its ready
yield() yield()
return task return task
"#; "#;
@ -46,8 +53,8 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'static>> {
.with_value("thread", task_wait_env_thread)? .with_value("thread", task_wait_env_thread)?
.with_value("yield", task_wait_env_yield)? .with_value("yield", task_wait_env_yield)?
.with_function( .with_function(
"resume_after", "resumeAfter",
|lua, (thread, secs): (LuaThread, Option<f64>)| { |lua, (secs, thread): (Option<f64>, LuaThread)| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_wait(secs.unwrap_or(0f64), LuaValue::Thread(thread)) sched.schedule_wait(secs.unwrap_or(0f64), LuaValue::Thread(thread))
}, },
@ -67,19 +74,12 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'static>> {
.with_value("thread", task_spawn_env_thread)? .with_value("thread", task_spawn_env_thread)?
.with_value("yield", task_spawn_env_yield)? .with_value("yield", task_spawn_env_yield)?
.with_function( .with_function(
"resume_first", "scheduleNext",
|lua, (tof, args): (LuaValue, LuaMultiValue)| { |lua, (tof, args): (LuaValue, LuaMultiValue)| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_current_resume(tof, args) sched.schedule_next(tof, args)
}, },
)? )?
.with_function("resume_second", |lua, thread: LuaThread| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_after_current_resume(
LuaValue::Thread(thread),
LuaMultiValue::new(),
)
})?
.build_readonly()?, .build_readonly()?,
)? )?
.into_function()?; .into_function()?;
@ -138,7 +138,7 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'static>> {
sched.create_task(TaskKind::Instant, LuaValue::Function(func), None, None)?; sched.create_task(TaskKind::Instant, LuaValue::Function(func), None, None)?;
lua.create_function(move |lua, args: LuaMultiValue| { lua.create_function(move |lua, args: LuaMultiValue| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.resume_task(task, Some(args.into_vec())) sched.resume_task(task, Some(Ok(args)))
}) })
})?, })?,
)?; )?;

View file

@ -103,7 +103,7 @@ impl Lune {
// Create our task scheduler and schedule the main thread on it // Create our task scheduler and schedule the main thread on it
let sched = TaskScheduler::new(lua)?.into_static(); let sched = TaskScheduler::new(lua)?.into_static();
lua.set_app_data(sched); lua.set_app_data(sched);
sched.schedule_current_resume( sched.schedule_next(
LuaValue::Function( LuaValue::Function(
lua.load(script_contents) lua.load(script_contents)
.set_name(script_name) .set_name(script_name)

View file

@ -0,0 +1,46 @@
use core::panic;
use mlua::prelude::*;
use tokio::sync::mpsc;
use super::message::TaskSchedulerMessage;
/**
A handle to a registered asynchronous background task.
[`TaskSchedulerAsyncHandle::unregister`] must be
called upon completion of the background task to
prevent the task scheduler from running indefinitely.
*/
#[must_use = "Background tasks must be unregistered"]
#[derive(Debug)]
pub struct TaskSchedulerAsyncHandle {
unregistered: bool,
sender: mpsc::UnboundedSender<TaskSchedulerMessage>,
}
impl TaskSchedulerAsyncHandle {
pub fn new(sender: mpsc::UnboundedSender<TaskSchedulerMessage>) -> Self {
Self {
unregistered: false,
sender,
}
}
pub fn unregister(mut self, result: LuaResult<()>) {
self.unregistered = true;
self.sender
.send(TaskSchedulerMessage::Terminated(result))
.unwrap_or_else(|_| {
panic!(
"\
\nFailed to unregister background task - this is an internal error! \
\nPlease report it at {} \
\nDetails: Manual \
",
env!("CARGO_PKG_REPOSITORY")
)
});
}
}

View file

@ -0,0 +1,8 @@
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub enum TaskSchedulerMessage {
NewBlockingTaskReady,
Spawned,
Terminated(LuaResult<()>),
}

View file

@ -1,3 +1,10 @@
mod async_handle;
mod message;
mod result;
mod scheduler; mod scheduler;
mod task_kind;
mod task_reference;
pub use scheduler::*; pub use scheduler::TaskScheduler;
pub use task_kind::TaskKind;
pub use task_reference::TaskReference;

View file

@ -0,0 +1,187 @@
use std::{fmt, process::ExitCode};
use mlua::prelude::*;
use super::TaskScheduler;
/// A struct representing the current state of the task scheduler
#[derive(Debug, Clone)]
pub struct TaskSchedulerState {
lua_error: Option<LuaError>,
exit_code: Option<ExitCode>,
num_blocking: usize,
num_futures: usize,
num_background: usize,
}
impl TaskSchedulerState {
pub(super) fn new(sched: &TaskScheduler) -> Self {
const MESSAGE: &str = "\
Failed to get lock on or borrow internal scheduler state!\
\nMake sure not to call during task scheduler resumption";
Self {
lua_error: None,
exit_code: sched.exit_code.get(),
num_blocking: sched
.tasks_queue_blocking
.try_borrow()
.expect(MESSAGE)
.len(),
num_futures: sched.futures.try_lock().expect(MESSAGE).len(),
num_background: sched.futures_registered_count.get(),
}
}
pub(super) fn err(sched: &TaskScheduler, err: LuaError) -> Self {
let mut this = Self::new(sched);
this.lua_error = Some(err);
this
}
pub(super) fn has_blocking_tasks(&self) -> bool {
self.num_blocking > 0
}
pub(super) fn has_future_tasks(&self) -> bool {
self.num_futures > 0
}
pub(super) fn has_background_tasks(&self) -> bool {
self.num_background > 0
}
/**
Returns a clone of the error from
this task scheduler result, if any.
*/
pub fn get_lua_error(&self) -> Option<LuaError> {
self.lua_error.clone()
}
/**
Returns a clone of the exit code from
this task scheduler result, if any.
*/
pub fn get_exit_code(&self) -> Option<ExitCode> {
self.exit_code
}
/**
Returns `true` if the task scheduler still
has blocking lua threads left to run.
*/
pub fn is_blocking(&self) -> bool {
self.num_blocking > 0
}
/**
Returns `true` if the task scheduler has finished all
blocking lua tasks, but still has yielding tasks running.
*/
pub fn is_yielding(&self) -> bool {
self.num_blocking == 0 && self.num_futures > 0
}
/**
Returns `true` if the task scheduler has finished all
lua threads, but still has background tasks running.
*/
pub fn is_background(&self) -> bool {
self.num_blocking == 0 && self.num_futures == 0 && self.num_background > 0
}
/**
Returns `true` if the task scheduler is done,
meaning it has no lua threads left to run, and
no spawned tasks are running in the background.
*/
pub fn is_done(&self) -> bool {
self.num_blocking == 0 && self.num_futures == 0 && self.num_background == 0
}
}
impl fmt::Display for TaskSchedulerState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.is_blocking() {
"Busy"
} else if self.is_yielding() {
"Yielding"
} else if self.is_background() {
"Background"
} else {
"Done"
};
let code = match self.get_exit_code() {
Some(code) => format!("{code:?}"),
None => "-".to_string(),
};
let err = match self.get_lua_error() {
Some(e) => format!("{e:?}")
.as_bytes()
.chunks(42) // Kinda arbitrary but should fit in most terminals
.enumerate()
.map(|(idx, buf)| {
format!(
"{}{}{}{}{}",
if idx == 0 { "" } else { "\n" },
if idx == 0 {
"".to_string()
} else {
" ".repeat(16)
},
if idx == 0 { "" } else { "" },
String::from_utf8_lossy(buf),
if buf.len() == 42 { "" } else { "" },
)
})
.collect::<String>(),
None => "-".to_string(),
};
let parts = vec![
format!("Status │ {status}"),
format!("Tasks active │ {}", self.num_blocking),
format!("Tasks background │ {}", self.num_background),
format!("Status code │ {code}"),
format!("Lua error │ {err}"),
];
let lengths = parts
.iter()
.map(|part| {
part.lines()
.next()
.unwrap()
.trim_end_matches("")
.chars()
.count()
})
.collect::<Vec<_>>();
let longest = &parts
.iter()
.enumerate()
.fold(0, |acc, (index, _)| acc.max(lengths[index]));
let sep = "".repeat(longest + 2);
writeln!(f, "┌{}┐", &sep)?;
for (index, part) in parts.iter().enumerate() {
writeln!(
f,
"│ {}{} │",
part.trim_end_matches(""),
" ".repeat(
longest
- part
.lines()
.last()
.unwrap()
.trim_end_matches("")
.chars()
.count()
)
)?;
if index < parts.len() - 1 {
writeln!(f, "┝{}┥", &sep)?;
}
}
write!(f, "└{}┘", &sep)?;
Ok(())
}
}

View file

@ -1,12 +1,8 @@
use core::panic; use core::panic;
use std::{ use std::{
cell::{Cell, RefCell},
collections::{HashMap, VecDeque}, collections::{HashMap, VecDeque},
fmt,
process::ExitCode, process::ExitCode,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc, Mutex,
},
time::Duration, time::Duration,
}; };
@ -20,58 +16,19 @@ use tokio::{
use crate::utils::table::TableBuilder; use crate::utils::table::TableBuilder;
type TaskSchedulerQueue = Arc<Mutex<VecDeque<TaskReference>>>; use super::{
async_handle::TaskSchedulerAsyncHandle, message::TaskSchedulerMessage,
result::TaskSchedulerState, task_kind::TaskKind, task_reference::TaskReference,
};
type TaskFutureArgsOverride<'fut> = Option<Vec<LuaValue<'fut>>>; type TaskFutureRets<'fut> = LuaResult<Option<LuaMultiValue<'fut>>>;
type TaskFutureReturns<'fut> = LuaResult<TaskFutureArgsOverride<'fut>>; type TaskFuture<'fut> = LocalBoxFuture<'fut, (TaskReference, TaskFutureRets<'fut>)>;
type TaskFuture<'fut> = LocalBoxFuture<'fut, (TaskReference, TaskFutureReturns<'fut>)>;
const TASK_ASYNC_IMPL_LUA: &str = r#" const TASK_ASYNC_IMPL_LUA: &str = r#"
resume_async(thread(), ...) resumeAsync(thread(), ...)
return yield() return yield()
"#; "#;
/// An enum representing different kinds of tasks
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TaskKind {
Instant,
Deferred,
Future,
}
impl fmt::Display for TaskKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name: &'static str = match self {
TaskKind::Instant => "Instant",
TaskKind::Deferred => "Deferred",
TaskKind::Future => "Future",
};
write!(f, "{name}")
}
}
/// A lightweight, copyable struct that represents a
/// task in the scheduler and is accessible from Lua
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TaskReference {
kind: TaskKind,
guid: usize,
}
impl TaskReference {
pub const fn new(kind: TaskKind, guid: usize) -> Self {
Self { kind, guid }
}
}
impl fmt::Display for TaskReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TaskReference({} - {})", self.kind, self.guid)
}
}
impl LuaUserData for TaskReference {}
/// A struct representing a task contained in the task scheduler /// A struct representing a task contained in the task scheduler
#[derive(Debug)] #[derive(Debug)]
pub struct Task { pub struct Task {
@ -80,244 +37,33 @@ pub struct Task {
queued_at: Instant, queued_at: Instant,
} }
/**
A handle to a registered background task.
[`TaskSchedulerUnregistrar::unregister`] must be
called upon completion of the background task to
prevent the task scheduler from running indefinitely.
*/
#[must_use = "Background tasks must be unregistered"]
#[derive(Debug)]
pub struct TaskSchedulerBackgroundTaskHandle {
unregistered: bool,
sender: mpsc::UnboundedSender<TaskSchedulerMessage>,
}
impl TaskSchedulerBackgroundTaskHandle {
pub fn new(sender: mpsc::UnboundedSender<TaskSchedulerMessage>) -> Self {
Self {
unregistered: false,
sender,
}
}
pub fn unregister(mut self, result: LuaResult<()>) {
self.unregistered = true;
self.sender
.send(TaskSchedulerMessage::Terminated(result))
.unwrap_or_else(|_| {
panic!(
"\
\nFailed to unregister background task - this is an internal error! \
\nPlease report it at {} \
\nDetails: Manual \
",
env!("CARGO_PKG_REPOSITORY")
)
});
}
}
/// A struct representing the current state of the task scheduler
#[derive(Debug, Clone)]
pub struct TaskSchedulerResult {
lua_error: Option<LuaError>,
exit_code: Option<ExitCode>,
num_instant: usize,
num_deferred: usize,
num_futures: usize,
num_background: usize,
num_active: usize,
}
impl TaskSchedulerResult {
fn new(sched: &TaskScheduler) -> Self {
const MESSAGE: &str =
"Failed to get lock - make sure not to call during task scheduler resumption";
let num_instant = sched.task_queue_instant.try_lock().expect(MESSAGE).len();
let num_deferred = sched.task_queue_deferred.try_lock().expect(MESSAGE).len();
let num_active = num_instant + num_deferred;
Self {
lua_error: None,
exit_code: if sched.exit_code_set.load(Ordering::Relaxed) {
Some(*sched.exit_code.try_lock().expect(MESSAGE))
} else {
None
},
num_instant,
num_deferred,
num_futures: sched.futures.try_lock().expect(MESSAGE).len(),
num_background: sched.futures_in_background.load(Ordering::Relaxed),
num_active,
}
}
fn err(sched: &TaskScheduler, err: LuaError) -> Self {
let mut this = Self::new(sched);
this.lua_error = Some(err);
this
}
/**
Returns a clone of the error from
this task scheduler result, if any.
*/
pub fn get_lua_error(&self) -> Option<LuaError> {
self.lua_error.clone()
}
/**
Returns a clone of the exit code from
this task scheduler result, if any.
*/
pub fn get_exit_code(&self) -> Option<ExitCode> {
self.exit_code
}
/**
Returns `true` if the task scheduler is still busy,
meaning it still has lua threads left to run.
*/
pub fn is_busy(&self) -> bool {
self.num_active > 0
}
/**
Returns `true` if the task scheduler has finished all
blocking lua tasks, but still has yielding tasks running.
*/
pub fn is_yielding(&self) -> bool {
self.num_active == 0 && self.num_futures > 0
}
/**
Returns `true` if the task scheduler has finished all
lua threads, but still has background tasks running.
*/
pub fn is_background(&self) -> bool {
self.num_active == 0 && self.num_futures == 0 && self.num_background > 0
}
/**
Returns `true` if the task scheduler is done,
meaning it has no lua threads left to run, and
no spawned tasks are running in the background.
*/
pub fn is_done(&self) -> bool {
self.num_active == 0 && self.num_futures == 0 && self.num_background == 0
}
}
impl fmt::Display for TaskSchedulerResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.is_busy() {
"Busy"
} else if self.is_yielding() {
"Yielding"
} else if self.is_background() {
"Background"
} else {
"Done"
};
let code = match self.get_exit_code() {
Some(code) => format!("{code:?}"),
None => "-".to_string(),
};
let err = match self.get_lua_error() {
Some(e) => format!("{e:?}")
.as_bytes()
.chunks(42) // Kinda arbitrary but should fit in most terminals
.enumerate()
.map(|(idx, buf)| {
format!(
"{}{}{}{}{}",
if idx == 0 { "" } else { "\n" },
if idx == 0 {
"".to_string()
} else {
" ".repeat(16)
},
if idx == 0 { "" } else { "" },
String::from_utf8_lossy(buf),
if buf.len() == 42 { "" } else { "" },
)
})
.collect::<String>(),
None => "-".to_string(),
};
let parts = vec![
format!("Status │ {status}"),
format!("Tasks active │ {}", self.num_active),
format!("Tasks background │ {}", self.num_background),
format!("Status code │ {code}"),
format!("Lua error │ {err}"),
];
let lengths = parts
.iter()
.map(|part| {
part.lines()
.next()
.unwrap()
.trim_end_matches("")
.chars()
.count()
})
.collect::<Vec<_>>();
let longest = &parts
.iter()
.enumerate()
.fold(0, |acc, (index, _)| acc.max(lengths[index]));
let sep = "".repeat(longest + 2);
writeln!(f, "┌{}┐", &sep)?;
for (index, part) in parts.iter().enumerate() {
writeln!(
f,
"│ {}{} │",
part.trim_end_matches(""),
" ".repeat(
longest
- part
.lines()
.last()
.unwrap()
.trim_end_matches("")
.chars()
.count()
)
)?;
if index < parts.len() - 1 {
writeln!(f, "┝{}┥", &sep)?;
}
}
writeln!(f, "└{}┘", &sep)?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub enum TaskSchedulerMessage {
NewBlockingTaskReady,
Spawned,
Terminated(LuaResult<()>),
}
/// A task scheduler that implements task queues /// A task scheduler that implements task queues
/// with instant, deferred, and delayed tasks /// with instant, deferred, and delayed tasks
#[derive(Debug)] #[derive(Debug)]
pub struct TaskScheduler<'fut> { pub struct TaskScheduler<'fut> {
/*
Lots of cell and refcell here, however we need full interior mutability and never outer
since the scheduler struct may be accessed from lua more than once at the same time.
An example of this is the implementation of coroutine.resume, which instantly resumes the given
task, where the task getting resumed may also create new scheduler tasks during its resumption.
The same goes for values used during resumption of futures (`futures` and `futures_rx`)
which must use async-aware mutexes to be cancellation safe across await points.
*/
// Internal state & flags
lua: &'static Lua, lua: &'static Lua,
tasks: Arc<Mutex<HashMap<TaskReference, Task>>>, guid: Cell<usize>,
futures: Arc<AsyncMutex<FuturesUnordered<TaskFuture<'fut>>>>, guid_running: Cell<Option<usize>>,
pub(super) exit_code: Cell<Option<ExitCode>>,
// Blocking tasks
pub(super) tasks: RefCell<HashMap<TaskReference, Task>>,
pub(super) tasks_queue_blocking: RefCell<VecDeque<TaskReference>>,
// Future tasks & objects for waking
pub(super) futures: AsyncMutex<FuturesUnordered<TaskFuture<'fut>>>,
pub(super) futures_registered_count: Cell<usize>,
futures_tx: mpsc::UnboundedSender<TaskSchedulerMessage>, futures_tx: mpsc::UnboundedSender<TaskSchedulerMessage>,
futures_rx: Arc<AsyncMutex<mpsc::UnboundedReceiver<TaskSchedulerMessage>>>, futures_rx: AsyncMutex<mpsc::UnboundedReceiver<TaskSchedulerMessage>>,
futures_in_background: AtomicUsize,
task_queue_instant: TaskSchedulerQueue,
task_queue_deferred: TaskSchedulerQueue,
exit_code_set: AtomicBool,
exit_code: Arc<Mutex<ExitCode>>,
guid: AtomicUsize,
guid_running_task: AtomicUsize,
} }
impl<'fut> TaskScheduler<'fut> { impl<'fut> TaskScheduler<'fut> {
@ -328,19 +74,15 @@ impl<'fut> TaskScheduler<'fut> {
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
Ok(Self { Ok(Self {
lua, lua,
tasks: Arc::new(Mutex::new(HashMap::new())), guid: Cell::new(0),
futures: Arc::new(AsyncMutex::new(FuturesUnordered::new())), guid_running: Cell::new(None),
exit_code: Cell::new(None),
tasks: RefCell::new(HashMap::new()),
tasks_queue_blocking: RefCell::new(VecDeque::new()),
futures: AsyncMutex::new(FuturesUnordered::new()),
futures_tx: tx, futures_tx: tx,
futures_rx: Arc::new(AsyncMutex::new(rx)), futures_rx: AsyncMutex::new(rx),
futures_in_background: AtomicUsize::new(0), futures_registered_count: Cell::new(0),
task_queue_instant: Arc::new(Mutex::new(VecDeque::new())),
task_queue_deferred: Arc::new(Mutex::new(VecDeque::new())),
exit_code_set: AtomicBool::new(false),
exit_code: Arc::new(Mutex::new(ExitCode::SUCCESS)),
// Global ids must start at 1, since 0 is a special
// value for guid_running_task that means "no task"
guid: AtomicUsize::new(1),
guid_running_task: AtomicUsize::new(0),
}) })
} }
@ -367,8 +109,7 @@ impl<'fut> TaskScheduler<'fut> {
should stop resuming tasks, and gracefully terminate the program. should stop resuming tasks, and gracefully terminate the program.
*/ */
pub fn set_exit_code(&self, code: ExitCode) { pub fn set_exit_code(&self, code: ExitCode) {
*self.exit_code.lock().unwrap() = code; self.exit_code.set(Some(code));
self.exit_code_set.store(true, Ordering::Relaxed);
} }
/** /**
@ -379,8 +120,7 @@ impl<'fut> TaskScheduler<'fut> {
*/ */
#[allow(dead_code)] #[allow(dead_code)]
pub fn contains_task(&self, reference: TaskReference) -> bool { pub fn contains_task(&self, reference: TaskReference) -> bool {
let tasks = self.tasks.lock().unwrap(); self.tasks.borrow().contains_key(&reference)
tasks.contains_key(&reference)
} }
/** /**
@ -411,6 +151,8 @@ impl<'fut> TaskScheduler<'fut> {
} }
}; };
// Store the thread and its arguments in the registry // Store the thread and its arguments in the registry
// NOTE: We must convert to a vec since multis
// can't be stored in the registry directly
let task_args_vec: Option<Vec<LuaValue>> = thread_args.map(|opt| opt.into_vec()); let task_args_vec: Option<Vec<LuaValue>> = thread_args.map(|opt| opt.into_vec());
let task_args_key: LuaRegistryKey = self.lua.create_registry_value(task_args_vec)?; let task_args_key: LuaRegistryKey = self.lua.create_registry_value(task_args_vec)?;
let task_thread_key: LuaRegistryKey = self.lua.create_registry_value(task_thread)?; let task_thread_key: LuaRegistryKey = self.lua.create_registry_value(task_thread)?;
@ -425,12 +167,13 @@ impl<'fut> TaskScheduler<'fut> {
let task_ref = if let Some(reusable_guid) = guid_to_reuse { let task_ref = if let Some(reusable_guid) = guid_to_reuse {
TaskReference::new(kind, reusable_guid) TaskReference::new(kind, reusable_guid)
} else { } else {
let guid = self.guid.fetch_add(1, Ordering::Relaxed); let guid = self.guid.get();
self.guid.set(guid + 1);
TaskReference::new(kind, guid) TaskReference::new(kind, guid)
}; };
// Add the task to the scheduler // Add the task to the scheduler
{ {
let mut tasks = self.tasks.lock().unwrap(); let mut tasks = self.tasks.borrow_mut();
tasks.insert(task_ref, task); tasks.insert(task_ref, task);
} }
Ok(task_ref) Ok(task_ref)
@ -456,13 +199,13 @@ impl<'fut> TaskScheduler<'fut> {
are no tasks left in the task list, the futures do not matter there are no tasks left in the task list, the futures do not matter there
*/ */
let mut found = false; let mut found = false;
let mut tasks = self.tasks.lock().unwrap(); let mut tasks = self.tasks.borrow_mut();
// Unfortunately we have to loop through to find which task // Unfortunately we have to loop through to find which task
// references to remove instead of removing directly since // references to remove instead of removing directly since
// tasks can switch kinds between instant, deferred, future // tasks can switch kinds between instant, deferred, future
let tasks_to_remove: Vec<_> = tasks let tasks_to_remove: Vec<_> = tasks
.keys() .keys()
.filter(|task_ref| task_ref.guid == reference.guid) .filter(|task_ref| task_ref.id() == reference.id())
.copied() .copied()
.collect(); .collect();
for task_ref in tasks_to_remove { for task_ref in tasks_to_remove {
@ -486,27 +229,36 @@ impl<'fut> TaskScheduler<'fut> {
pub fn resume_task<'a>( pub fn resume_task<'a>(
&self, &self,
reference: TaskReference, reference: TaskReference,
override_args: Option<Vec<LuaValue<'a>>>, override_args: Option<LuaResult<LuaMultiValue<'a>>>,
) -> LuaResult<LuaMultiValue<'a>> { ) -> LuaResult<LuaMultiValue<'a>> {
self.guid_running_task
.store(reference.guid, Ordering::Relaxed);
let task = { let task = {
let mut tasks = self.tasks.lock().unwrap(); let mut tasks = self.tasks.borrow_mut();
match tasks.remove(&reference) { match tasks.remove(&reference) {
Some(task) => task, Some(task) => task,
None => return Ok(LuaMultiValue::new()), // Task was removed None => return Ok(LuaMultiValue::new()), // Task was removed
} }
}; };
let thread: LuaThread = self.lua.registry_value(&task.thread)?; let thread: LuaThread = self.lua.registry_value(&task.thread)?;
let args_vec_opt = override_args.or_else(|| { let args_opt_res = override_args.or_else(|| {
self.lua Ok(self
.lua
.registry_value::<Option<Vec<LuaValue>>>(&task.args) .registry_value::<Option<Vec<LuaValue>>>(&task.args)
.expect("Failed to get stored args for task") .expect("Failed to get stored args for task")
.map(LuaMultiValue::from_vec))
.transpose()
}); });
self.lua.remove_registry_value(task.thread)?; self.lua.remove_registry_value(task.thread)?;
self.lua.remove_registry_value(task.args)?; self.lua.remove_registry_value(task.args)?;
let rets = if let Some(args) = args_vec_opt { if let Some(args_res) = args_opt_res {
thread.resume::<_, LuaMultiValue>(LuaMultiValue::from_vec(args)) match args_res {
Err(e) => Err(e), // FIXME: We need to throw this error in lua to let pcall & friends handle it properly
Ok(args) => {
self.guid_running.set(Some(reference.id()));
let rets = thread.resume::<_, LuaMultiValue>(args);
self.guid_running.set(None);
rets
}
}
} else { } else {
/* /*
The tasks did not get any arguments from either: The tasks did not get any arguments from either:
@ -519,10 +271,11 @@ impl<'fut> TaskScheduler<'fut> {
want the amount of time waited returned to them. want the amount of time waited returned to them.
*/ */
let elapsed = task.queued_at.elapsed().as_secs_f64(); let elapsed = task.queued_at.elapsed().as_secs_f64();
thread.resume::<_, LuaMultiValue>(elapsed) self.guid_running.set(Some(reference.id()));
}; let rets = thread.resume::<_, LuaMultiValue>(elapsed);
self.guid_running_task.store(0, Ordering::Relaxed); self.guid_running.set(None);
rets rets
}
} }
/** /**
@ -541,48 +294,26 @@ impl<'fut> TaskScheduler<'fut> {
-- Here we have either yielded or finished the above task -- Here we have either yielded or finished the above task
``` ```
*/ */
fn queue_blocking_task( pub(super) fn queue_blocking_task(
&self, &self,
kind: TaskKind, kind: TaskKind,
thread_or_function: LuaValue<'_>, thread_or_function: LuaValue<'_>,
thread_args: Option<LuaMultiValue<'_>>, thread_args: Option<LuaMultiValue<'_>>,
guid_to_reuse: Option<usize>, guid_to_reuse: Option<usize>,
after_current_resume: bool,
) -> LuaResult<TaskReference> { ) -> LuaResult<TaskReference> {
if kind == TaskKind::Future { if kind == TaskKind::Future {
panic!("Tried to schedule future using normal task schedule method") panic!("Tried to schedule future using normal task schedule method")
} }
let task_ref = self.create_task(kind, thread_or_function, thread_args, guid_to_reuse)?; let task_ref = self.create_task(kind, thread_or_function, thread_args, guid_to_reuse)?;
// Note that we create two new inner new // Add the task to the front of the queue, unless it
// scopes to drop mutexes as fast as possible // should be deferred, in that case add it to the back
let num_prev_blocking_tasks = { let mut queue = self.tasks_queue_blocking.borrow_mut();
let (should_defer, num_prev_tasks, mut queue) = { let num_prev_blocking_tasks = queue.len();
let queue_instant = self.task_queue_instant.lock().unwrap(); if kind == TaskKind::Deferred {
let queue_deferred = self.task_queue_deferred.lock().unwrap(); queue.push_back(task_ref);
let num_prev_tasks = queue_instant.len() + queue_deferred.len(); } else {
( queue.push_front(task_ref);
kind == TaskKind::Deferred, }
num_prev_tasks,
match kind {
TaskKind::Instant => queue_instant,
TaskKind::Deferred => queue_deferred,
TaskKind::Future => unreachable!(),
},
)
};
if should_defer {
queue.push_back(task_ref);
} else if after_current_resume {
assert!(
queue.len() > 0,
"Cannot schedule a task after the first instant when task queue is empty"
);
queue.insert(1, task_ref);
} else {
queue.push_front(task_ref);
}
num_prev_tasks
};
/* /*
If we had any previous task and are currently async If we had any previous task and are currently async
waiting on tasks, we should send a signal to wake up waiting on tasks, we should send a signal to wake up
@ -603,12 +334,12 @@ impl<'fut> TaskScheduler<'fut> {
/** /**
Queues a new future to run on the task scheduler. Queues a new future to run on the task scheduler.
*/ */
fn queue_async_task( pub(super) fn queue_async_task(
&self, &self,
thread_or_function: LuaValue<'_>, thread_or_function: LuaValue<'_>,
thread_args: Option<LuaMultiValue<'_>>, thread_args: Option<LuaMultiValue<'_>>,
guid_to_reuse: Option<usize>, guid_to_reuse: Option<usize>,
fut: impl Future<Output = TaskFutureReturns<'fut>> + 'fut, fut: impl Future<Output = TaskFutureRets<'fut>> + 'fut,
) -> LuaResult<TaskReference> { ) -> LuaResult<TaskReference> {
let task_ref = self.create_task( let task_ref = self.create_task(
TaskKind::Future, TaskKind::Future,
@ -634,7 +365,7 @@ impl<'fut> TaskScheduler<'fut> {
The given lua thread or function will be resumed The given lua thread or function will be resumed
using the given `thread_args` as its argument(s). using the given `thread_args` as its argument(s).
*/ */
pub fn schedule_current_resume( pub fn schedule_next(
&self, &self,
thread_or_function: LuaValue<'_>, thread_or_function: LuaValue<'_>,
thread_args: LuaMultiValue<'_>, thread_args: LuaMultiValue<'_>,
@ -644,34 +375,6 @@ impl<'fut> TaskScheduler<'fut> {
thread_or_function, thread_or_function,
Some(thread_args), Some(thread_args),
None, None,
false,
)
}
/**
Schedules a lua thread or function to resume ***after the first***
currently resuming task, during this resumption point.
The given lua thread or function will be resumed
using the given `thread_args` as its argument(s).
*/
pub fn schedule_after_current_resume(
&self,
thread_or_function: LuaValue<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference> {
self.queue_blocking_task(
TaskKind::Instant,
thread_or_function,
Some(thread_args),
// This should recycle the guid of the current task,
// since it will only be called to schedule resuming
// current thread after it gives resumption to another
match self.guid_running_task.load(Ordering::Relaxed) {
0 => panic!("Tried to schedule with no task running"),
guid => Some(guid),
},
true,
) )
} }
@ -692,7 +395,6 @@ impl<'fut> TaskScheduler<'fut> {
thread_or_function, thread_or_function,
Some(thread_args), Some(thread_args),
None, None,
false,
) )
} }
@ -733,15 +435,7 @@ impl<'fut> TaskScheduler<'fut> {
// Wait should recycle the guid of the current task, // Wait should recycle the guid of the current task,
// which ensures that the TaskReference is identical and // which ensures that the TaskReference is identical and
// that any waits inside of spawned tasks will also cancel // that any waits inside of spawned tasks will also cancel
match self.guid_running_task.load(Ordering::Relaxed) { self.guid_running.get(),
0 => {
// NOTE: We had this here to verify the behavior of our task scheduler during development,
// but for re-implementing coroutine.resume (which is not registered) we must not panic here
// panic!("Tried to schedule waiting task with no registered task running")
None
}
guid => Some(guid),
},
async move { async move {
sleep(Duration::from_secs_f64(after_secs)).await; sleep(Duration::from_secs_f64(after_secs)).await;
Ok(None) Ok(None)
@ -756,12 +450,27 @@ impl<'fut> TaskScheduler<'fut> {
The given lua thread or function will be resumed The given lua thread or function will be resumed
using the optional arguments returned by the future. using the optional arguments returned by the future.
*/ */
pub fn schedule_async( #[allow(dead_code)]
&self, pub fn schedule_async<'sched, R, F, FR>(
&'sched self,
thread_or_function: LuaValue<'_>, thread_or_function: LuaValue<'_>,
fut: impl Future<Output = TaskFutureReturns<'fut>> + 'fut, func: F,
) -> LuaResult<TaskReference> { ) -> LuaResult<TaskReference>
self.queue_async_task(thread_or_function, None, None, fut) where
'sched: 'fut, // Scheduler must live at least as long as the future
R: ToLuaMulti<'static>,
F: 'static + Fn(&'static Lua) -> FR,
FR: 'static + Future<Output = LuaResult<R>>,
{
self.queue_async_task(thread_or_function, None, None, async move {
match func(self.lua).await {
Ok(res) => match res.to_lua_multi(self.lua) {
Ok(multi) => Ok(Some(multi)),
Err(e) => Err(e),
},
Err(e) => Err(e),
}
})
} }
/** /**
@ -784,14 +493,14 @@ impl<'fut> TaskScheduler<'fut> {
.with_value("thread", async_env_thread)? .with_value("thread", async_env_thread)?
.with_value("yield", async_env_yield)? .with_value("yield", async_env_yield)?
.with_function( .with_function(
"resume_async", "resumeAsync",
move |lua: &Lua, (thread, args): (LuaThread, A)| { move |lua: &Lua, (thread, args): (LuaThread, A)| {
let fut = func(lua, args); let fut = func(lua, args);
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_async(LuaValue::Thread(thread), async { sched.queue_async_task(LuaValue::Thread(thread), None, None, async {
let rets = fut.await?; let rets = fut.await?;
let mult = rets.to_lua_multi(lua)?; let mult = rets.to_lua_multi(lua)?;
Ok(Some(mult.into_vec())) Ok(Some(mult))
}) })
}, },
)? )?
@ -810,7 +519,7 @@ impl<'fut> TaskScheduler<'fut> {
must be called upon completion of the background task to must be called upon completion of the background task to
prevent the task scheduler from running indefinitely. prevent the task scheduler from running indefinitely.
*/ */
pub fn register_background_task(&self) -> TaskSchedulerBackgroundTaskHandle { pub fn register_background_task(&self) -> TaskSchedulerAsyncHandle {
let sender = self.futures_tx.clone(); let sender = self.futures_tx.clone();
sender sender
.send(TaskSchedulerMessage::Spawned) .send(TaskSchedulerMessage::Spawned)
@ -824,110 +533,82 @@ impl<'fut> TaskScheduler<'fut> {
env!("CARGO_PKG_REPOSITORY") env!("CARGO_PKG_REPOSITORY")
) )
}); });
TaskSchedulerBackgroundTaskHandle::new(sender) TaskSchedulerAsyncHandle::new(sender)
}
/**
Retrieves the queue for a specific kind of task.
Panics for [`TaskKind::Future`] since
futures do not use the normal task queue.
*/
fn get_queue(&self, kind: TaskKind) -> &TaskSchedulerQueue {
match kind {
TaskKind::Instant => &self.task_queue_instant,
TaskKind::Deferred => &self.task_queue_deferred,
TaskKind::Future => {
panic!("Future tasks do not use the normal task queue")
}
}
} }
/** /**
Resumes the next queued Lua task, if one exists, blocking Resumes the next queued Lua task, if one exists, blocking
the current thread until it either yields or finishes. the current thread until it either yields or finishes.
*/ */
fn resume_next_queue_task( fn resume_next_blocking_task(
&self, &self,
kind: TaskKind, override_args: Option<LuaResult<LuaMultiValue>>,
override_args: Option<Vec<LuaValue>>, ) -> TaskSchedulerState {
) -> TaskSchedulerResult {
match { match {
let mut queue_guard = self.get_queue(kind).lock().unwrap(); let mut queue_guard = self.tasks_queue_blocking.borrow_mut();
queue_guard.pop_front() let task = queue_guard.pop_front();
drop(queue_guard);
task
} { } {
None => TaskSchedulerResult::new(self), None => TaskSchedulerState::new(self),
Some(task) => match self.resume_task(task, override_args) { Some(task) => match self.resume_task(task, override_args) {
Ok(_) => TaskSchedulerResult::new(self), Ok(_) => TaskSchedulerState::new(self),
Err(task_err) => TaskSchedulerResult::err(self, task_err), Err(task_err) => TaskSchedulerState::err(self, task_err),
}, },
} }
} }
/** /**
Awaits the first available queued future, and resumes its Awaits the first available queued future, and resumes its associated
associated Lua task which will then be ready for resumption. Lua task which will be ready for resumption when that future wakes.
Panics if there are no futures currently queued. Panics if there are no futures currently queued.
Use [`TaskScheduler::next_queue_future_exists`] Use [`TaskScheduler::next_queue_future_exists`]
to check if there are any queued futures. to check if there are any queued futures.
*/ */
async fn resume_next_queue_future(&self) -> TaskSchedulerResult { async fn resume_next_async_task(&self) -> TaskSchedulerState {
let result = { let (task, result) = {
let mut futs = self let mut futs = self
.futures .futures
.try_lock() .try_lock()
.expect("Failed to get lock on futures"); .expect("Tried to resume next queued future while already resuming or modifying");
futs.next() futs.next()
.await .await
.expect("Tried to resume next queued future but none are queued") .expect("Tried to resume next queued future but none are queued")
}; };
match result { // Promote this future task to a blocking task and resume it
(task, Err(fut_err)) => { // right away, also taking care to not borrow mutably twice
// Future errored, don't resume its associated task // by dropping this guard before trying to resume it
// and make sure to cancel / remove it completely, if removal let mut queue_guard = self.tasks_queue_blocking.borrow_mut();
// also errors then we send that error back instead of the future's error queue_guard.push_front(task);
TaskSchedulerResult::err( drop(queue_guard);
self, self.resume_next_blocking_task(result.transpose())
match self.remove_task(task) {
Err(cancel_err) => cancel_err,
Ok(_) => fut_err,
},
)
}
(task, Ok(args)) => {
// Promote this future task to an instant task
// and resume the instant queue right away, taking
// care to not deadlock by dropping the mutex guard
let mut queue_guard = self.get_queue(TaskKind::Instant).lock().unwrap();
queue_guard.push_front(task);
drop(queue_guard);
self.resume_next_queue_task(TaskKind::Instant, args)
}
}
} }
/** /**
Awaits the next background task registration Awaits the next background task registration
message, if any messages exist in the queue. message, if any messages exist in the queue.
This is a no-op if there are no messages. This is a no-op if there are no background tasks left running
and / or the background task messages channel was closed.
*/ */
async fn receive_next_message(&self) -> TaskSchedulerResult { async fn receive_next_message(&self) -> TaskSchedulerState {
let message_opt = { let message_opt = {
let mut rx = self.futures_rx.lock().await; let mut rx = self.futures_rx.lock().await;
rx.recv().await rx.recv().await
}; };
if let Some(message) = message_opt { if let Some(message) = message_opt {
match message { match message {
TaskSchedulerMessage::NewBlockingTaskReady => TaskSchedulerResult::new(self), TaskSchedulerMessage::NewBlockingTaskReady => TaskSchedulerState::new(self),
TaskSchedulerMessage::Spawned => { TaskSchedulerMessage::Spawned => {
self.futures_in_background.fetch_add(1, Ordering::Relaxed); let prev = self.futures_registered_count.get();
TaskSchedulerResult::new(self) self.futures_registered_count.set(prev + 1);
TaskSchedulerState::new(self)
} }
TaskSchedulerMessage::Terminated(result) => { TaskSchedulerMessage::Terminated(result) => {
let prev = self.futures_in_background.fetch_sub(1, Ordering::Relaxed); let prev = self.futures_registered_count.get();
self.futures_registered_count.set(prev - 1);
if prev == 0 { if prev == 0 {
panic!( panic!(
r#" r#"
@ -938,14 +619,14 @@ impl<'fut> TaskScheduler<'fut> {
) )
} }
if let Err(e) = result { if let Err(e) = result {
TaskSchedulerResult::err(self, e) TaskSchedulerState::err(self, e)
} else { } else {
TaskSchedulerResult::new(self) TaskSchedulerState::new(self)
} }
} }
} }
} else { } else {
TaskSchedulerResult::new(self) TaskSchedulerState::new(self)
} }
} }
@ -958,37 +639,34 @@ impl<'fut> TaskScheduler<'fut> {
this will process delayed tasks, waiting tasks, and native Rust this will process delayed tasks, waiting tasks, and native Rust
futures concurrently, awaiting the first one to be ready for resumption. futures concurrently, awaiting the first one to be ready for resumption.
*/ */
pub async fn resume_queue(&self) -> TaskSchedulerResult { pub async fn resume_queue(&self) -> TaskSchedulerState {
let current = TaskSchedulerResult::new(self); let current = TaskSchedulerState::new(self);
/* /*
Resume tasks in the internal queue, in this order: Resume tasks in the internal queue, in this order:
* 🛑 = blocking - lua tasks, in order * 🛑 = blocking - lua tasks, in order
* = async - first come, first serve * = async - first come, first serve
1. 🛑 Tasks from task.spawn and the main thread 1. 🛑 Tasks from task.spawn / task.defer, the main thread
2. 🛑 Tasks from task.defer 2. Tasks from task.delay / task.wait, spawned background tasks
3. Tasks from task.delay / task.wait, spawned background tasks
*/ */
if current.num_instant > 0 { if current.has_blocking_tasks() {
self.resume_next_queue_task(TaskKind::Instant, None) self.resume_next_blocking_task(None)
} else if current.num_deferred > 0 { } else if current.has_future_tasks() && current.has_background_tasks() {
self.resume_next_queue_task(TaskKind::Deferred, None)
} else if current.num_futures > 0 && current.num_background > 0 {
// Futures, spawned background tasks // Futures, spawned background tasks
tokio::select! { tokio::select! {
result = self.resume_next_queue_future() => result, result = self.resume_next_async_task() => result,
result = self.receive_next_message() => result, result = self.receive_next_message() => result,
} }
} else if current.num_futures > 0 { } else if current.has_future_tasks() {
// Futures // Futures
self.resume_next_queue_future().await self.resume_next_async_task().await
} else if current.num_background > 0 { } else if current.has_background_tasks() {
// Only spawned background tasks, these may then // Only spawned background tasks, these may then
// spawn new lua tasks and "wake up" the scheduler // spawn new lua tasks and "wake up" the scheduler
self.receive_next_message().await self.receive_next_message().await
} else { } else {
TaskSchedulerResult::new(self) TaskSchedulerState::new(self)
} }
} }
} }

View file

@ -0,0 +1,39 @@
use std::fmt;
/// An enum representing different kinds of tasks
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TaskKind {
Instant,
Deferred,
Future,
}
#[allow(dead_code)]
impl TaskKind {
pub fn is_instant(&self) -> bool {
*self == Self::Instant
}
pub fn is_deferred(&self) -> bool {
*self == Self::Deferred
}
pub fn is_blocking(&self) -> bool {
*self != Self::Future
}
pub fn is_future(&self) -> bool {
*self == Self::Future
}
}
impl fmt::Display for TaskKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name: &'static str = match self {
TaskKind::Instant => "Instant",
TaskKind::Deferred => "Deferred",
TaskKind::Future => "Future",
};
write!(f, "{name}")
}
}

View file

@ -0,0 +1,31 @@
use std::fmt;
use mlua::prelude::*;
use super::task_kind::TaskKind;
/// A lightweight, copyable struct that represents a
/// task in the scheduler and is accessible from Lua
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TaskReference {
kind: TaskKind,
guid: usize,
}
impl TaskReference {
pub const fn new(kind: TaskKind, guid: usize) -> Self {
Self { kind, guid }
}
pub const fn id(&self) -> usize {
self.guid
}
}
impl fmt::Display for TaskReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TaskReference({} - {})", self.kind, self.guid)
}
}
impl LuaUserData for TaskReference {}

View file

@ -1,4 +1,4 @@
local PORT = 8080 local PORT = 8081
local URL = `http://127.0.0.1:{PORT}` local URL = `http://127.0.0.1:{PORT}`
local WS_URL = `ws://127.0.0.1:{PORT}` local WS_URL = `ws://127.0.0.1:{PORT}`
local REQUEST = "Hello from client!" local REQUEST = "Hello from client!"