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 Note that the given Lua state must be the same one that was
266 used to create this scheduler, otherwise this method will panic.
267
268 # Panics
269
270 Panics if the given Lua state already has a scheduler attached to it.
271 */
272 #[allow(clippy::too_many_lines)]
273 #[instrument(level = "debug", name = "Scheduler::run", skip(self))]
274 pub async fn run(&self) {
275 /*
276 Create new executors to use - note that we do not need create multiple executors
277 for work stealing, the user may do that themselves if they want to and it will work
278 just fine, as long as anything async is .await-ed from within a Lua async function.
279
280 The main purpose of the two executors here is just to have one with
281 the Send bound, and another (local) one without it, for Lua scheduling.
282
283 We also use the main executor to drive the main loop below forward,
284 saving a tiny bit of processing from going on the Lua executor itself.
285 */
286 let local_exec = LocalExecutor::new();
287 let main_exec = Arc::new(Executor::new());
288 let fut_queue = FuturesQueue::new();
289
290 /*
291 Store the main executor and queue in Lua, so that they may be used with LuaSchedulerExt.
292
293 Also ensure we do not already have an executor or queues - these are definite user errors
294 and may happen if the user tries to run multiple schedulers on the same Lua state at once.
295 */
296 assert!(
297 self.lua.app_data_ref::<WeakArc<Executor>>().is_none(),
298 "{ERR_METADATA_ALREADY_ATTACHED}"
299 );
300 assert!(
301 self.lua.app_data_ref::<FuturesQueue>().is_none(),
302 "{ERR_METADATA_ALREADY_ATTACHED}"
303 );
304
305 self.lua.set_app_data(Arc::downgrade(&main_exec));
306 self.lua.set_app_data(fut_queue.clone());
307
308 /*
309 Manually tick the Lua executor, while running under the main executor.
310 Each tick we wait for the next action to perform, in prioritized order:
311
312 1. The exit event is triggered by setting an exit code
313 2. A Lua thread is available to run on the spawned queue
314 3. A Lua thread is available to run on the deferred queue
315 4. A new thread-local future is available to run on the local executor
316 5. Task(s) scheduled on the Lua executor have made progress and should be polled again
317
318 This ordering is vital to ensure that we don't accidentally exit the main loop
319 when there are new Lua threads to enqueue and potentially more work to be done.
320 */
321 let fut = async {
322 let result_map = self.thread_map.clone();
323 let process_thread = |thread: LuaThread, args| {
324 // NOTE: Thread may have been cancelled from Lua
325 // before we got here, so we need to check it again
326 if thread.status() == LuaThreadStatus::Resumable {
327 // Check if we should be tracking this thread
328 let id = ThreadId::from(&thread);
329 let id_tracked = result_map.is_tracked(id);
330 let result_map_inner = if id_tracked {
331 Some(result_map.clone())
332 } else {
333 None
334 };
335 // Create our future which will run the thread and store its final result
336 let fut = async move {
337 if id_tracked {
338 // Run until yield and check if we got a final result
339 if let Some(res) = run_until_yield(thread.clone(), args).await {
340 if let Err(e) = res.as_ref() {
341 self.error_callback.call(e);
342 }
343 if thread.status() != LuaThreadStatus::Resumable {
344 result_map_inner.unwrap().insert(id, res);
345 }
346 }
347 } else {
348 // Just run until yield
349 if let Some(res) = run_until_yield(thread, args).await {
350 if let Err(e) = res.as_ref() {
351 self.error_callback.call(e);
352 }
353 }
354 }
355 };
356 // Spawn it on the executor
357 local_exec.spawn(fut).detach();
358 }
359 };
360
361 loop {
362 let fut_exit = self.exit.listen(); // 1
363 let fut_spawn = self.queue_spawn.wait_for_item(); // 2
364 let fut_defer = self.queue_defer.wait_for_item(); // 3
365 let fut_futs = fut_queue.wait_for_item(); // 4
366
367 // 5
368 let mut num_processed = 0;
369 let span_tick = trace_span!("Scheduler::tick");
370 let fut_tick = async {
371 local_exec.tick().await;
372 // NOTE: Try to do as much work as possible instead of just a single tick()
373 num_processed += 1;
374 while local_exec.try_tick() {
375 num_processed += 1;
376 }
377 };
378
379 // 1 + 2 + 3 + 4 + 5
380 fut_exit
381 .or(fut_spawn)
382 .or(fut_defer)
383 .or(fut_futs)
384 .or(fut_tick.instrument(span_tick.or_current()))
385 .await;
386
387 // Check if we should exit
388 if self.exit.get().is_some() {
389 debug!("exit signal received");
390 break;
391 }
392
393 // Process spawned threads first, then deferred threads, then futures
394 let mut num_spawned = 0;
395 let mut num_deferred = 0;
396 let mut num_futures = 0;
397 {
398 let _span = trace_span!("Scheduler::drain_spawned").entered();
399 for (thread, args) in self.queue_spawn.take_items() {
400 process_thread(thread, args);
401 num_spawned += 1;
402 }
403 }
404 {
405 let _span = trace_span!("Scheduler::drain_deferred").entered();
406 for (thread, args) in self.queue_defer.take_items() {
407 process_thread(thread, args);
408 num_deferred += 1;
409 }
410 }
411 {
412 let _span = trace_span!("Scheduler::drain_futures").entered();
413 for fut in fut_queue.take_items() {
414 local_exec.spawn(fut).detach();
415 num_futures += 1;
416 }
417 }
418
419 // Empty executor = we didn't spawn any new Lua tasks
420 // above, and there are no remaining tasks to run later
421 let completed = local_exec.is_empty()
422 && self.queue_spawn.is_empty()
423 && self.queue_defer.is_empty();
424 trace!(
425 futures_spawned = num_futures,
426 futures_processed = num_processed,
427 lua_threads_spawned = num_spawned,
428 lua_threads_deferred = num_deferred,
429 "loop"
430 );
431 if completed {
432 break;
433 }
434 }
435 };
436
437 // Run the executor inside a span until all lua threads complete
438 self.set_status(Status::Running);
439 main_exec.run(fut).await;
440 self.set_status(Status::Completed);
441
442 // Clean up
443 self.lua
444 .remove_app_data::<WeakArc<Executor>>()
445 .expect(ERR_METADATA_REMOVED);
446 self.lua
447 .remove_app_data::<FuturesQueue>()
448 .expect(ERR_METADATA_REMOVED);
449 }
450}
451
452impl Drop for Scheduler {
453 fn drop(&mut self) {
454 if panicking() {
455 // Do not cause further panics if already panicking, as
456 // this may abort the program instead of safely unwinding
457 self.lua.remove_app_data::<SpawnedThreadQueue>();
458 self.lua.remove_app_data::<DeferredThreadQueue>();
459 self.lua.remove_app_data::<ThreadErrorCallback>();
460 self.lua.remove_app_data::<ThreadMap>();
461 self.lua.remove_app_data::<Exit>();
462 } else {
463 // In any other case we panic if metadata was removed incorrectly
464 self.lua
465 .remove_app_data::<SpawnedThreadQueue>()
466 .expect(ERR_METADATA_REMOVED);
467 self.lua
468 .remove_app_data::<DeferredThreadQueue>()
469 .expect(ERR_METADATA_REMOVED);
470 self.lua
471 .remove_app_data::<ThreadErrorCallback>()
472 .expect(ERR_METADATA_REMOVED);
473 self.lua
474 .remove_app_data::<ThreadMap>()
475 .expect(ERR_METADATA_REMOVED);
476 self.lua
477 .remove_app_data::<Exit>()
478 .expect(ERR_METADATA_REMOVED);
479 }
480 }
481}