use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use rushdb_lmpg::reconstruct::ReconstructSession;
use crate::aggregate_rows::{row_bytes, Cell, Partial, RowCtx, RowPlan};
use crate::planner::index_candidates_with_budget;
use crate::{
is_match, normalize, resolve_field, scan, walk_total, Budget, EvalCtx, Expr, Json, Lmpg,
PointFields, QueryError, QuerySnapshot, SearchRequest, WorkingSet,
};
pub(crate) const PART_ORDINALS: usize = 8192;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
Count,
Roots,
Groups,
}
pub(crate) struct Pass {
pub(crate) total: u64,
pub(crate) roots: Vec<u64>,
pub(crate) groups: Partial,
pub(crate) candidate_bytes: usize,
}
pub(crate) struct Scan<'a> {
pub(crate) lmpg: &'a Lmpg,
pub(crate) search: &'a SearchRequest,
pub(crate) snap: &'a QuerySnapshot,
pub(crate) budget: Option<usize>,
pub(crate) plan: &'a RowPlan<'a>,
}
enum PartError {
Stopped,
Failed(QueryError),
}
impl From<QueryError> for PartError {
fn from(error: QueryError) -> Self {
Self::Failed(error)
}
}
#[derive(Default)]
struct PartOut {
matched: u64,
roots: Vec<u64>,
groups: Partial,
}
struct Shared<'a> {
check: bool,
predicate: Option<&'a Expr>,
predicate_fields: Option<&'a PointFields>,
rows: &'a Rows<'a>,
mode: Mode,
base: usize,
stop: AtomicBool,
retained: AtomicUsize,
}
pub(crate) struct Rows<'a> {
pub(crate) points: Option<&'a PointFields>,
pub(crate) selected: &'a HashSet<String>,
}
impl Rows<'_> {
pub(crate) fn read(
&self,
plan: &RowPlan<'_>,
snap: &QuerySnapshot,
tenant: rushdb_types::ids::TenantId,
session: &mut ReconstructSession<'_, QuerySnapshot>,
ord: u64,
out: &mut Vec<Option<Cell>>,
) -> Result<bool, QueryError> {
if let Some(points) = self.points {
points.cells(snap, tenant, ord, out)?;
return Ok(true);
}
let Some(Json::Object(record)) = session.reconstruct_record_selected(ord, self.selected)?
else {
return Ok(false);
};
out.clear();
out.extend(
plan.names
.iter()
.map(|name| resolve_field(&record, name).map(Cell::from_json)),
);
Ok(true)
}
pub(crate) fn selection(plan: &RowPlan<'_>) -> HashSet<String> {
plan.names
.iter()
.flat_map(|name| [name.clone(), normalize(name)])
.collect()
}
}
pub(crate) fn run(
scan: &Scan<'_>,
rows: &Rows<'_>,
mode: Mode,
is_cancelled: &dyn Fn() -> bool,
) -> Result<Pass, QueryError> {
let (lmpg, snap, search) = (scan.lmpg, scan.snap, scan.search);
let (source, check, predicate) =
match index_candidates_with_budget(lmpg, snap, search, scan.budget, is_cancelled)? {
Some(plan) => (
Source::Owners(plan.ords.into_iter().collect()),
!plan.exact,
plan.residual,
),
None => (
Source::walk(lmpg, snap, &search.labels, is_cancelled)?,
search.where_.is_some() || !search.related.is_empty(),
search.where_.clone(),
),
};
let candidate_bytes = match &source {
Source::Owners(ords) => ords.capacity().saturating_mul(std::mem::size_of::<u64>()),
Source::Walk { .. } => 0,
};
Budget::WorkingSet.check(scan.budget, candidate_bytes)?;
let predicate_fields = match (&predicate, check) {
(Some(expr), true) => PointFields::for_predicate(snap, lmpg.tenant, &search.labels, expr)?,
_ => None,
};
let shared = Shared {
check,
predicate: predicate.as_ref(),
predicate_fields: predicate_fields.as_ref(),
rows,
mode,
base: candidate_bytes,
stop: AtomicBool::new(false),
retained: AtomicUsize::new(0),
};
let parts = source.parts();
let mut pass = Pass {
total: 0,
roots: Vec::new(),
groups: Partial::default(),
candidate_bytes,
};
let round = rushdb_storage::parallel::budget().saturating_add(1);
let mut start = 0;
while start < parts {
if is_cancelled() {
return Err(QueryError::Cancelled);
}
let end = start.saturating_add(round).min(parts);
let outs = rushdb_storage::parallel::run_parts(end - start, |i| {
let out = source
.part(scan, start + i)
.map_err(PartError::Failed)
.and_then(|ords| run_part(scan, &shared, &ords, source.held(&ords)));
if matches!(out, Err(PartError::Failed(_))) {
shared.stop.store(true, Ordering::SeqCst);
}
Ok::<_, std::convert::Infallible>(out)
})
.unwrap_or_else(|never| match never {});
let mut stopped = false;
for out in outs {
match out {
Ok(out) if !stopped => {
pass.total += out.matched;
pass.roots.extend(out.roots);
pass.groups.merge(out.groups)?;
}
Ok(_) | Err(PartError::Stopped | PartError::Failed(QueryError::Cancelled)) => {
stopped = true;
}
Err(PartError::Failed(error)) => return Err(error),
}
}
if stopped {
return Err(QueryError::Cancelled);
}
start = end;
}
Ok(pass)
}
enum Source {
Owners(Vec<u64>),
Walk { width: u64, end: u64 },
}
impl Source {
fn walk(
lmpg: &Lmpg,
snap: &QuerySnapshot,
labels: &[String],
is_cancelled: &dyn Fn() -> bool,
) -> Result<Self, QueryError> {
let Some(end) = scan::ordinal_end(snap, lmpg.tenant)? else {
return Ok(Self::Owners(
scan::Candidates::walk(snap, lmpg.tenant, labels)?.collect(is_cancelled)?,
));
};
let members = walk_total(lmpg, snap, labels)?.max(1);
let part = u64::try_from(PART_ORDINALS).unwrap_or(u64::MAX);
let width = part.saturating_mul(end.div_ceil(members).max(1));
Ok(Self::Walk { width, end })
}
fn parts(&self) -> usize {
match self {
Self::Owners(ords) => ords.len().div_ceil(PART_ORDINALS),
Self::Walk { width, end } => {
usize::try_from(end.div_ceil(*width)).unwrap_or(usize::MAX)
}
}
}
fn part(&self, scan: &Scan<'_>, part: usize) -> Result<Cow<'_, [u64]>, QueryError> {
match self {
Self::Owners(ords) => {
let start = part.saturating_mul(PART_ORDINALS);
let end = start.saturating_add(PART_ORDINALS).min(ords.len());
Ok(Cow::Borrowed(&ords[start..end]))
}
Self::Walk { width, end } => {
let start = u64::try_from(part)
.unwrap_or(u64::MAX)
.saturating_mul(*width);
let stop = start.saturating_add(*width).min(*end);
Ok(Cow::Owned(scan::walk_range(
scan.snap,
scan.lmpg.tenant,
&scan.search.labels,
start..stop,
)?))
}
}
}
fn held(&self, ords: &[u64]) -> usize {
match self {
Self::Owners(_) => 0,
Self::Walk { .. } => ords.len().saturating_mul(std::mem::size_of::<u64>()),
}
}
}
fn run_part(
scan: &Scan<'_>,
shared: &Shared<'_>,
ords: &[u64],
own_candidates: usize,
) -> Result<PartOut, PartError> {
let (lmpg, snap, budget, plan) = (scan.lmpg, scan.snap, scan.budget, scan.plan);
let limits = lmpg.limits();
let stopped = || shared.stop.load(Ordering::Relaxed) || limits.expired();
let ctx = EvalCtx::at(lmpg, Arc::clone(snap), Some(&stopped));
let mut session = ReconstructSession::new(snap, lmpg.tenant);
let room = budget.map(|limit| limit.saturating_sub(shared.base));
let mut working = WorkingSet {
limit: room.map(|bytes| u64::try_from(bytes).unwrap_or(u64::MAX)),
charged: 0,
};
let mut out = PartOut::default();
let mut row: Vec<Option<Cell>> = Vec::with_capacity(plan.names.len());
let mut own = own_candidates;
Budget::WorkingSet.check(budget, shared.base.saturating_add(own))?;
for (i, &ord) in ords.iter().enumerate() {
if i % 256 == 0 && stopped() {
return Err(if shared.stop.load(Ordering::Relaxed) {
PartError::Stopped
} else {
PartError::Failed(QueryError::Cancelled)
});
}
if shared.check
&& !is_match(
&ctx,
scan.search,
shared.predicate,
shared.predicate_fields,
&mut session,
&mut working,
ord,
)?
{
continue;
}
out.matched += 1;
let added = match shared.mode {
Mode::Count => continue,
Mode::Roots => {
out.roots.push(ord);
std::mem::size_of::<u64>()
}
Mode::Groups => {
if !shared
.rows
.read(plan, snap, lmpg.tenant, &mut session, ord, &mut row)?
{
continue;
}
let row_bytes = row_bytes(&row);
let held = shared.base.saturating_add(own).saturating_add(row_bytes);
Budget::WorkingSet.check(budget, held)?;
let alias_budget = budget.map(|limit| {
limit.saturating_sub(held) / plan.metric_count().saturating_add(1)
});
let row_ctx = RowCtx {
lmpg,
snap,
ord,
alias_budget,
};
out.groups.accumulate(plan, &row, &row_ctx)?
}
};
if added > 0 {
own = own.saturating_add(added);
let total = shared
.retained
.fetch_add(added, Ordering::SeqCst)
.saturating_add(added);
Budget::WorkingSet.check(budget, shared.base.saturating_add(total))?;
}
}
Ok(out)
}