Skip to main content

better_duck_core/raw/
task_state.rs

1//! RAII wrapper for DuckDB's external task scheduler (`duckdb_task_state`).
2//!
3//! By default DuckDB runs its own background threads. The external task API lets an
4//! application drive DuckDB's work on its *own* threads instead: create a
5//! [`TaskState`] for a database, then call [`execute`](TaskState::execute) /
6//! [`execute_n`](TaskState::execute_n) from one or more worker threads; those calls
7//! run pending tasks until [`finish`](TaskState::finish) is signalled.
8//!
9//! Safety-critical ownership rule (from the C API): `duckdb_destroy_task_state` must
10//! **not** run while any `execute_*` call is still active on that state. [`TaskState`]
11//! encodes this by keeping the database [`Arc`] alive for its whole lifetime and by
12//! taking `&self` for the executor calls — a `TaskState` shared across worker threads
13//! lives behind an [`Arc`], so it is only destroyed once the last worker has dropped
14//! its clone. It is `Send + Sync`: the C API explicitly documents that multiple
15//! threads may share one task state.
16// FFI pointer args are used safely inside `unsafe` blocks.
17#![allow(clippy::not_unsafe_ptr_arg_deref)]
18
19use std::sync::Arc;
20
21use crate::{
22    database::Database,
23    ffi::{
24        duckdb_create_task_state, duckdb_destroy_task_state, duckdb_execute_n_tasks_state,
25        duckdb_execute_tasks, duckdb_execute_tasks_state, duckdb_finish_execution,
26        duckdb_task_state, duckdb_task_state_is_finished, idx_t,
27    },
28    raw::connection::RawDatabase,
29};
30
31/// Runs up to `max_tasks` of a database's pending tasks on the calling thread, once.
32///
33/// A convenience over the stateful API for the common "help DuckDB along" case; for
34/// long-running or multi-threaded execution use a [`TaskState`].
35pub fn execute_tasks(
36    db: &Database,
37    max_tasks: u64,
38) {
39    // SAFETY: `db.handle()` is a valid open database handle kept alive by `db`.
40    unsafe { duckdb_execute_tasks(db.handle(), max_tasks as idx_t) };
41}
42
43/// An owned `duckdb_task_state` that drives a database's task execution.
44///
45/// Holds the database [`Arc`] so the underlying database outlives the state.
46pub struct TaskState {
47    state: duckdb_task_state,
48    // Keeps the database alive at least as long as this task state.
49    _db: Arc<RawDatabase>,
50}
51
52// SAFETY: DuckDB documents that a single `duckdb_task_state` may be shared across
53// multiple threads (they all call `duckdb_execute_tasks_state` on it), so it is safe
54// to send between and share across threads.
55unsafe impl Send for TaskState {}
56// SAFETY: as above — concurrent `execute_*` on one state from several threads is the
57// documented usage.
58unsafe impl Sync for TaskState {}
59
60impl TaskState {
61    /// Creates a task state for `db`.
62    #[must_use]
63    pub fn new(db: &Database) -> TaskState {
64        // SAFETY: `db.handle()` is a valid open database handle.
65        let state = unsafe { duckdb_create_task_state(db.handle()) };
66        TaskState { state, _db: db.arc() }
67    }
68
69    /// Executes pending tasks on the calling thread until [`finish`](TaskState::finish)
70    /// is signalled. Intended to be run on a dedicated worker thread (and may be run
71    /// on several threads sharing this state).
72    pub fn execute(&self) {
73        // SAFETY: `self.state` is a valid task state; the database is kept alive by the
74        // retained `Arc`, and destruction cannot race this call (see the type docs).
75        unsafe { duckdb_execute_tasks_state(self.state) };
76    }
77
78    /// Executes up to `max_tasks` pending tasks on the calling thread, returning how
79    /// many actually ran. Stops early if [`finish`](TaskState::finish) is signalled
80    /// or there are no more tasks.
81    pub fn execute_n(
82        &self,
83        max_tasks: u64,
84    ) -> u64 {
85        // SAFETY: `self.state` is a valid task state kept usable by the retained `Arc`.
86        unsafe { duckdb_execute_n_tasks_state(self.state, max_tasks as idx_t) as u64 }
87    }
88
89    /// Signals every `execute_*` call on this state to stop.
90    pub fn finish(&self) {
91        // SAFETY: `self.state` is a valid task state.
92        unsafe { duckdb_finish_execution(self.state) };
93    }
94
95    /// Whether [`finish`](TaskState::finish) has been signalled on this state.
96    #[must_use]
97    pub fn is_finished(&self) -> bool {
98        // SAFETY: `self.state` is a valid task state.
99        unsafe { duckdb_task_state_is_finished(self.state) }
100    }
101}
102
103impl Drop for TaskState {
104    fn drop(&mut self) {
105        if !self.state.is_null() {
106            // Ensure no executor keeps running, then destroy. The C API forbids
107            // destroying a state with an active `execute_*`; by construction every
108            // executor borrows `&self`, so no `execute_*` can outlive this drop.
109            // SAFETY: `self.state` is a valid, non-null task state; signalling finish
110            // then destroying it exactly once is the documented teardown order.
111            unsafe {
112                duckdb_finish_execution(self.state);
113                duckdb_destroy_task_state(self.state);
114            }
115            self.state = std::ptr::null_mut();
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn task_state_lifecycle_and_execution() {
126        let db = Database::open_in_memory().unwrap();
127        // Run some work so there are tasks to drive.
128        let mut conn = db.connect().unwrap();
129        conn.execute_batch("CREATE TABLE t AS SELECT * FROM range(1000) tbl(i)").unwrap();
130
131        let state = TaskState::new(&db);
132        assert!(!state.is_finished(), "a fresh state is not finished");
133        // Draining tasks on this thread is safe and returns a count.
134        let _ran = state.execute_n(16);
135        // Signalling finish flips the flag and makes execute() return promptly.
136        state.finish();
137        assert!(state.is_finished());
138        state.execute();
139        // Dropped here: finish + destroy, exactly once.
140    }
141
142    #[test]
143    fn shared_state_across_threads() {
144        let db = Database::open_in_memory().unwrap();
145        let state = Arc::new(TaskState::new(&db));
146        // Several worker threads share one task state (the documented usage).
147        let mut handles = Vec::new();
148        for _ in 0..3 {
149            let s = Arc::clone(&state);
150            handles.push(std::thread::spawn(move || {
151                s.execute_n(4);
152            }));
153        }
154        state.finish();
155        for h in handles {
156            h.join().unwrap();
157        }
158        assert!(state.is_finished());
159    }
160
161    #[test]
162    fn execute_tasks_convenience_runs() {
163        let db = Database::open_in_memory().unwrap();
164        // Just exercises the one-shot helper; it must not panic or hang.
165        execute_tasks(&db, 8);
166    }
167}