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}