use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
use std::sync::Arc;
use crate::sqlite::{PageHint, RowsView, Sort, SqliteStore};
const CANCEL_CHECK_OPS: i32 = 2_000;
#[derive(Debug, Clone)]
pub struct PageReq {
pub table: String,
pub limit: i64,
pub offset: i64,
pub sort: Option<Sort>,
pub filter: String,
pub hint: Option<PageHint>,
pub known_total: Option<i64>,
pub formats: std::collections::HashMap<String, crate::browse::Format>,
}
impl PageReq {
pub fn query(&self) -> crate::sqlite::PageQuery<'_> {
crate::sqlite::PageQuery {
table: &self.table,
limit: self.limit,
offset: self.offset,
sort: self.sort.as_ref(),
filter: &self.filter,
hint: self.hint.as_ref(),
known_total: self.known_total,
formats: &self.formats,
}
}
}
#[derive(Debug, Clone)]
pub struct CountReq {
pub table: String,
pub filter: String,
}
#[derive(Debug, Clone)]
pub struct SearchReq {
pub table: String,
pub columns: Vec<String>,
pub term: String,
pub sort: Option<Sort>,
pub filter: String,
pub from: Option<i64>,
pub forward: bool,
}
pub struct SearchDone {
pub generation: u64,
pub result: Result<Option<(i64, i64)>, String>,
}
pub struct PageDone {
pub generation: u64,
pub result: Result<RowsView, String>,
}
pub struct CountDone {
pub generation: u64,
pub table: String,
pub filter: String,
pub result: Result<i64, String>,
}
struct Worker<Req, Out> {
jobs: Sender<(u64, Req)>,
out: Receiver<Out>,
live: Arc<AtomicU64>,
inflight: bool,
}
impl<Req, Out> Worker<Req, Out> {
fn send(&mut self, generation: u64, req: Req) {
self.live.store(generation, Ordering::Relaxed);
if self.jobs.send((generation, req)).is_ok() {
self.inflight = true;
}
}
fn poll(&mut self) -> Option<Out> {
match self.out.try_recv() {
Ok(v) => {
self.inflight = false;
Some(v)
}
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => {
self.inflight = false;
None
}
}
}
}
pub struct Engine {
pages: Worker<PageReq, PageDone>,
counts: Worker<CountReq, CountDone>,
searches: Worker<SearchReq, SearchDone>,
next_generation: u64,
pub page_generation: u64,
}
impl Engine {
pub fn new(path: &std::path::Path) -> Engine {
Engine {
pages: spawn_worker(path.to_path_buf(), |store, generation, req: PageReq| {
PageDone {
generation,
result: store.rows(&req.query()).map_err(|e| e.to_string()),
}
}),
counts: spawn_worker(path.to_path_buf(), |store, generation, req: CountReq| {
CountDone {
generation,
result: store
.count_exact(&req.table, &req.filter)
.map_err(|e| e.to_string()),
table: req.table,
filter: req.filter,
}
}),
searches: spawn_worker(path.to_path_buf(), |store, generation, req: SearchReq| {
SearchDone {
generation,
result: search(store, &req),
}
}),
next_generation: 1,
page_generation: 0,
}
}
pub fn request(&mut self, page: PageReq, count: Option<CountReq>) -> u64 {
let generation = self.next_generation;
self.next_generation += 1;
self.pages.send(generation, page);
if let Some(count) = count {
self.counts.send(generation, count);
}
generation
}
pub fn request_count(&mut self, count: CountReq) {
let generation = self.next_generation;
self.next_generation += 1;
self.counts.send(generation, count);
}
pub fn request_search(&mut self, req: SearchReq) {
let generation = self.next_generation;
self.next_generation += 1;
self.searches.send(generation, req);
}
pub fn poll_search(&mut self) -> Option<SearchDone> {
let live = self.searches.live.load(Ordering::Relaxed);
while let Some(done) = self.searches.poll() {
if done.generation == live {
return Some(done);
}
}
None
}
pub fn searching(&self) -> bool {
self.searches.inflight
}
pub fn page_inflight(&self) -> bool {
self.pages.inflight
}
pub fn count_inflight(&self) -> bool {
self.counts.inflight
}
pub fn poll_page(&mut self) -> Option<PageDone> {
let live = self.pages.live.load(Ordering::Relaxed);
while let Some(done) = self.pages.poll() {
if done.generation == live {
self.page_generation = done.generation;
return Some(done);
}
}
None
}
pub fn wait_page(&mut self, grace: std::time::Duration) -> Option<PageDone> {
let deadline = std::time::Instant::now() + grace;
loop {
if let Some(done) = self.poll_page() {
return Some(done);
}
let left = deadline.saturating_duration_since(std::time::Instant::now());
if left.is_zero() {
return None;
}
match self.pages.out.recv_timeout(left) {
Ok(done) => {
self.pages.inflight = false;
if done.generation == self.pages.live.load(Ordering::Relaxed) {
self.page_generation = done.generation;
return Some(done);
}
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
self.pages.inflight = false;
return None;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => return None,
}
}
}
pub fn poll_count(&mut self) -> Option<CountDone> {
let live = self.counts.live.load(Ordering::Relaxed);
while let Some(done) = self.counts.poll() {
if done.generation == live {
return Some(done);
}
}
None
}
}
fn search(store: &SqliteStore, req: &SearchReq) -> Result<Option<(i64, i64)>, String> {
let query = crate::sqlite::RowQuery {
table: &req.table,
columns: &req.columns,
term: &req.term,
sort: req.sort.as_ref(),
filter: &req.filter,
};
let first = match req.from {
Some(from) => store.find_row(&query, from, req.forward),
None => store.find_row_edge(&query, req.forward),
};
let found = match first {
Err(e) => return Err(e.to_string()),
Ok(Some(r)) => Some(r),
Ok(None) => store
.find_row_edge(&query, req.forward)
.map_err(|e| e.to_string())?,
};
let Some(rowid) = found else {
return Ok(None);
};
let ordinal = store
.rowid_ordinal(&req.table, rowid, req.sort.as_ref(), &req.filter)
.unwrap_or(1);
Ok(Some((rowid, ordinal)))
}
fn spawn_worker<Req, Out, F>(path: PathBuf, work: F) -> Worker<Req, Out>
where
Req: Send + 'static,
Out: Send + 'static,
F: Fn(&SqliteStore, u64, Req) -> Out + Send + 'static,
{
let (jobs_tx, jobs_rx) = std::sync::mpsc::channel::<(u64, Req)>();
let (out_tx, out_rx) = std::sync::mpsc::channel::<Out>();
let live = Arc::new(AtomicU64::new(0));
let live_thread = Arc::clone(&live);
std::thread::spawn(move || {
let mut store = match SqliteStore::open_readonly(&path) {
Ok(s) => s,
Err(_) => return,
};
let mine = Arc::new(AtomicU64::new(0));
let (live_cb, mine_cb) = (Arc::clone(&live_thread), Arc::clone(&mine));
store.set_cancel(
CANCEL_CHECK_OPS,
Arc::new(move || live_cb.load(Ordering::Relaxed) != mine_cb.load(Ordering::Relaxed)),
);
while let Ok(job) = jobs_rx.recv() {
let (generation, req) = {
let mut latest = job;
while let Ok(next) = jobs_rx.try_recv() {
latest = next;
}
latest
};
if generation != live_thread.load(Ordering::Relaxed) {
continue;
}
mine.store(generation, Ordering::Relaxed);
if out_tx.send(work(&store, generation, req)).is_err() {
return;
}
}
});
Worker {
jobs: jobs_tx,
out: out_rx,
live,
inflight: false,
}
}
#[cfg(test)]
mod tests {
use super::{search, CountReq, Engine, PageReq, SearchReq};
use crate::sqlite::SqliteStore;
use std::path::PathBuf;
use std::time::{Duration, Instant};
fn db(name: &str, n: i64) -> PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("zdbview_query_{}_{}.db", std::process::id(), name));
let _ = std::fs::remove_file(&path);
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute("CREATE TABLE t (a TEXT, b TEXT)", []).unwrap();
for i in 0..n {
conn.execute("INSERT INTO t VALUES (?1, 'same')", [i.to_string()])
.unwrap();
}
path
}
fn page(offset: i64) -> PageReq {
PageReq {
table: "t".into(),
limit: 10,
offset,
sort: None,
filter: String::new(),
hint: None,
known_total: None,
formats: std::collections::HashMap::new(),
}
}
#[test]
fn only_the_newest_page_reaches_the_caller() {
let path = db("burst", 200);
let mut engine = Engine::new(&path);
let mut last = 0;
for offset in [0, 10, 20, 30, 40] {
last = engine.request(page(offset), None);
}
let done = engine
.wait_page(Duration::from_secs(5))
.expect("the newest page arrives");
assert_eq!(
done.generation, last,
"the newest generation, not an older one"
);
assert_eq!(engine.page_generation, last);
let view = done.result.expect("rows");
assert_eq!(view.rows[0][0], "40", "the page that was asked for last");
assert!(engine.poll_page().is_none());
let _ = std::fs::remove_file(&path);
}
#[test]
fn every_request_gets_the_next_generation() {
let path = db("gens", 5);
let mut engine = Engine::new(&path);
let first = engine.request(page(0), None);
let second = engine.request(page(0), None);
assert_eq!(second, first + 1);
engine.request_count(CountReq {
table: "t".into(),
filter: String::new(),
});
let third = engine.request(page(0), None);
assert_eq!(third, second + 2, "the count consumed one too");
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_count_says_what_it_counted() {
let path = db("counts", 60);
let mut engine = Engine::new(&path);
engine.request_count(CountReq {
table: "t".into(),
filter: "4".into(),
});
let deadline = Instant::now() + Duration::from_secs(5);
let done = loop {
if let Some(d) = engine.poll_count() {
break d;
}
assert!(Instant::now() < deadline, "the count never arrived");
};
assert_eq!(done.table, "t");
assert_eq!(done.filter, "4");
assert_eq!(done.result.unwrap(), 15);
let _ = std::fs::remove_file(&path);
}
#[test]
fn waiting_for_a_page_nobody_asked_for_gives_up() {
let path = db("idle", 1);
let mut engine = Engine::new(&path);
let start = Instant::now();
assert!(engine.wait_page(Duration::from_millis(80)).is_none());
let waited = start.elapsed();
assert!(waited >= Duration::from_millis(70), "it waited: {waited:?}");
assert!(
waited < Duration::from_secs(2),
"but not forever: {waited:?}"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_database_that_cannot_be_opened_does_not_hang_the_grid() {
let path =
std::env::temp_dir().join(format!("zdbview_query_{}_absent.db", std::process::id()));
let _ = std::fs::remove_file(&path);
let mut engine = Engine::new(&path);
engine.request(page(0), None);
let start = Instant::now();
assert!(engine.wait_page(Duration::from_secs(2)).is_none());
assert!(
start.elapsed() < Duration::from_secs(2),
"it did not sit out the grace"
);
assert!(
!engine.page_inflight(),
"and it stopped saying it was working"
);
}
#[test]
fn a_search_comes_back_through_its_own_worker() {
let path = db("search_engine", 60);
let mut engine = Engine::new(&path);
let req = |term: &str| SearchReq {
table: "t".into(),
columns: vec!["a".into(), "b".into()],
term: term.into(),
sort: None,
filter: String::new(),
from: None,
forward: true,
};
assert!(!engine.searching(), "nothing is out yet");
engine.request_search(req("42"));
assert!(engine.searching(), "and now something is");
let deadline = Instant::now() + Duration::from_secs(5);
let done = loop {
if let Some(d) = engine.poll_search() {
break d;
}
assert!(Instant::now() < deadline, "the search never came back");
};
let (_rowid, ordinal) = done.result.expect("a result").expect("a match");
assert_eq!(ordinal, 43, "row 43 holds '42'");
assert!(!engine.searching(), "and the worker is free again");
engine.request_search(req("no such value"));
engine.request_search(req("7"));
let deadline = Instant::now() + Duration::from_secs(5);
let done = loop {
if let Some(d) = engine.poll_search() {
break d;
}
assert!(
Instant::now() < deadline,
"the newest search never came back"
);
};
assert!(
done.result.expect("a result").is_some(),
"the result is the newest search's, not the abandoned one's"
);
assert!(
engine.poll_search().is_none(),
"and nothing stale follows it"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_search_wraps_and_reports_the_position() {
let path = db("search", 30);
let store = SqliteStore::open_readonly(&path).unwrap();
let req = |from: Option<i64>, forward: bool| SearchReq {
table: "t".into(),
columns: vec!["a".into(), "b".into()],
term: "29".into(),
sort: None,
filter: String::new(),
from,
forward,
};
let (rowid, ordinal) = search(&store, &req(Some(1), true)).unwrap().unwrap();
assert_eq!(ordinal, 30, "1-based position in display order");
let (again, _) = search(&store, &req(Some(rowid), true)).unwrap().unwrap();
assert_eq!(again, rowid, "the search wrapped instead of giving up");
let none = search(
&store,
&SearchReq {
term: "no such value".into(),
..req(None, true)
},
)
.unwrap();
assert!(none.is_none());
let _ = std::fs::remove_file(&path);
}
}