Skip to main content

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}