Skip to main content

tuwunel_database/
pool.rs

1mod configure;
2
3use std::{
4	mem::take,
5	sync::{
6		Arc, Mutex,
7		atomic::{AtomicUsize, Ordering},
8	},
9	thread,
10	thread::JoinHandle,
11};
12
13use async_channel::{QueueStrategy, Receiver, RecvError, Sender};
14use futures::{TryFutureExt, channel::oneshot};
15use oneshot::Sender as ResultSender;
16use rocksdb::Direction;
17use tuwunel_core::{
18	Error, Result, Server, debug, err, error, implement,
19	result::DebugInspect,
20	smallvec::SmallVec,
21	trace,
22	utils::sys::compute::{get_affinity, set_affinity},
23};
24
25use self::configure::configure;
26use crate::{Handle, Map, keyval::KeyBuf, stream};
27
28/// Runs blocking database reads away from asynchronous runtime workers.
29///
30/// Operating-system threads service uncached point queries and iterator seeks
31/// submitted through bounded queues. The pool keeps those blocking calls from
32/// occupying Tokio workers.
33pub(crate) struct Pool {
34	server: Arc<Server>,
35	queues: Vec<Sender<Cmd>>,
36	workers: Mutex<Vec<JoinHandle<()>>>,
37	topology: Vec<usize>,
38	busy: AtomicUsize,
39	queued_max: AtomicUsize,
40}
41
42/// Represents work accepted by the database thread pool.
43///
44/// Point queries use [`Get`], while iterator initialization uses [`Seek`]. Each
45/// command carries a response slot populated before it is enqueued.
46pub(crate) enum Cmd {
47	Get(Get),
48	Iter(Seek),
49}
50
51/// Carries a batched point query to a pool worker.
52///
53/// The map and owned keys cross the thread boundary. The optional response
54/// sender is installed immediately before the command is enqueued.
55pub(crate) struct Get {
56	pub(crate) map: Arc<Map>,
57	pub(crate) key: BatchQuery<'static>,
58	pub(crate) res: Option<ResultSender<BatchResult<'static>>>,
59}
60
61/// Carries an initial iterator seek to a pool worker.
62///
63/// Only the initial seek is offloaded because RocksDB prefetching is expected
64/// to keep later cursor movements nonblocking. The worker returns the
65/// positioned iterator state through the response sender.
66pub(crate) struct Seek {
67	pub(crate) map: Arc<Map>,
68	pub(crate) state: stream::State<'static>,
69	pub(crate) dir: Direction,
70	pub(crate) key: Option<KeyBuf>,
71	pub(crate) res: Option<ResultSender<stream::State<'static>>>,
72}
73
74/// Stores the owned keys in a batched point query.
75///
76/// Small batches remain inline up to the crate's configured batch budget.
77/// Larger batches spill to the heap without changing query order.
78pub(crate) type BatchQuery<'a> = SmallVec<[KeyBuf; BATCH_INLINE]>;
79
80/// Stores the handles returned by a batched point query.
81///
82/// Small result batches remain inline up to the same budget as their queries.
83/// Larger batches spill to the heap.
84pub(crate) type BatchResult<'a> = SmallVec<[ResultHandle<'a>; BATCH_INLINE]>;
85
86/// Represents one point-query result handle.
87///
88/// A successful handle pins the RocksDB value until it is dropped. Its
89/// lifetime remains tied to the database that produced it.
90pub(crate) type ResultHandle<'a> = Result<Handle<'a>>;
91
92const WORKER_LIMIT: (usize, usize) = (1, 4096);
93const QUEUE_LIMIT: (usize, usize) = (1, 1024);
94const BATCH_INLINE: usize = 1;
95
96const WORKER_STACK_SIZE: usize = 1_048_576;
97const WORKER_NAME: &str = "tuwunel:db";
98
99/// Constructs the configured database worker pool.
100///
101/// Queue topology and worker counts derive from detected hardware together
102/// with server configuration. Worker groups are spawned before the shared pool
103/// handle is returned.
104#[implement(Pool)]
105pub(crate) fn new(server: &Arc<Server>) -> Result<Arc<Self>> {
106	const CHAN_SCHED: (QueueStrategy, QueueStrategy) = (QueueStrategy::Fifo, QueueStrategy::Lifo);
107
108	let (topology, workers, queues) = configure(server);
109
110	let (senders, receivers): (Vec<_>, Vec<_>) = queues
111		.into_iter()
112		.map(|cap| cap.max(QUEUE_LIMIT.0))
113		.map(|cap| async_channel::bounded_with_queue_strategy(cap, CHAN_SCHED))
114		.unzip();
115
116	let pool = Arc::new(Self {
117		server: server.clone(),
118		queues: senders,
119		workers: Vec::new().into(),
120		topology,
121		busy: AtomicUsize::default(),
122		queued_max: AtomicUsize::default(),
123	});
124
125	for (chan_id, &count) in workers.iter().enumerate() {
126		pool.spawn_group(&receivers, chan_id, count)?;
127	}
128
129	Ok(pool)
130}
131
132impl Drop for Pool {
133	fn drop(&mut self) {
134		self.close();
135
136		debug_assert!(
137			self.queues.iter().all(Sender::is_empty),
138			"channel must should not have requests queued on drop"
139		);
140		debug_assert!(
141			self.queues.iter().all(Sender::is_closed),
142			"channel should be closed on drop"
143		);
144	}
145}
146
147#[implement(Pool)]
148#[tracing::instrument(skip_all)]
149pub(crate) fn close(&self) {
150	let workers = take(&mut *self.workers.lock().expect("locked"));
151
152	let senders = self
153		.queues
154		.iter()
155		.map(Sender::sender_count)
156		.sum::<usize>();
157
158	let receivers = self
159		.queues
160		.iter()
161		.map(Sender::receiver_count)
162		.sum::<usize>();
163
164	for queue in &self.queues {
165		queue.close();
166	}
167
168	if workers.is_empty() {
169		return;
170	}
171
172	debug!(
173		senders,
174		receivers,
175		queues = self.queues.len(),
176		workers = workers.len(),
177		"Closing pool. Waiting for workers to join..."
178	);
179
180	workers
181		.into_iter()
182		.map(JoinHandle::join)
183		.map(|result| result.map_err(Error::from_panic))
184		.enumerate()
185		.for_each(|(id, result)| match result {
186			| Ok(()) => trace!(?id, "worker joined"),
187			| Err(error) => error!(?id, "worker joined with error: {error}"),
188		});
189}
190
191#[implement(Pool)]
192fn spawn_group(self: &Arc<Self>, recv: &[Receiver<Cmd>], chan_id: usize, count: usize) -> Result {
193	let mut workers = self.workers.lock().expect("locked");
194	for _ in 0..count {
195		self.clone()
196			.spawn_one(&mut workers, recv, chan_id)?;
197	}
198
199	Ok(())
200}
201
202#[implement(Pool)]
203#[tracing::instrument(
204	name = "spawn",
205	level = "trace",
206	skip_all,
207	fields(id = %workers.len())
208)]
209fn spawn_one(
210	self: Arc<Self>,
211	workers: &mut Vec<JoinHandle<()>>,
212	recv: &[Receiver<Cmd>],
213	chan_id: usize,
214) -> Result {
215	debug_assert!(!self.queues.is_empty(), "Must have at least one queue");
216	debug_assert!(!recv.is_empty(), "Must have at least one receiver");
217
218	let id = workers.len();
219	let recv = recv[chan_id].clone();
220
221	let handle = thread::Builder::new()
222		.name(WORKER_NAME.into())
223		.stack_size(WORKER_STACK_SIZE)
224		.spawn(move || self.worker(id, chan_id, &recv))?;
225
226	workers.push(handle);
227
228	Ok(())
229}
230
231#[implement(Pool)]
232#[tracing::instrument(level = "trace", name = "get", skip(self, cmd))]
233pub(crate) async fn execute_get(self: &Arc<Self>, mut cmd: Get) -> Result<BatchResult<'_>> {
234	let (send, recv) = oneshot::channel();
235	_ = cmd.res.insert(send);
236
237	let queue = self.select_queue();
238	self.execute(queue, Cmd::Get(cmd))
239		.and_then(move |()| {
240			recv.map_ok(into_recv_get)
241				.map_err(|e| err!(error!("recv failed {e:?}")))
242		})
243		.await
244}
245
246#[implement(Pool)]
247#[tracing::instrument(level = "trace", name = "iter", skip(self, cmd))]
248pub(crate) async fn execute_iter(self: &Arc<Self>, mut cmd: Seek) -> Result<stream::State<'_>> {
249	let (send, recv) = oneshot::channel();
250	_ = cmd.res.insert(send);
251
252	let queue = self.select_queue();
253	self.execute(queue, Cmd::Iter(cmd))
254		.and_then(|()| {
255			recv.map_ok(into_recv_seek)
256				.map_err(|e| err!(error!("recv failed {e:?}")))
257		})
258		.await
259}
260
261/// Selects the queue assigned to the first CPU affinity entry.
262///
263/// The configured topology maps that affinity identifier to a worker group,
264/// falling back to the first queue when the mapped group is absent.
265///
266/// # Panics
267///
268/// Panics if the current thread has no available CPU affinity entry.
269#[implement(Pool)]
270fn select_queue(&self) -> &Sender<Cmd> {
271	let core_id = get_affinity()
272		.next()
273		.expect("Affinity mask should be available.");
274
275	let chan_id = self.topology[core_id];
276
277	self.queues
278		.get(chan_id)
279		.unwrap_or_else(|| &self.queues[0])
280}
281
282#[implement(Pool)]
283#[tracing::instrument(
284	level = "trace",
285	name = "execute",
286	skip(self, cmd),
287	fields(
288		task = ?tokio::task::try_id(),
289		receivers = queue.receiver_count(),
290		queued = queue.len(),
291		queued_max = self.queued_max.load(Ordering::Relaxed),
292	),
293)]
294async fn execute(&self, queue: &Sender<Cmd>, cmd: Cmd) -> Result {
295	if cfg!(debug_assertions) {
296		self.queued_max
297			.fetch_max(queue.len(), Ordering::Relaxed);
298	}
299
300	queue
301		.send(cmd)
302		.await
303		.map_err(|e| err!(error!("send failed {e:?}")))
304}
305
306#[implement(Pool)]
307#[tracing::instrument(
308	parent = None,
309	level = "debug",
310	skip_all,
311	fields(
312		id,
313		chan_id,
314		thread_id = ?thread::current().id(),
315	),
316)]
317fn worker(self: Arc<Self>, id: usize, chan_id: usize, recv: &Receiver<Cmd>) {
318	self.worker_init(id, chan_id);
319	self.worker_loop(recv);
320}
321
322#[implement(Pool)]
323fn worker_init(&self, id: usize, chan_id: usize) {
324	let affinity = self
325		.topology
326		.iter()
327		.enumerate()
328		.filter(|_| self.server.config.db_pool_affinity)
329		.filter_map(|(core_id, &queue_id)| (chan_id == queue_id).then_some(core_id));
330
331	// affinity is empty (no-op) if there's only one queue
332	set_affinity(affinity.clone());
333
334	trace!(
335		?id,
336		?chan_id,
337		affinity = ?affinity.collect::<Vec<_>>(),
338		"worker ready"
339	);
340}
341
342#[implement(Pool)]
343fn worker_loop(self: &Arc<Self>, recv: &Receiver<Cmd>) {
344	// initial +1 needed prior to entering wait
345	self.busy.fetch_add(1, Ordering::Relaxed);
346
347	while let Ok(cmd) = self.worker_wait(recv) {
348		worker_handle(cmd);
349	}
350}
351
352#[implement(Pool)]
353#[tracing::instrument(
354	name = "wait",
355	level = "trace",
356	skip_all,
357	fields(
358		receivers = recv.receiver_count(),
359		queued = recv.len(),
360		busy = self.busy.fetch_sub(1, Ordering::AcqRel) - 1,
361	),
362)]
363fn worker_wait(self: &Arc<Self>, recv: &Receiver<Cmd>) -> Result<Cmd, RecvError> {
364	recv.recv_blocking().debug_inspect(|_| {
365		self.busy.fetch_add(1, Ordering::Relaxed);
366	})
367}
368
369fn worker_handle(cmd: Cmd) {
370	match cmd {
371		| Cmd::Get(cmd) if cmd.key.len() == 1 => handle_get(cmd),
372		| Cmd::Get(cmd) => handle_batch(cmd),
373		| Cmd::Iter(cmd) => handle_iter(cmd),
374	}
375}
376
377#[tracing::instrument(
378	name = "iter",
379	level = "trace",
380	skip_all,
381	fields(%cmd.map),
382)]
383fn handle_iter(mut cmd: Seek) {
384	let chan = cmd.res.take().expect("missing result channel");
385
386	if chan.is_canceled() {
387		return;
388	}
389
390	let from = cmd.key.as_deref();
391
392	let result = match cmd.dir {
393		| Direction::Forward => cmd.state.init_fwd(from),
394		| Direction::Reverse => cmd.state.init_rev(from),
395	};
396
397	let chan_result = chan.send(into_send_seek(result));
398
399	let _chan_sent = chan_result.is_ok();
400}
401
402#[tracing::instrument(
403	name = "batch",
404	level = "trace",
405	skip_all,
406	fields(
407		%cmd.map,
408		keys = %cmd.key.len(),
409	),
410)]
411fn handle_batch(mut cmd: Get) {
412	debug_assert!(cmd.key.len() > 1, "should have more than one key");
413	debug_assert!(!cmd.key.iter().any(SmallVec::is_empty), "querying for empty key");
414
415	let chan = cmd.res.take().expect("missing result channel");
416
417	if chan.is_canceled() {
418		return;
419	}
420
421	let keys = cmd.key.iter();
422
423	let result: SmallVec<_> = cmd.map.get_batch_blocking(keys).collect();
424
425	let chan_result = chan.send(into_send_get(result));
426
427	let _chan_sent = chan_result.is_ok();
428}
429
430#[tracing::instrument(
431	name = "get",
432	level = "trace",
433	skip_all,
434	fields(%cmd.map),
435)]
436fn handle_get(mut cmd: Get) {
437	debug_assert!(!cmd.key[0].is_empty(), "querying for empty key");
438
439	// Obtain the result channel.
440	let chan = cmd.res.take().expect("missing result channel");
441
442	// It is worth checking if the future was dropped while the command was queued
443	// so we can bail without paying for any query.
444	if chan.is_canceled() {
445		return;
446	}
447
448	// Perform the actual database query. We reuse our database::Map interface but
449	// limited to the blocking calls, rather than creating another surface directly
450	// with rocksdb here.
451	let result = cmd.map.get_blocking(&cmd.key[0]);
452
453	// Send the result back to the submitter.
454	let chan_result = chan.send(into_send_get([result].into()));
455
456	// If the future was dropped during the query this will fail acceptably.
457	let _chan_sent = chan_result.is_ok();
458}
459
460fn into_send_get(result: BatchResult<'_>) -> BatchResult<'static> {
461	// SAFETY: Necessary to send the Handle (rust_rocksdb::PinnableSlice) through
462	// the channel. The lifetime on the handle is a device by rust-rocksdb to
463	// associate a database lifetime with its assets. The Handle must be dropped
464	// before the database is dropped.
465	unsafe { std::mem::transmute(result) }
466}
467
468fn into_recv_get<'a>(result: BatchResult<'static>) -> BatchResult<'a> {
469	// SAFETY: This is to receive the Handle from the channel.
470	unsafe { std::mem::transmute(result) }
471}
472
473pub(crate) fn into_send_seek(result: stream::State<'_>) -> stream::State<'static> {
474	// SAFETY: Necessary to send the State through the channel; see above.
475	unsafe { std::mem::transmute(result) }
476}
477
478fn into_recv_seek<'a>(result: stream::State<'static>) -> stream::State<'a> {
479	// SAFETY: This is to receive the State from the channel; see above.
480	unsafe { std::mem::transmute(result) }
481}