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}