mlua_luau_scheduler/scheduler.rs
1#![allow(clippy::module_name_repetitions)]
2
3use std::{
4 cell::Cell,
5 rc::Rc,
6 sync::{Arc, Weak as WeakArc},
7 thread::panicking,
8};
9
10use futures_lite::prelude::*;
11use mlua::prelude::*;
12
13use async_executor::{Executor, LocalExecutor};
14use tracing::{Instrument, debug, instrument, trace, trace_span};
15
16use crate::{
17 error_callback::ThreadErrorCallback,
18 exit::Exit,
19 queue::{DeferredThreadQueue, FuturesQueue, SpawnedThreadQueue},
20 status::Status,
21 threads::{ThreadId, ThreadMap},
22 traits::IntoLuaThread,
23 util::run_until_yield,
24};
25
26const ERR_METADATA_ALREADY_ATTACHED: &str = "\
27Lua state already has scheduler metadata attached!\
28\nThis may be caused by running multiple schedulers on the same Lua state, or a call to Scheduler::run being cancelled.\
29\nOnly one scheduler can be used per Lua state at once, and schedulers must always run until completion.\
30";
31
32const ERR_METADATA_REMOVED: &str = "\
33Lua state scheduler metadata was unexpectedly removed!\
34\nThis should never happen, and is likely a bug in the scheduler.\
35";
36
37const ERR_SET_CALLBACK_WHEN_RUNNING: &str = "\
38Cannot set error callback when scheduler is running!\
39";
40
41/**
42 A scheduler for running Lua threads and async tasks.
43*/
44#[derive(Clone)]
45pub struct Scheduler {
46 lua: Lua,
47 queue_spawn: SpawnedThreadQueue,
48 queue_defer: DeferredThreadQueue,
49 error_callback: ThreadErrorCallback,
50 thread_map: ThreadMap,
51 status: Rc<Cell<Status>>,
52 exit: Exit,
53}
54
55impl Scheduler {
56 /**
57 Creates a new scheduler for the given Lua state.
58
59 This scheduler will have a default error callback that prints errors to stderr.
60
61 # Panics
62
63 Panics if the given Lua state already has a scheduler attached to it.
64 */
65 #[must_use]
66 pub fn new(lua: Lua) -> Scheduler {
67 let queue_spawn = SpawnedThreadQueue::new();
68 let queue_defer = DeferredThreadQueue::new();
69 let error_callback = ThreadErrorCallback::default();
70 let result_map = ThreadMap::new();
71 let exit = Exit::new();
72
73 assert!(
74 lua.app_data_ref::<SpawnedThreadQueue>().is_none(),
75 "{ERR_METADATA_ALREADY_ATTACHED}"
76 );
77 assert!(
78 lua.app_data_ref::<DeferredThreadQueue>().is_none(),
79 "{ERR_METADATA_ALREADY_ATTACHED}"
80 );
81 assert!(
82 lua.app_data_ref::<ThreadErrorCallback>().is_none(),
83 "{ERR_METADATA_ALREADY_ATTACHED}"
84 );
85 assert!(
86 lua.app_data_ref::<ThreadMap>().is_none(),
87 "{ERR_METADATA_ALREADY_ATTACHED}"
88 );
89 assert!(
90 lua.app_data_ref::<Exit>().is_none(),
91 "{ERR_METADATA_ALREADY_ATTACHED}"
92 );
93
94 lua.set_app_data(queue_spawn.clone());
95 lua.set_app_data(queue_defer.clone());
96 lua.set_app_data(error_callback.clone());
97 lua.set_app_data(result_map.clone());
98 lua.set_app_data(exit.clone());
99
100 let status = Rc::new(Cell::new(Status::NotStarted));
101
102 Scheduler {
103 lua,
104 queue_spawn,
105 queue_defer,
106 error_callback,
107 thread_map: result_map,
108 status,
109 exit,
110 }
111 }
112
113 /**
114 Sets the current status of this scheduler and emits relevant tracing events.
115 */
116 fn set_status(&self, status: Status) {
117 debug!(status = ?status, "status");
118 self.status.set(status);
119 }
120
121 /**
122 Returns the current status of this scheduler.
123 */
124 #[must_use]
125 pub fn status(&self) -> Status {
126 self.status.get()
127 }
128
129 /**
130 Sets the error callback for this scheduler.
131
132 This callback will be called whenever a Lua thread errors.
133
134 Overwrites any previous error callback.
135
136 # Panics
137
138 Panics if the scheduler is currently running.
139 */
140 pub fn set_error_callback(&self, callback: impl Fn(LuaError) + Send + 'static) {
141 assert!(
142 !self.status().is_running(),
143 "{ERR_SET_CALLBACK_WHEN_RUNNING}"
144 );
145 self.error_callback.replace(callback);
146 }
147
148 /**
149 Clears the error callback for this scheduler.
150
151 This will remove any current error callback, including default(s).
152
153 # Panics
154
155 Panics if the scheduler is currently running.
156 */
157 pub fn remove_error_callback(&self) {
158 assert!(
159 !self.status().is_running(),
160 "{ERR_SET_CALLBACK_WHEN_RUNNING}"
161 );
162 self.error_callback.clear();
163 }
164
165 /**
166 Gets the exit code for this scheduler, if one has been set.
167 */
168 #[must_use]
169 pub fn get_exit_code(&self) -> Option<u8> {
170 self.exit.get()
171 }
172
173 /**
174 Sets the exit code for this scheduler.
175
176 This will cause [`Scheduler::run`] to exit immediately.
177 */
178 pub fn set_exit_code(&self, code: u8) {
179 self.exit.set(code);
180 }
181
182 /**
183 Spawns a chunk / function / thread onto the scheduler queue.
184
185 Threads are guaranteed to be resumed in the order that they were pushed to the queue.
186
187 # Returns
188
189 Returns a [`ThreadId`] that can be used to retrieve the result of the thread.
190
191 Note that the result may not be available until [`Scheduler::run`] completes.
192
193 # Errors
194
195 Errors when out of memory.
196 */
197 pub fn push_thread_front(
198 &self,
199 thread: impl IntoLuaThread,
200 args: impl IntoLuaMulti,
201 ) -> LuaResult<ThreadId> {
202 let id = self.queue_spawn.push_item(&self.lua, thread, args)?;
203 self.thread_map.track(id);
204 Ok(id)
205 }
206
207 /**
208 Defers a chunk / function / thread onto the scheduler queue.
209
210 Deferred threads are guaranteed to run after all spawned threads either yield or complete.
211
212 Threads are guaranteed to be resumed in the order that they were pushed to the queue.
213
214 # Returns
215
216 Returns a [`ThreadId`] that can be used to retrieve the result of the thread.
217
218 Note that the result may not be available until [`Scheduler::run`] completes.
219
220 # Errors
221
222 Errors when out of memory.
223 */
224 pub fn push_thread_back(
225 &self,
226 thread: impl IntoLuaThread,
227 args: impl IntoLuaMulti,
228 ) -> LuaResult<ThreadId> {
229 let id = self.queue_defer.push_item(&self.lua, thread, args)?;
230 self.thread_map.track(id);
231 Ok(id)
232 }
233
234 /**
235 Gets the tracked result for the [`LuaThread`] with the given [`ThreadId`].
236
237 Depending on the current [`Scheduler::status`], this method will return:
238
239 - [`Status::NotStarted`]: returns `None`.
240 - [`Status::Running`]: may return `Some(Ok(v))` or `Some(Err(e))`, but it is not guaranteed.
241 - [`Status::Completed`]: returns `Some(Ok(v))` or `Some(Err(e))`.
242
243 Note that this method also takes the value out of the scheduler and
244 stops tracking the given thread, so it may only be called once.
245
246 Any subsequent calls after this method returns `Some` will return `None`.
247 */
248 #[must_use]
249 pub fn get_thread_result(&self, id: ThreadId) -> Option<LuaResult<LuaMultiValue>> {
250 self.thread_map.remove(id)
251 }
252
253 /**
254 Waits for the [`LuaThread`] with the given [`ThreadId`] to complete.
255
256 This will return instantly if the thread has already completed.
257 */
258 pub async fn wait_for_thread(&self, id: ThreadId) {
259 self.thread_map.listen(id).await;
260 }
261
262 /**
263 Runs the scheduler until all Lua threads have completed.
264
265 This will return instantly if no threads have been scheduled.
266
267 Note that the given Lua state must be the same one that was
268 used to create this scheduler, otherwise this method will panic.
269
270 # Panics
271
272 Panics if the given Lua state already has a scheduler attached to it.
273 */
274 #[allow(clippy::too_many_lines)]
275 #[instrument(level = "debug", name = "Scheduler::run", skip(self))]
276 pub async fn run(&self) {
277 if self.queue_spawn.is_empty() && self.queue_defer.is_empty() {
278 return;
279 }
280
281 /*
282 Create new executors to use - note that we do not need to create multiple executors
283 for work stealing, the user may do that themselves if they want to and it will work
284 just fine, as long as anything async is .await-ed from within a Lua async function.
285
286 The main purpose of the two executors here is just to have one with
287 the Send bound, and another (local) one without it, for Lua scheduling.
288
289 We also use the main executor to drive the main loop below forward,
290 saving a tiny bit of processing from going on the Lua executor itself.
291 */
292 let local_exec = LocalExecutor::new();
293 let main_exec = Arc::new(Executor::new());
294 let fut_queue = FuturesQueue::new();
295
296 /*
297 Store the main executor and queue in Lua, so that they may be used with LuaSchedulerExt.
298
299 Also ensure we do not already have an executor or queues - these are definite user errors
300 and may happen if the user tries to run multiple schedulers on the same Lua state at once.
301 */
302 assert!(
303 self.lua.app_data_ref::<WeakArc<Executor>>().is_none(),
304 "{ERR_METADATA_ALREADY_ATTACHED}"
305 );
306 assert!(
307 self.lua.app_data_ref::<FuturesQueue>().is_none(),
308 "{ERR_METADATA_ALREADY_ATTACHED}"
309 );
310
311 self.lua.set_app_data(Arc::downgrade(&main_exec));
312 self.lua.set_app_data(fut_queue.clone());
313
314 /*
315 Manually tick the Lua executor, while running under the main executor.
316 Each tick we wait for the next action to perform, in prioritized order:
317
318 1. The exit event is triggered by setting an exit code
319 2. A Lua thread is available to run on the spawned queue
320 3. A Lua thread is available to run on the deferred queue
321 4. A new thread-local future is available to run on the local executor
322 5. Task(s) scheduled on the Lua executor have made progress and should be polled again
323
324 This ordering is vital to ensure that we don't accidentally exit the main loop
325 when there are new Lua threads to enqueue and potentially more work to be done.
326 */
327 let fut = async {
328 let result_map = self.thread_map.clone();
329 let process_thread = |thread: LuaThread, args| {
330 // NOTE: Thread may have been cancelled from Lua
331 // before we got here, so we need to check it again
332 if thread.status() == LuaThreadStatus::Resumable {
333 // Check if we should be tracking this thread
334 let id = ThreadId::from(&thread);
335 let id_tracked = result_map.is_tracked(id);
336 let result_map_inner = if id_tracked {
337 Some(result_map.clone())
338 } else {
339 None
340 };
341 // Create our future which will run the thread and store its final result
342 let fut = async move {
343 if id_tracked {
344 // Run until yield and check if we got a final result
345 if let Some(res) = run_until_yield(thread.clone(), args).await {
346 if let Err(e) = res.as_ref() {
347 self.error_callback.call(e);
348 }
349 if thread.status() != LuaThreadStatus::Resumable {
350 result_map_inner.unwrap().insert(id, res);
351 }
352 }
353 } else {
354 // Just run until yield
355 if let Some(res) = run_until_yield(thread, args).await
356 && let Err(e) = res.as_ref()
357 {
358 self.error_callback.call(e);
359 }
360 }
361 };
362 // Spawn it on the executor
363 local_exec.spawn(fut).detach();
364 }
365 };
366
367 loop {
368 let fut_exit = self.exit.listen(); // 1
369 let fut_spawn = self.queue_spawn.wait_for_item(); // 2
370 let fut_defer = self.queue_defer.wait_for_item(); // 3
371 let fut_futs = fut_queue.wait_for_item(); // 4
372
373 // 5
374 let mut num_processed = 0;
375 let span_tick = trace_span!("Scheduler::tick");
376 let fut_tick = async {
377 local_exec.tick().await;
378 // NOTE: Try to do as much work as possible instead of just a single tick()
379 num_processed += 1;
380 while local_exec.try_tick() {
381 num_processed += 1;
382 }
383 };
384
385 // 1 + 2 + 3 + 4 + 5
386 fut_exit
387 .or(fut_spawn)
388 .or(fut_defer)
389 .or(fut_futs)
390 .or(fut_tick.instrument(span_tick.or_current()))
391 .await;
392
393 // Check if we should exit
394 if self.exit.get().is_some() {
395 debug!("exit signal received");
396 break;
397 }
398
399 // Process spawned threads first, then deferred threads, then futures
400 let mut num_spawned = 0;
401 let mut num_deferred = 0;
402 let mut num_futures = 0;
403 {
404 let _span = trace_span!("Scheduler::drain_spawned").entered();
405 for (thread, args) in self.queue_spawn.take_items() {
406 process_thread(thread, args);
407 num_spawned += 1;
408 }
409 }
410 {
411 let _span = trace_span!("Scheduler::drain_deferred").entered();
412 for (thread, args) in self.queue_defer.take_items() {
413 process_thread(thread, args);
414 num_deferred += 1;
415 }
416 }
417 {
418 let _span = trace_span!("Scheduler::drain_futures").entered();
419 for fut in fut_queue.take_items() {
420 local_exec.spawn(fut).detach();
421 num_futures += 1;
422 }
423 }
424
425 // Empty executor = we didn't spawn any new Lua tasks
426 // above, and there are no remaining tasks to run later
427 let completed = local_exec.is_empty()
428 && self.queue_spawn.is_empty()
429 && self.queue_defer.is_empty();
430 trace!(
431 futures_spawned = num_futures,
432 futures_processed = num_processed,
433 lua_threads_spawned = num_spawned,
434 lua_threads_deferred = num_deferred,
435 "loop"
436 );
437 if completed {
438 break;
439 }
440 }
441 };
442
443 // Run the executor inside a span until all lua threads complete
444 self.set_status(Status::Running);
445 main_exec.run(fut).await;
446 self.set_status(Status::Completed);
447
448 // Clean up
449 self.lua
450 .remove_app_data::<WeakArc<Executor>>()
451 .expect(ERR_METADATA_REMOVED);
452 self.lua
453 .remove_app_data::<FuturesQueue>()
454 .expect(ERR_METADATA_REMOVED);
455 }
456}
457
458impl Drop for Scheduler {
459 fn drop(&mut self) {
460 if panicking() {
461 // Do not cause further panics if already panicking, as
462 // this may abort the program instead of safely unwinding
463 self.lua.remove_app_data::<SpawnedThreadQueue>();
464 self.lua.remove_app_data::<DeferredThreadQueue>();
465 self.lua.remove_app_data::<ThreadErrorCallback>();
466 self.lua.remove_app_data::<ThreadMap>();
467 self.lua.remove_app_data::<Exit>();
468 } else {
469 // In any other case we panic if metadata was removed incorrectly
470 self.lua
471 .remove_app_data::<SpawnedThreadQueue>()
472 .expect(ERR_METADATA_REMOVED);
473 self.lua
474 .remove_app_data::<DeferredThreadQueue>()
475 .expect(ERR_METADATA_REMOVED);
476 self.lua
477 .remove_app_data::<ThreadErrorCallback>()
478 .expect(ERR_METADATA_REMOVED);
479 self.lua
480 .remove_app_data::<ThreadMap>()
481 .expect(ERR_METADATA_REMOVED);
482 self.lua
483 .remove_app_data::<Exit>()
484 .expect(ERR_METADATA_REMOVED);
485 }
486 }
487}