1use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
12use std::sync::mpsc::{
13 self, Receiver, RecvTimeoutError, Sender, SyncSender, TryRecvError, TrySendError,
14};
15use std::sync::{Arc, Mutex};
16use std::thread::{self, JoinHandle};
17use std::time::{Duration, Instant};
18
19use crate::executor::base::{MemoryBudget, MemoryReservation};
20use linkrs_core::error::QueryError;
21
22use super::chunk::DataChunk;
23use super::executor::StreamingExecutor;
24use super::runtime::ExecutionRuntime;
25
26const CHANNEL_WAIT: Duration = Duration::from_millis(2);
27
28pub trait TaskScheduler: Send + Sync + std::fmt::Debug {
33 fn submit(&self, batch: Arc<PartitionBatch>);
35 fn max_workers(&self) -> usize;
37}
38
39#[derive(Debug)]
49pub struct SharedScheduler {
50 pool: Arc<MorselWorkerPool>,
51}
52
53impl SharedScheduler {
54 pub fn new(max_workers: usize) -> Self {
59 Self {
60 pool: Arc::new(MorselWorkerPool::new(max_workers.max(1))),
61 }
62 }
63
64 pub fn apply_to_runtime(&self, runtime: &super::runtime::ExecutionRuntime) {
70 runtime.set_shared_scheduler_raw(Some(self.pool.clone() as Arc<dyn TaskScheduler>));
71 }
72
73 pub fn max_workers(&self) -> usize {
75 self.pool.max_workers()
76 }
77
78 pub fn shutdown(&mut self) {
84 if let Some(pool) = Arc::get_mut(&mut self.pool) {
88 pool.shutdown();
89 }
90 }
93
94 pub fn shutdown_shared(&self) {
101 self.pool.shutdown_shared();
102 }
103}
104
105#[derive(Debug)]
106pub struct BufferedChunk {
107 pub(crate) chunk: DataChunk,
108 pub(crate) _reservation: MemoryReservation,
109 pub(crate) bytes: usize,
110}
111
112#[derive(Debug)]
113pub enum PartitionMessage {
114 Chunk(BufferedChunk),
115 Finished,
116}
117
118#[allow(clippy::too_many_arguments)]
121pub(crate) fn run_partition(
122 tree: &mut StreamingExecutor,
123 sender: &SyncSender<PartitionMessage>,
124 stop: &AtomicBool,
125 runtime: &ExecutionRuntime,
126 queued_chunks: &AtomicUsize,
127 queued_chunks_peak: &AtomicUsize,
128 queued_bytes: &AtomicUsize,
129 queued_bytes_peak: &AtomicUsize,
130) -> Result<(), QueryError> {
131 tree.open()?;
132 let execution_result = (|| {
133 while !stop.load(Ordering::Relaxed) {
134 runtime.ensure_not_cancelled()?;
135 let Some(chunk) = tree.advance()? else {
136 send_message(sender, PartitionMessage::Finished, stop, runtime)?;
137 return Ok(());
138 };
139 let bytes = MemoryBudget::estimate_rows_memory(&chunk.rows);
140 let reservation = runtime.memory_budget.reserve(bytes)?;
141 let message = PartitionMessage::Chunk(BufferedChunk {
142 chunk,
143 _reservation: reservation,
144 bytes,
145 });
146 send_chunk(
147 sender,
148 message,
149 stop,
150 runtime,
151 queued_chunks,
152 queued_chunks_peak,
153 queued_bytes,
154 queued_bytes_peak,
155 )?;
156 }
157 Ok(())
158 })();
159 let close_result = tree.close_tree();
160 execution_result.and(close_result)
161}
162
163#[allow(clippy::too_many_arguments)]
164fn send_chunk(
165 sender: &SyncSender<PartitionMessage>,
166 mut message: PartitionMessage,
167 stop: &AtomicBool,
168 runtime: &ExecutionRuntime,
169 queued_chunks: &AtomicUsize,
170 queued_chunks_peak: &AtomicUsize,
171 queued_bytes: &AtomicUsize,
172 queued_bytes_peak: &AtomicUsize,
173) -> Result<(), QueryError> {
174 loop {
175 if stop.load(Ordering::Relaxed) || runtime.is_cancelled() {
176 return Ok(());
177 }
178 let bytes = match &message {
179 PartitionMessage::Chunk(buffered) => buffered.bytes,
180 PartitionMessage::Finished => 0,
181 };
182 let pre_count = queued_chunks.fetch_add(1, Ordering::Relaxed);
183 let pre_bytes = queued_bytes.fetch_add(bytes, Ordering::Relaxed);
184 match sender.try_send(message) {
185 Ok(()) => {
186 update_peak_value(queued_chunks_peak, pre_count + 1);
187 update_peak_value(queued_bytes_peak, pre_bytes + bytes);
188 return Ok(());
189 }
190 Err(TrySendError::Full(returned)) => {
191 queued_chunks.fetch_sub(1, Ordering::Relaxed);
192 queued_bytes.fetch_sub(bytes, Ordering::Relaxed);
193 message = returned;
194 thread::sleep(CHANNEL_WAIT);
195 }
196 Err(TrySendError::Disconnected(_)) => {
197 queued_chunks.fetch_sub(1, Ordering::Relaxed);
198 queued_bytes.fetch_sub(bytes, Ordering::Relaxed);
199 return Ok(());
200 }
201 }
202 }
203}
204
205pub(crate) fn update_peak_value(peak: &AtomicUsize, candidate: usize) {
206 let mut prev = peak.load(Ordering::Relaxed);
207 while candidate > prev {
208 match peak.compare_exchange_weak(prev, candidate, Ordering::Relaxed, Ordering::Relaxed) {
209 Ok(_) => return,
210 Err(actual) => prev = actual,
211 }
212 }
213}
214
215fn send_message(
216 sender: &SyncSender<PartitionMessage>,
217 mut message: PartitionMessage,
218 stop: &AtomicBool,
219 runtime: &ExecutionRuntime,
220) -> Result<(), QueryError> {
221 loop {
222 if stop.load(Ordering::Relaxed) || runtime.is_cancelled() {
223 return Ok(());
224 }
225 match sender.try_send(message) {
226 Ok(()) => return Ok(()),
227 Err(TrySendError::Full(returned)) => {
228 message = returned;
229 thread::sleep(CHANNEL_WAIT);
230 }
231 Err(TrySendError::Disconnected(_)) => return Ok(()),
232 }
233 }
234}
235
236pub(crate) fn release_queue_metrics(
237 queued_chunks: &AtomicUsize,
238 queued_bytes: &AtomicUsize,
239 bytes: usize,
240) {
241 queued_chunks.fetch_sub(1, Ordering::Relaxed);
242 queued_bytes.fetch_sub(bytes, Ordering::Relaxed);
243}
244
245pub struct PartitionBatch {
253 pub partitions: Vec<Mutex<Option<StreamingExecutor>>>,
254 pub senders: Vec<SyncSender<PartitionMessage>>,
255 pub error_tx: Sender<(usize, QueryError)>,
256 pub next_index: AtomicUsize,
257 pub total: usize,
258 pub runtime: Arc<ExecutionRuntime>,
259 pub worker_time_us: Arc<AtomicU64>,
260 pub buffered_chunks: Arc<AtomicUsize>,
261 pub buffered_bytes: Arc<AtomicUsize>,
262 pub buffered_chunks_peak: Arc<AtomicUsize>,
263 pub buffered_bytes_peak: Arc<AtomicUsize>,
264 pub stop: Arc<AtomicBool>,
265}
266
267impl PartitionBatch {
268 pub(crate) fn new(
271 partitions: Vec<StreamingExecutor>,
272 runtime: Arc<ExecutionRuntime>,
273 max_buffered_chunks: usize,
274 ) -> (
275 Self,
276 Vec<Receiver<PartitionMessage>>,
277 Receiver<(usize, QueryError)>,
278 ) {
279 let partition_count = partitions.len();
280 let capacity = max_buffered_chunks.max(1);
281 let mut senders = Vec::with_capacity(partition_count);
282 let mut receivers = Vec::with_capacity(partition_count);
283 for _ in 0..partition_count {
284 let (tx, rx) = mpsc::sync_channel(capacity);
285 senders.push(tx);
286 receivers.push(rx);
287 }
288 let (error_tx, error_rx) = mpsc::channel();
289
290 let batch = Self {
291 partitions: partitions
292 .into_iter()
293 .map(|p| Mutex::new(Some(p)))
294 .collect(),
295 senders,
296 error_tx,
297 next_index: AtomicUsize::new(0),
298 total: partition_count,
299 runtime,
300 worker_time_us: Arc::new(AtomicU64::new(0)),
301 buffered_chunks: Arc::new(AtomicUsize::new(0)),
302 buffered_bytes: Arc::new(AtomicUsize::new(0)),
303 buffered_chunks_peak: Arc::new(AtomicUsize::new(0)),
304 buffered_bytes_peak: Arc::new(AtomicUsize::new(0)),
305 stop: Arc::new(AtomicBool::new(false)),
306 };
307 (batch, receivers, error_rx)
308 }
309}
310
311#[derive(Debug)]
316pub struct PartitionHandle {
317 pub receivers: Vec<Receiver<PartitionMessage>>,
318 pub error_rx: Mutex<Receiver<(usize, QueryError)>>,
319 pub stop: Arc<AtomicBool>,
320 pub partition_count: usize,
321 pub buffered_chunks: Arc<AtomicUsize>,
322 pub buffered_bytes: Arc<AtomicUsize>,
323 pub buffered_chunks_peak: Arc<AtomicUsize>,
324 pub buffered_bytes_peak: Arc<AtomicUsize>,
325 pub worker_time_us: Arc<AtomicU64>,
326 pub started_at: Instant,
327 pub profile_recorded: bool,
328 pub runtime: Arc<ExecutionRuntime>,
329 worker_count: usize,
330}
331
332impl PartitionHandle {
333 pub(crate) fn from_batch(
335 batch: &Arc<PartitionBatch>,
336 receivers: Vec<Receiver<PartitionMessage>>,
337 error_rx: Receiver<(usize, QueryError)>,
338 runtime: Arc<ExecutionRuntime>,
339 started_at: Instant,
340 worker_count: usize,
341 ) -> Self {
342 Self {
343 receivers,
344 error_rx: Mutex::new(error_rx),
345 stop: batch.stop.clone(),
346 partition_count: batch.total,
347 buffered_chunks: batch.buffered_chunks.clone(),
348 buffered_bytes: batch.buffered_bytes.clone(),
349 buffered_chunks_peak: batch.buffered_chunks_peak.clone(),
350 buffered_bytes_peak: batch.buffered_bytes_peak.clone(),
351 worker_time_us: Arc::clone(&batch.worker_time_us),
352 started_at,
353 profile_recorded: false,
354 runtime,
355 worker_count,
356 }
357 }
358
359 pub fn next_for_partition_blocking(
370 &mut self,
371 partition_id: usize,
372 ) -> Result<Option<DataChunk>, QueryError> {
373 if partition_id >= self.receivers.len() {
374 return Err(QueryError::execution(format!(
375 "Morsel partition handle has no partition {partition_id}",
376 )));
377 }
378 match self.receivers[partition_id].recv() {
379 Ok(PartitionMessage::Chunk(buffered)) => {
380 release_queue_metrics(&self.buffered_chunks, &self.buffered_bytes, buffered.bytes);
381 Ok(Some(buffered.chunk))
382 }
383 Ok(PartitionMessage::Finished) => Ok(None),
384 Err(_) => {
385 self.check_worker_error()?;
386 if self.runtime.is_cancelled() {
387 let _ = self.stop_and_join();
388 return Err(QueryError::execution("Query cancelled".to_string()));
389 }
390 let _ = self.stop_and_join();
391 Err(QueryError::execution(format!(
392 "Morsel partition {partition_id} disconnected before completion",
393 )))
394 }
395 }
396 }
397
398 pub fn next_for_partition(
400 &mut self,
401 partition_id: usize,
402 ) -> Result<Option<DataChunk>, QueryError> {
403 if partition_id >= self.receivers.len() {
404 return Err(QueryError::execution(format!(
405 "Morsel partition handle has no partition {partition_id}",
406 )));
407 }
408
409 loop {
410 self.check_worker_error()?;
411 if self.runtime.is_cancelled() {
412 let _ = self.stop_and_join();
413 return Err(QueryError::execution("Query cancelled".to_string()));
414 }
415 match self.receivers[partition_id].recv_timeout(CHANNEL_WAIT) {
416 Ok(PartitionMessage::Chunk(buffered)) => {
417 release_queue_metrics(
418 &self.buffered_chunks,
419 &self.buffered_bytes,
420 buffered.bytes,
421 );
422 return Ok(Some(buffered.chunk));
423 }
424 Ok(PartitionMessage::Finished) => {
425 return Ok(None);
426 }
427 Err(RecvTimeoutError::Timeout) => continue,
428 Err(RecvTimeoutError::Disconnected) => {
429 self.check_worker_error()?;
430 let _ = self.stop_and_join();
431 return Err(QueryError::execution(format!(
432 "Morsel partition {partition_id} disconnected before completion",
433 )));
434 }
435 }
436 }
437 }
438
439 pub fn stop_and_join(&mut self) -> Result<(), QueryError> {
440 self.stop.store(true, Ordering::Relaxed);
441 self.record_profile();
442 Ok(())
443 }
444
445 fn check_worker_error(&mut self) -> Result<(), QueryError> {
446 let error = { self.error_rx.lock().unwrap().try_recv() };
447 match error {
448 Ok((partition_id, error)) => {
449 let _ = self.stop_and_join();
450 Err(QueryError::execution(format!(
451 "Morsel partition {partition_id} failed: {error}",
452 )))
453 }
454 Err(TryRecvError::Empty) | Err(TryRecvError::Disconnected) => Ok(()),
455 }
456 }
457
458 fn record_profile(&mut self) {
459 if self.profile_recorded {
460 return;
461 }
462 self.profile_recorded = true;
463 let board = self.runtime.profile();
464 board.parallel_wall_time_us.fetch_add(
465 self.started_at.elapsed().as_micros() as u64,
466 Ordering::Relaxed,
467 );
468 board.parallel_work_time_us.fetch_add(
469 self.worker_time_us.load(Ordering::Relaxed),
470 Ordering::Relaxed,
471 );
472 board.parallel_workers.fetch_max(
476 self.worker_count.min(self.partition_count),
477 Ordering::Relaxed,
478 );
479 board.parallel_buffered_chunks_peak.fetch_max(
480 self.buffered_chunks_peak.load(Ordering::Relaxed),
481 Ordering::Relaxed,
482 );
483 board.parallel_buffered_bytes_peak.fetch_max(
484 self.buffered_bytes_peak.load(Ordering::Relaxed),
485 Ordering::Relaxed,
486 );
487 }
488}
489
490pub struct MorselWorkerPool {
497 batch_tx: Mutex<Option<Sender<Arc<PartitionBatch>>>>,
498 workers: Vec<JoinHandle<()>>,
499 stop: Arc<AtomicBool>,
500 max_workers: usize,
501}
502
503impl MorselWorkerPool {
504 pub fn new(max_workers: usize) -> Self {
506 let (batch_tx, batch_rx) = mpsc::channel::<Arc<PartitionBatch>>();
507 let batch_rx = Arc::new(Mutex::new(batch_rx));
508 let stop = Arc::new(AtomicBool::new(false));
509 let worker_count = max_workers.max(1);
510 let mut workers = Vec::with_capacity(worker_count);
511
512 for _ in 0..worker_count {
513 let rx = batch_rx.clone();
514 let stopper = stop.clone();
515 workers.push(thread::spawn(move || loop {
516 if stopper.load(Ordering::Relaxed) {
517 return;
518 }
519 let batch = {
520 let guard = rx.lock().unwrap();
521 guard.recv()
522 };
523 match batch {
524 Ok(batch) => {
525 let error_tx = batch.error_tx.clone();
528 let stop = batch.stop.clone();
529
530 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
531 Self::process_batch(batch, &stopper)
532 }));
533 if let Err(panic_payload) = result {
534 let msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
535 s.to_string()
536 } else if let Some(s) = panic_payload.downcast_ref::<String>() {
537 s.clone()
538 } else {
539 "Worker panicked".to_string()
540 };
541 log::error!("Morsel worker panicked: {msg}");
542 stop.store(true, Ordering::Relaxed);
543 let _ = error_tx.send((usize::MAX, QueryError::execution(msg)));
544 return;
545 }
546 }
547 Err(_) => return,
548 }
549 }));
550 }
551
552 Self {
553 batch_tx: Mutex::new(Some(batch_tx)),
554 workers,
555 stop,
556 max_workers: worker_count,
557 }
558 }
559
560 pub(crate) fn submit(&self, batch: Arc<PartitionBatch>) {
565 let guard = self.batch_tx.lock().unwrap();
566 if let Some(ref tx) = *guard {
567 let workers_to_notify = self.max_workers.min(batch.total);
568 for _ in 0..workers_to_notify {
569 if tx.send(batch.clone()).is_err() {
570 break;
571 }
572 }
573 }
574 }
575
576 pub fn max_workers(&self) -> usize {
578 self.max_workers
579 }
580
581 pub fn shutdown(&mut self) {
586 self.stop.store(true, Ordering::Relaxed);
587 *self.batch_tx.get_mut().unwrap() = None;
588 for handle in self.workers.drain(..) {
589 let _ = handle.join();
590 }
591 }
592
593 pub fn shutdown_shared(&self) {
600 self.stop.store(true, Ordering::Relaxed);
601 if let Ok(mut guard) = self.batch_tx.lock() {
602 *guard = None;
603 }
604 }
605
606 pub fn detach(&mut self) {
612 self.stop.store(true, Ordering::Relaxed);
613 *self.batch_tx.get_mut().unwrap() = None;
614 self.workers.clear();
615 }
616
617 fn process_batch(batch: Arc<PartitionBatch>, stopper: &AtomicBool) {
618 loop {
619 if stopper.load(Ordering::Relaxed) || batch.runtime.is_cancelled() {
620 return;
621 }
622 let index = batch.next_index.fetch_add(1, Ordering::Relaxed);
623 if index >= batch.total {
624 return;
625 }
626
627 let mut tree = batch.partitions[index].lock().unwrap().take();
628 let Some(ref mut tree) = tree else {
629 continue;
630 };
631
632 let started = Instant::now();
633 let result = run_partition(
634 tree,
635 &batch.senders[index],
636 &batch.stop,
637 &batch.runtime,
638 &batch.buffered_chunks,
639 &batch.buffered_chunks_peak,
640 &batch.buffered_bytes,
641 &batch.buffered_bytes_peak,
642 );
643 batch
644 .worker_time_us
645 .fetch_add(started.elapsed().as_micros() as u64, Ordering::Relaxed);
646
647 if let Err(error) = result {
648 batch.stop.store(true, Ordering::Relaxed);
649 let _ = batch.error_tx.send((index, error));
650 return;
651 }
652 }
653 }
654}
655
656impl Drop for MorselWorkerPool {
657 fn drop(&mut self) {
658 self.detach();
661 }
662}
663
664impl TaskScheduler for MorselWorkerPool {
665 fn submit(&self, batch: Arc<PartitionBatch>) {
666 self.submit(batch);
667 }
668
669 fn max_workers(&self) -> usize {
670 self.max_workers
671 }
672}
673
674impl std::fmt::Debug for MorselWorkerPool {
675 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676 f.debug_struct("MorselWorkerPool")
677 .field("max_workers", &self.max_workers)
678 .field("worker_count", &self.workers.len())
679 .finish()
680 }
681}