use std::any::Any;
use std::collections::BTreeSet;
use std::ops::Range;
use std::panic::{self, AssertUnwindSafe};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
use std::sync::Arc;
use arrow_array::{new_empty_array, Array, ArrayRef, RecordBatch};
use arrow_schema::SchemaRef;
use super::source::{PageError, PageQuery, PageSource};
const PAGES_PER_CALL: u64 = 4;
#[derive(Debug)]
pub(super) enum Ask {
Query {
epoch: u64,
query: PageQuery,
count: bool,
},
Pages { epoch: u64, pages: Vec<u64> },
}
#[derive(Debug)]
pub(super) enum Got {
Count {
epoch: u64,
rows: u64,
},
Pages {
epoch: u64,
first: u64,
pages: Vec<Fetched>,
},
Failed {
epoch: u64,
pages: Option<Range<u64>>,
message: String,
},
Skipped {
epoch: u64,
pages: Vec<u64>,
},
Stopped {
message: String,
},
}
#[derive(Debug)]
pub(super) struct Fetched {
pub(super) rows: usize,
pub(super) columns: Vec<ArrayRef>,
}
#[derive(Debug, Default)]
pub(super) struct Wanted {
first: AtomicU64,
last: AtomicU64,
}
impl Wanted {
pub(super) fn set(&self, first: u64, last: u64) {
self.first.store(first, Ordering::Relaxed);
self.last.store(last, Ordering::Relaxed);
}
fn get(&self) -> (u64, u64) {
(
self.first.load(Ordering::Relaxed),
self.last.load(Ordering::Relaxed),
)
}
}
#[derive(Debug, Default)]
struct Queue {
pages: BTreeSet<u64>,
alone: BTreeSet<u64>,
}
impl Queue {
fn is_empty(&self) -> bool {
self.pages.is_empty()
}
fn clear(&mut self) {
self.pages.clear();
self.alone.clear();
}
fn put_back(&mut self, pages: Range<u64>) {
self.pages.extend(pages.clone());
self.alone.extend(pages);
}
fn skip(&mut self, first: u64, last: u64) -> Vec<u64> {
let mut inside = self.pages.split_off(&first);
let above = match last.checked_add(1) {
Some(after) => inside.split_off(&after),
None => BTreeSet::new(),
};
let below = std::mem::replace(&mut self.pages, inside);
let skipped: Vec<u64> = below.into_iter().chain(above).collect();
for page in &skipped {
self.alone.remove(page);
}
skipped
}
fn next(&mut self, first: u64, last: u64) -> Option<Range<u64>> {
let middle = first.midpoint(last);
let above = self.pages.range(middle..).next().copied();
let below = self.pages.range(..middle).next_back().copied();
let nearest = match (below, above) {
(Some(below), Some(above)) if middle - below <= above - middle => below,
(_, Some(above)) => above,
(Some(below), None) => below,
(None, None) => return None,
};
let (mut start, mut end) = (nearest, nearest + 1);
if !self.alone.contains(&nearest) {
let joins = |page: u64| self.pages.contains(&page) && !self.alone.contains(&page);
while end - start < PAGES_PER_CALL && joins(end) {
end += 1;
}
while end - start < PAGES_PER_CALL && start > 0 && joins(start - 1) {
start -= 1;
}
}
for page in start..end {
self.pages.remove(&page);
self.alone.remove(&page);
}
Some(start..end)
}
}
pub(super) struct Worker<S> {
pub(super) source: S,
pub(super) schema: SchemaRef,
pub(super) page_rows: usize,
pub(super) asks: Receiver<Ask>,
pub(super) gots: Sender<Got>,
pub(super) wanted: Arc<Wanted>,
pub(super) wake: Box<dyn Fn() + Send>,
}
impl<S: PageSource> Worker<S> {
pub(super) fn serve(mut self) {
let Err(panic) = panic::catch_unwind(AssertUnwindSafe(|| self.run())) else {
return;
};
let message = format!("a panic: {}", panic_text(&*panic));
if self.gots.send(Got::Stopped { message }).is_ok() {
let _ = panic::catch_unwind(AssertUnwindSafe(|| (self.wake)()));
}
}
fn run(&mut self) {
let mut epoch = 0;
let mut query = PageQuery::new();
let mut queue = Queue::default();
loop {
let mut count = false;
if queue.is_empty() {
match self.asks.recv() {
Ok(ask) => count |= take(ask, &mut epoch, &mut query, &mut queue),
Err(_) => return,
}
}
loop {
match self.asks.try_recv() {
Ok(ask) => count |= take(ask, &mut epoch, &mut query, &mut queue),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => return,
}
}
if count {
self.count(epoch, &query);
}
let (first, last) = self.wanted.get();
if first <= last {
let pages = queue.skip(first, last);
if !pages.is_empty() {
self.send(Got::Skipped { epoch, pages });
}
}
if let Some(pages) = queue.next(first, last) {
self.fetch(epoch, &query, pages, &mut queue);
}
}
}
fn call<T>(&mut self, call: impl FnOnce(&mut S) -> Result<T, PageError>) -> Result<T, String> {
match panic::catch_unwind(AssertUnwindSafe(|| call(&mut self.source))) {
Ok(Ok(value)) => Ok(value),
Ok(Err(error)) => Err(error.to_string()),
Err(panic) => Err(format!("the page source panicked: {}", panic_text(&*panic))),
}
}
fn count(&mut self, epoch: u64, query: &PageQuery) {
let got = match self.call(|source| source.row_count(query)) {
Ok(rows) => Got::Count { epoch, rows },
Err(message) => Got::Failed {
epoch,
pages: None,
message,
},
};
self.send(got);
}
fn fetch(&mut self, epoch: u64, query: &PageQuery, pages: Range<u64>, queue: &mut Queue) {
let (page_rows, count) = (self.page_rows as u64, (pages.end - pages.start) as usize);
let offset = pages.start.saturating_mul(page_rows);
let limit = self.page_rows.saturating_mul(count);
let fetched = self
.call(|source| source.page(query, offset, limit))
.and_then(|batches| self.join(&batches));
let got = match fetched {
Ok((columns, rows)) => Got::Pages {
epoch,
first: pages.start,
pages: split(&columns, rows, self.page_rows, count),
},
Err(_) if count > 1 => {
queue.put_back(pages);
return;
}
Err(message) => Got::Failed {
epoch,
pages: Some(pages),
message,
},
};
self.send(got);
}
fn join(&self, batches: &[RecordBatch]) -> Result<(Vec<ArrayRef>, usize), String> {
for batch in batches {
self.check(batch)?;
}
let rows = batches.iter().map(RecordBatch::num_rows).sum();
let columns = self
.schema
.fields()
.iter()
.enumerate()
.map(|(at, field)| match batches {
[] => Ok(new_empty_array(field.data_type())),
[one] => Ok(Arc::clone(one.column(at))),
many => {
let arrays: Vec<&dyn Array> =
many.iter().map(|batch| batch.column(at).as_ref()).collect();
arrow_select::concat::concat(&arrays).map_err(|error| {
format!("the page source's batches could not be joined: {error}")
})
}
})
.collect::<Result<Vec<_>, _>>()?;
Ok((columns, rows))
}
fn check(&self, batch: &RecordBatch) -> Result<(), String> {
let ours = self.schema.fields();
let theirs = batch.schema_ref().fields();
if ours.len() != theirs.len() {
return Err(format!(
"the page source gave {} columns, the model's schema has {}: give pages with \
the schema the model was made with",
theirs.len(),
ours.len()
));
}
match ours
.iter()
.zip(theirs.iter())
.find(|(a, b)| a.data_type() != b.data_type())
{
Some((a, b)) => Err(format!(
"the page source gave column {} as {}, the model's schema has {}",
a.name(),
b.data_type(),
a.data_type()
)),
None => Ok(()),
}
}
fn send(&self, got: Got) {
if self.gots.send(got).is_ok() {
(self.wake)();
}
}
}
fn take(ask: Ask, epoch: &mut u64, query: &mut PageQuery, queue: &mut Queue) -> bool {
match ask {
Ask::Query {
epoch: e,
query: q,
count,
} => {
*epoch = e;
*query = q;
queue.clear();
count
}
Ask::Pages { epoch: e, pages } => {
if e == *epoch {
queue.pages.extend(pages);
}
false
}
}
}
fn split(columns: &[ArrayRef], rows: usize, page_rows: usize, pages: usize) -> Vec<Fetched> {
(0..pages)
.map(|page| {
let start = page.saturating_mul(page_rows).min(rows);
let len = page_rows.min(rows - start);
Fetched {
rows: len,
columns: columns
.iter()
.map(|column| column.slice(start, len))
.collect(),
}
})
.collect()
}
fn panic_text(panic: &(dyn Any + Send)) -> &str {
panic
.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
.unwrap_or("no message")
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::Duration;
use arrow_array::{ArrayRef, Int64Array, RecordBatch};
use arrow_schema::{DataType, Field, Schema};
use super::*;
#[test]
fn the_queue_gives_the_middle_first_then_the_nearer_side() {
let mut queue = Queue::default();
queue.pages.extend(0..10);
assert_eq!(queue.next(0, 9), Some(4..8));
assert_eq!(queue.next(0, 9), Some(0..4));
assert_eq!(queue.next(0, 9), Some(8..10));
assert_eq!(queue.next(0, 9), None);
}
#[test]
fn pages_put_back_are_fetched_alone_and_join_no_call() {
let mut queue = Queue::default();
queue.pages.extend([4, 6]);
queue.put_back(5..6);
assert_eq!(queue.next(4, 6), Some(5..6));
assert_eq!(queue.next(4, 6), Some(4..5));
assert_eq!(queue.next(4, 6), Some(6..7));
queue.pages.extend([6, 7]);
queue.put_back(4..6);
assert_eq!(queue.next(4, 8), Some(6..8));
assert_eq!(queue.next(4, 8), Some(5..6));
assert_eq!(queue.next(4, 8), Some(4..5));
assert!(queue.is_empty() && queue.alone.is_empty());
}
#[test]
fn a_page_skipped_is_no_longer_fetched_alone() {
let mut queue = Queue::default();
queue.put_back(4..8);
assert_eq!(queue.skip(5, 6), [4, 7]);
assert_eq!(queue.alone, BTreeSet::from([5, 6]));
queue.clear();
assert!(queue.is_empty() && queue.alone.is_empty());
}
struct Rows;
impl PageSource for Rows {
fn row_count(&mut self, _: &PageQuery) -> Result<u64, PageError> {
Ok(1_000)
}
fn page(
&mut self,
_: &PageQuery,
offset: u64,
limit: usize,
) -> Result<Vec<RecordBatch>, PageError> {
let start = i64::try_from(offset)?;
let n: Vec<i64> = (start..start + i64::try_from(limit)?).collect();
let n: ArrayRef = Arc::new(Int64Array::from(n));
Ok(vec![RecordBatch::try_from_iter([("n", n)])?])
}
}
fn worker() -> (Sender<Ask>, Receiver<Got>, Arc<Wanted>) {
let (asks, asked) = mpsc::channel();
let (sent, gots) = mpsc::channel();
let wanted = Arc::new(Wanted::default());
let worker = Worker {
source: Rows,
schema: Arc::new(Schema::new(vec![Field::new("n", DataType::Int64, false)])),
page_rows: 10,
asks: asked,
gots: sent,
wanted: Arc::clone(&wanted),
wake: Box::new(|| {}),
};
thread::spawn(move || worker.serve());
(asks, gots, wanted)
}
#[test]
fn hints_read_backwards_skip_nothing() {
let (asks, gots, wanted) = worker();
let query = PageQuery::new();
let (epoch, count) = (1, false);
asks.send(Ask::Query {
epoch,
query,
count,
})
.unwrap();
wanted.set(1_000, 10);
asks.send(Ask::Pages {
epoch,
pages: vec![5],
})
.unwrap();
let got = gots.recv_timeout(Duration::from_secs(5)).unwrap();
assert!(
matches!(got, Got::Pages { first: 5, .. }),
"fetched, not skipped: {got:?}"
);
}
}