use super::grace_partitioner::GraceSpec;
use super::grace_spill::PartitionedSpiller;
use super::params::JoinParams;
use super::row_source::RowSource;
use crate::bridge::envelope::{ErrorCode, Response};
use crate::data::executor::core_loop::CoreLoop;
use crate::data::executor::handlers::scan_budget::budget_exceeded;
const GRACE_PARTITIONS: usize = 64;
pub(super) struct GraceSources {
pub(super) build: RowSource,
pub(super) probe: RowSource,
}
pub(super) struct LocalJoinSides<'a> {
pub(super) left_collection: &'a str,
pub(super) right_collection: &'a str,
pub(super) left_alias: Option<&'a str>,
pub(super) right_alias: Option<&'a str>,
}
enum BuildState {
Buffering {
docs: Vec<(String, Vec<u8>)>,
bytes: usize,
},
Spilling(Box<PartitionedSpiller>),
}
impl CoreLoop {
pub(super) fn try_grace_hash_join(
&self,
join: &JoinParams<'_>,
tid: u64,
sides: LocalJoinSides<'_>,
budget: usize,
) -> Option<Response> {
let LocalJoinSides {
left_collection,
right_collection,
left_alias,
right_alias,
} = sides;
let probe_collection = left_alias.unwrap_or(left_collection);
let index_collection = right_alias.unwrap_or(right_collection);
let probe_keys: Vec<&str> = join.on.iter().map(|(l, _)| l.as_str()).collect();
let build_keys: Vec<&str> = join.on.iter().map(|(_, r)| r.as_str()).collect();
if join.join_type == "cross" || build_keys.is_empty() || probe_keys.is_empty() {
return None;
}
let did = join.task.request.database_id.as_u64();
let sources = GraceSources {
build: RowSource::LocalScan {
database_id: did,
tenant_id: tid,
collection: right_collection.to_string(),
},
probe: RowSource::LocalScan {
database_id: did,
tenant_id: tid,
collection: left_collection.to_string(),
},
};
let (probe_limit, enforce_output_budget) = if join.limit != usize::MAX {
(join.limit, false)
} else if budget == 0 {
(usize::MAX, false)
} else {
(
crate::data::executor::handlers::scan_budget::fetch_limit_for(
usize::MAX,
0,
budget,
),
true,
)
};
let spec = GraceSpec {
build_keys: &build_keys,
probe_keys: &probe_keys,
join_type: join.join_type,
limit: probe_limit,
probe_collection,
index_collection,
emit_unmatched_right: true,
};
let unique_join_id = join.task.request_id().as_u64();
Some(self.finish_grace_join(
join,
sources,
&spec,
budget,
unique_join_id,
enforce_output_budget,
))
}
pub(super) fn finish_grace_join(
&self,
join: &JoinParams<'_>,
sources: GraceSources,
spec: &GraceSpec<'_>,
budget: usize,
unique_join_id: u64,
enforce_output_budget: bool,
) -> Response {
let mut results = match self.drive_grace_build(sources, spec, budget, unique_join_id) {
Ok(rows) => rows,
Err(crate::Error::MemoryExhausted { .. }) => {
return self.response_error(join.task, ErrorCode::ResourcesExhausted);
}
Err(e) => {
return self.response_error(
join.task,
ErrorCode::Internal {
detail: e.to_string(),
},
);
}
};
if enforce_output_budget && results.len() >= spec.limit {
return self.response_error(join.task, ErrorCode::ResourcesExhausted);
}
if let Err(e) = join.filter_and_project(&mut results) {
return self.response_error(
join.task,
ErrorCode::Internal {
detail: e.to_string(),
},
);
}
let payload = crate::data::executor::response_codec::encode_binary_rows(&results);
self.response_with_payload(join.task, payload)
}
pub(super) fn drive_grace_build(
&self,
sources: GraceSources,
spec: &GraceSpec<'_>,
budget: usize,
unique_join_id: u64,
) -> crate::Result<Vec<Vec<u8>>> {
let spill_dir = self
.data_dir
.join("join-spill")
.join(format!("core-{}", self.core_id()))
.join(format!("{unique_join_id}"));
let per_partition_budget = (budget / GRACE_PARTITIONS).max(1);
let mut state = BuildState::Buffering {
docs: Vec::new(),
bytes: 0,
};
let GraceSources {
build: build_source,
probe: probe_source,
} = sources;
build_source.for_each(self, |id, bytes| {
let drained: Option<Vec<(String, Vec<u8>)>> = match &mut state {
BuildState::Buffering { docs, bytes: total } => {
*total = total.saturating_add(bytes.len()).saturating_add(id.len());
docs.push((String::new(), bytes.to_vec()));
if budget_exceeded(*total, budget) {
Some(std::mem::take(docs))
} else {
None
}
}
BuildState::Spilling(spiller) => {
spiller.push_build(bytes)?;
None
}
};
if let Some(drained) = drained {
std::fs::create_dir_all(&spill_dir).map_err(|e| crate::Error::Storage {
engine: "join-spill".into(),
detail: format!(
"failed to create grace-join spill dir {}: {e}",
spill_dir.display()
),
})?;
let mut spiller = PartitionedSpiller::new(
spec,
GRACE_PARTITIONS,
per_partition_budget,
budget,
spill_dir.clone(),
);
for (_, row) in &drained {
spiller.push_build(row)?;
}
state = BuildState::Spilling(Box::new(spiller));
}
Ok(())
})?;
match state {
BuildState::Buffering { docs, .. } => {
self.stream_probe_against_index(&probe_source, &docs, spec, budget)
}
BuildState::Spilling(mut spiller) => {
let probe_result = (|| -> crate::Result<Vec<Vec<u8>>> {
probe_source.for_each(self, |_id, bytes| spiller.push_probe(bytes))?;
spiller.finish_and_probe()
})();
if let Err(e) = std::fs::remove_dir_all(&spill_dir)
&& spill_dir.exists()
{
tracing::warn!(
error = %e,
dir = %spill_dir.display(),
"failed to remove grace-join spill dir"
);
}
probe_result
}
}
}
}