use std::{
any::Any,
future::Future,
panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
pin::Pin,
sync::{
Arc, Mutex, OnceLock,
atomic::{AtomicBool, Ordering},
},
task::{Context, Poll},
time::Instant,
};
use arrow::{datatypes::SchemaRef, record_batch::RecordBatch};
use datafusion::{
common::{DataFusionError, Result as DataFusionResult},
execution::{SessionState, SessionStateBuilder, context::TaskContext},
physical_plan::{
ExecutionPlan, RecordBatchStream, SendableRecordBatchStream, execute_stream as execute_physical_stream,
},
prelude::{DataFrame, SessionContext},
};
use futures::{FutureExt, Stream, future::BoxFuture};
#[cfg(test)]
use query_planning::admission::AttemptCapabilityEvidence;
use query_planning::error::QueryError;
use query_runtime::{
attempt::{
AttemptAdmissionBinding, AttemptAdmissionRejection, AttemptCompletionFactory, AttemptHandle, AttemptOwner,
BeginAttemptRequest,
},
observability::AttemptFailureSummary,
runtime::GpuBackendSession,
};
pub use query_runtime::{
attempt::{AttemptBackendCountersSnapshot, AttemptCancellation, AttemptCompletion},
observability::{
AttemptCleanupDisposition, AttemptDisposition, AttemptId, AttemptQueryId, AttemptTraceContext,
QueryAttemptReport, RuntimeObservationConfig, RuntimeTerminalOutcome, SealedRuntimeObservationSnapshot,
graph_observation::GraphMeasuredPeakRow,
},
};
#[cfg(test)]
use crate::native_exec::admission::{QueryAdmissionSource, QueryAdmissionTicket};
pub use crate::native_exec::{
GraphClaimCalibrationExecution, GraphClaimCalibrationNonPhysicalReason, SealedAttemptGraphClaimCalibrationSnapshot,
};
#[cfg(feature = "server")]
use crate::planner::native::InvocationExecution;
use crate::{
backend::AlgeonGpuBackend,
error::{DataFusionErrorExt, Error, ErrorCode, Result},
final_plan_observation::NativePlanIndex,
native_exec::{
SealedAttemptMetricsTarget,
admission::{AttemptTicketBinding, NativeAdmissionExec, task_context_with_query_admission_ticket},
transparent_root_wrapper_input,
},
native_report::NativeDataFusionExecutionMetrics,
planner::{
FinalPlanCoverage, FinalPlanDisposition, NativePlanningDiagnostics, classify_final_plan, native::Invocation,
},
};
#[derive(Debug, Clone)]
pub struct AttemptOptions {
pub query_id: Option<String>,
pub trace_context: Option<String>,
pub deadline: Option<Instant>,
pub observation: RuntimeObservationConfig,
}
impl Default for AttemptOptions {
fn default() -> Self {
Self {
query_id: None,
trace_context: None,
deadline: None,
observation: RuntimeObservationConfig::native_execution(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SealedAttemptNativeExecutionSnapshot {
pub attempt_id: AttemptId,
pub coverage: FinalPlanCoverage,
pub terminal_host_materialization_count: usize,
pub intermediate_device_to_host_materialization_count: usize,
pub metrics: NativeDataFusionExecutionMetrics,
}
#[derive(Debug)]
struct AttemptExecutionState {
handle: AttemptHandle,
admission_rejection: AttemptAdmissionRejection,
admission_binding: AttemptAdmissionBinding,
query_id: Option<AttemptQueryId>,
query_cap_bytes: Option<u64>,
cancellation: AttemptCancellation,
sealed_metrics_target: Mutex<Option<(SealedAttemptMetricsTarget, FinalPlanCoverage)>>,
result_stream_started: AtomicBool,
sealed_graph_claim_calibration: OnceLock<SealedAttemptGraphClaimCalibrationSnapshot>,
sealed_native_execution: OnceLock<SealedAttemptNativeExecutionSnapshot>,
}
impl AttemptExecutionState {
fn set_sealed_metrics_target(&self, index: NativePlanIndex) {
let Some(target) = index.into_sealed_metrics_target() else { return };
let mut slot = self.sealed_metrics_target.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
*slot = Some(target);
}
fn publish_sealed_metrics(&self, report: &QueryAttemptReport) {
let target = self.sealed_metrics_target.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take();
if let Some((target, observation)) = target {
let (snapshot, metrics) = target.record(report);
let intermediate_device_to_host_materialization_count =
metrics.device_to_host_materializations.saturating_sub(observation.host_materialization_count());
let _ = self.sealed_graph_claim_calibration.set(snapshot);
let _ = self.sealed_native_execution.set(SealedAttemptNativeExecutionSnapshot {
attempt_id: report.attempt_id,
coverage: observation,
terminal_host_materialization_count: observation.host_materialization_count(),
intermediate_device_to_host_materialization_count,
metrics,
});
}
}
fn sealed_graph_claim_calibration_snapshot(&self) -> Option<SealedAttemptGraphClaimCalibrationSnapshot> {
self.sealed_graph_claim_calibration.get().cloned()
}
fn sealed_native_execution_snapshot(&self) -> Option<SealedAttemptNativeExecutionSnapshot> {
self.sealed_native_execution.get().cloned()
}
}
#[derive(Debug, Clone)]
pub(crate) struct InvocationAttempt {
state: Arc<AttemptExecutionState>,
}
impl InvocationAttempt {
fn new(state: Arc<AttemptExecutionState>) -> Self {
Self { state }
}
pub(crate) fn query_id(&self) -> Option<&AttemptQueryId> {
self.state.query_id.as_ref()
}
pub(crate) fn cancellation(&self) -> &AttemptCancellation {
&self.state.cancellation
}
#[cfg(feature = "cugraph")]
pub(crate) fn attempt_id(&self) -> AttemptId {
self.state.handle.attempt_id()
}
pub(crate) fn ticket_binding(&self) -> AttemptTicketBinding {
AttemptTicketBinding {
handle: self.state.handle.clone(),
admission_rejection: self.state.admission_rejection.clone(),
admission_binding: self.state.admission_binding.clone(),
cancellation: self.state.cancellation.clone(),
requested_query_cap_bytes: self.state.query_cap_bytes,
}
}
pub(crate) fn native_admission_planned(&self, plan: &dyn ExecutionPlan) {
let index = NativePlanIndex::build(plan);
let _ = self.state.handle.set_disposition(disposition_for(index.disposition()));
let _ = self.state.handle.record_planning_finished();
if !self.state.result_stream_started.load(Ordering::Acquire) {
self.state.set_sealed_metrics_target(index);
}
}
}
pub struct AlgeonAttempt {
owner: Mutex<Option<AttemptOwner>>,
state: Arc<AttemptExecutionState>,
invocation: Invocation,
completion_factory: AttemptCompletionFactory,
session: GpuBackendSession,
}
impl std::fmt::Debug for AlgeonAttempt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AlgeonAttempt").field("attempt_id", &self.attempt_id()).finish()
}
}
impl AlgeonGpuBackend {
pub fn begin_attempt(&self, options: AttemptOptions) -> Result<AlgeonAttempt> {
self.begin_attempt_with_cap(options, None)
}
pub fn begin_attempt_with_query_cap_bytes(
&self,
options: AttemptOptions,
query_cap_bytes: u64,
) -> Result<AlgeonAttempt> {
self.begin_attempt_with_cap(options, Some(query_cap_bytes))
}
fn begin_attempt_with_cap(&self, options: AttemptOptions, query_cap_bytes: Option<u64>) -> Result<AlgeonAttempt> {
AlgeonAttempt::begin(self.session_capability(), options, query_cap_bytes)
}
pub fn take_abandoned_attempt_reports(&self, max: usize) -> Vec<Arc<QueryAttemptReport>> {
self.session_capability().take_abandoned_attempt_reports(max)
}
pub fn attempt_counters_snapshot(&self) -> AttemptBackendCountersSnapshot {
self.session_capability().attempt_counters_snapshot()
}
}
impl AlgeonAttempt {
fn begin(session: &GpuBackendSession, options: AttemptOptions, query_cap_bytes: Option<u64>) -> Result<Self> {
let query_id = options.query_id.map(AttemptQueryId::new).transpose().map_err(|error| {
Error::from_native("algeon_datafusion.attempt.begin", "invalid attempt query correlation", error)
})?;
let trace_context = options.trace_context.map(AttemptTraceContext::new).transpose().map_err(|error| {
Error::from_native("algeon_datafusion.attempt.begin", "invalid attempt trace correlation", error)
})?;
let state_query_id = query_id.clone();
let begin = session
.begin_attempt(BeginAttemptRequest {
query_id,
trace_context,
deadline: options.deadline,
observation: options.observation,
})
.map_err(|error| {
Error::from_native("algeon_datafusion.attempt.begin", "failed to begin execution attempt", error)
})?;
let state = Arc::new(AttemptExecutionState {
handle: begin.owner.handle(),
admission_rejection: begin.owner.admission_rejection(),
admission_binding: begin.owner.admission_binding(),
query_id: state_query_id,
query_cap_bytes,
cancellation: begin.owner.cancellation(),
sealed_metrics_target: Mutex::new(None),
result_stream_started: AtomicBool::new(false),
sealed_graph_claim_calibration: OnceLock::new(),
sealed_native_execution: OnceLock::new(),
});
let completion_factory = begin.completion;
let metrics_state = Arc::clone(&state);
begin.owner.register_sealed_report_sink(move |report| {
metrics_state.publish_sealed_metrics(&report);
});
Ok(Self {
owner: Mutex::new(Some(begin.owner)),
invocation: Invocation::for_attempt(InvocationAttempt::new(Arc::clone(&state))),
state,
completion_factory,
session: session.clone(),
})
}
pub fn attempt_id(&self) -> AttemptId {
self.state.handle.attempt_id()
}
pub fn completion(&self) -> AttemptCompletion {
self.completion_factory.completion()
}
pub fn sealed_graph_claim_calibration_snapshot(&self) -> Option<SealedAttemptGraphClaimCalibrationSnapshot> {
self.state.sealed_graph_claim_calibration_snapshot()
}
pub fn sealed_native_execution_snapshot(&self) -> Option<SealedAttemptNativeExecutionSnapshot> {
self.state.sealed_native_execution_snapshot()
}
pub fn cancellation(&self) -> AttemptCancellation {
self.state.cancellation.clone()
}
pub fn planning_diagnostics(&self) -> Option<NativePlanningDiagnostics> {
self.invocation.planning_diagnostics()
}
pub fn register_sealed_report_observer<F>(&self, observer: F)
where F: Fn(Option<Arc<QueryAttemptReport>>, SealedRuntimeObservationSnapshot) + Send + Sync + 'static {
self.register_sealed_execution_observer(move |report, snapshot, _native_execution| observer(report, snapshot));
}
pub(crate) fn register_sealed_execution_observer<F>(&self, observer: F)
where F: Fn(
Option<Arc<QueryAttemptReport>>,
SealedRuntimeObservationSnapshot,
Option<SealedAttemptNativeExecutionSnapshot>,
) + Send
+ Sync
+ 'static {
let factory = self.completion_factory.clone();
let state = Arc::clone(&self.state);
self.state.admission_binding.register_seal_observer(move |snapshot| {
let report = factory.try_report();
if report.is_none() {
tracing::error!("attempt observation sealed before its immutable report was published");
}
observer(report, snapshot, state.sealed_native_execution_snapshot());
});
}
fn commit(&self, outcome: RuntimeTerminalOutcome) {
let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(owner) = owner.as_ref() {
let _ = owner.commit_terminal(outcome);
}
}
fn commit_datafusion_error(&self, error: &DataFusionError) {
let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(owner) = owner.as_ref() {
commit_datafusion_error(owner, error, &self.state.cancellation);
}
}
fn take_owner(&self) -> DataFusionResult<AttemptOwner> {
let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take().ok_or_else(|| {
DataFusionError::from(Error::new(
ErrorCode::Internal,
"algeon_datafusion.attempt.execute",
"execution attempt already has a result stream",
))
})?;
self.state.result_stream_started.store(true, Ordering::Release);
Ok(owner)
}
fn invocation_state(&self, state: SessionState) -> SessionState {
let mut builder = SessionStateBuilder::new_from_existing(state);
self.invocation.install(builder.config().get_or_insert_with(Default::default));
builder.build()
}
fn invocation_context(&self, ctx: &SessionContext) -> SessionContext {
SessionContext::new_with_state(self.invocation_state(ctx.state()))
}
fn cancelled_error(&self, operation: &'static str) -> DataFusionError {
let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let error = match owner.as_ref() {
Some(owner) => commit_effective_cancellation(owner, &self.state.cancellation, operation),
None => self.state.cancellation.check(operation).err(),
}
.expect("cancellation was checked after it was requested");
DataFusionError::from(Error::from_native(operation, "execution attempt was cancelled", error))
}
fn fail_if_backend_closing(&self, operation: &'static str) -> DataFusionResult<()> {
if !self.session.is_closing() {
return Ok(());
}
let error =
Error::new(ErrorCode::Cancelled, operation, "backend is closing; the attempt cannot start planning")
.with_fact("reason", "backend_closing");
let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(owner) = owner.as_ref() {
let _ = owner.commit_failure_summary(AttemptFailureSummary::backend_closing());
}
Err(DataFusionError::from(error))
}
pub fn plan_sql<'a>(
&'a self,
ctx: &'a SessionContext,
sql: &'a str,
) -> BoxFuture<'a, DataFusionResult<Arc<dyn ExecutionPlan>>> {
Box::pin(async move {
let invocation = self.invocation_context(ctx);
self.plan_logical("algeon_datafusion.attempt.plan_sql", async {
invocation.sql(sql).await?.create_physical_plan().await
})
.await
})
}
pub fn sql<'a>(
&'a self,
ctx: &'a SessionContext,
sql: &'a str,
) -> impl Future<Output = DataFusionResult<AttemptStream>> + Send + 'a {
self.plan_sql(ctx, sql).map(move |plan| plan.and_then(|plan| self.execute_physical_plan(plan, ctx)))
}
pub async fn execute_dataframe(&self, df: DataFrame) -> DataFusionResult<AttemptStream> {
let operation = "algeon_datafusion.attempt.execute_dataframe";
let (state, logical) = df.into_parts();
let invocation = SessionContext::new_with_state(self.invocation_state(state));
let plan = self
.plan_logical(operation, async { DataFrame::new(invocation.state(), logical).create_physical_plan().await })
.await?;
self.start_stream(plan, invocation.task_ctx())
}
pub fn execute_physical_plan(
&self,
plan: Arc<dyn ExecutionPlan>,
ctx: &SessionContext,
) -> DataFusionResult<AttemptStream> {
self.check_startable("algeon_datafusion.attempt.execute")?;
if self.state.handle.disposition().is_none() {
let _ = self.state.handle.set_disposition(disposition_for(classify_final_plan(plan.as_ref())));
}
let invocation = self.invocation_context(ctx);
self.start_stream(plan, invocation.task_ctx())
}
fn check_startable(&self, operation: &'static str) -> DataFusionResult<()> {
if self.cancellation().check(operation).is_err() {
return Err(self.cancelled_error(operation));
}
self.fail_if_backend_closing(operation)
}
async fn plan_logical(
&self,
operation: &'static str,
planning: impl Future<Output = DataFusionResult<Arc<dyn ExecutionPlan>>>,
) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
self.check_startable(operation)?;
let _ = self.state.handle.record_planning_started();
let plan = match AssertUnwindSafe(planning).catch_unwind().await {
Ok(Ok(plan)) => plan,
Ok(Err(error)) => {
self.commit_datafusion_error(&error);
return Err(error);
},
Err(panic) => {
self.commit(RuntimeTerminalOutcome::Panicked);
resume_unwind(panic);
},
};
if self.cancellation().check(operation).is_err() {
return Err(self.cancelled_error(operation));
}
let _ = self.state.handle.set_disposition(disposition_for(classify_final_plan(plan.as_ref())));
let _ = self.state.handle.record_planning_finished();
Ok(plan)
}
fn start_stream(
&self,
plan: Arc<dyn ExecutionPlan>,
task_context: Arc<TaskContext>,
) -> DataFusionResult<AttemptStream> {
let owner = self.take_owner()?;
self.state.set_sealed_metrics_target(NativePlanIndex::build(plan.as_ref()));
let task_context = self.task_context_with_attempt_ticket(plan.as_ref(), task_context);
match execute_physical_stream(plan, task_context) {
Ok(inner) => Ok(AttemptStream::new(inner, owner, Arc::clone(&self.state))),
Err(error) => {
commit_datafusion_error(&owner, &error, &self.state.cancellation);
Err(error)
},
}
}
fn task_context_with_attempt_ticket(
&self,
plan: &dyn ExecutionPlan,
task_context: Arc<TaskContext>,
) -> Arc<TaskContext> {
let Some(admission) = native_admission_plan(plan) else { return task_context };
let (ticket, _) = self
.invocation
.attempt_admission_ticket(admission.source(), Some(admission.evidence()))
.expect("an attempt's invocation carries its attempt");
task_context_with_query_admission_ticket(task_context, ticket)
}
}
pub(crate) fn execute_in_internal_attempt(
session: &GpuBackendSession,
plan: &dyn ExecutionPlan,
task_context: Arc<TaskContext>,
execute: impl FnOnce(Arc<TaskContext>) -> DataFusionResult<SendableRecordBatchStream>,
) -> DataFusionResult<SendableRecordBatchStream> {
let attempt = AlgeonAttempt::begin(session, AttemptOptions::default(), None).map_err(DataFusionError::from)?;
let index = NativePlanIndex::build(plan);
let _ = attempt.state.handle.set_disposition(disposition_for(index.disposition()));
let owner = attempt.take_owner()?;
attempt.state.set_sealed_metrics_target(index);
let mut session_config = task_context.session_config().clone();
attempt.invocation.install(&mut session_config);
let task_context = Arc::new(TaskContext::new(
task_context.task_id(),
task_context.session_id(),
session_config,
task_context.scalar_functions().clone(),
task_context.higher_order_functions().clone(),
task_context.aggregate_functions().clone(),
task_context.window_functions().clone(),
task_context.runtime_env(),
));
match execute(task_context) {
Ok(inner) => Ok(Box::pin(AttemptStream::new(inner, owner, Arc::clone(&attempt.state)))),
Err(error) => {
commit_datafusion_error(&owner, &error, &attempt.state.cancellation);
Err(error)
},
}
}
#[cfg(test)]
pub(crate) fn attempt_ticket_for_test(
source: QueryAdmissionSource,
evidence: Option<AttemptCapabilityEvidence>,
) -> (AlgeonAttempt, Arc<QueryAdmissionTicket>) {
attempt_ticket_with_options_for_test(source, evidence, AttemptOptions::default())
}
#[cfg(test)]
pub(crate) fn attempt_ticket_with_options_for_test(
source: QueryAdmissionSource,
evidence: Option<AttemptCapabilityEvidence>,
options: AttemptOptions,
) -> (AlgeonAttempt, Arc<QueryAdmissionTicket>) {
let attempt = AlgeonAttempt::begin(source.session(), options, None).expect("a test attempt begins");
attempt.state.handle.set_disposition(AttemptDisposition::Native).expect("a new attempt accepts a disposition");
let (ticket, _) = attempt
.invocation
.attempt_admission_ticket(source, evidence)
.expect("an attempt's invocation carries its attempt");
(attempt, ticket)
}
fn native_admission_plan(plan: &dyn ExecutionPlan) -> Option<&NativeAdmissionExec> {
let mut current = plan;
loop {
if let Some(admission) = (current as &dyn Any).downcast_ref::<NativeAdmissionExec>() {
return Some(admission);
}
let Some(input) = transparent_root_wrapper_input(current) else {
break;
};
current = input.as_ref();
}
current.children().into_iter().find_map(|child| native_admission_plan(child.as_ref()))
}
fn disposition_for(disposition: FinalPlanDisposition) -> AttemptDisposition {
match disposition {
FinalPlanDisposition::DataFusion => AttemptDisposition::CpuFallback,
FinalPlanDisposition::Native => AttemptDisposition::Native,
FinalPlanDisposition::Mixed => AttemptDisposition::PartialNative,
}
}
fn commit_datafusion_error(owner: &AttemptOwner, error: &DataFusionError, cancellation: &AttemptCancellation) {
if commit_effective_cancellation(owner, cancellation, "algeon_datafusion.attempt.terminal_error").is_some() {
return;
}
match error.find_algeon_error() {
Some(error) => commit_adapter_error(owner, error),
None => {
let _ = owner.commit_failure_summary(AttemptFailureSummary::datafusion_fallback());
},
}
}
fn commit_effective_cancellation(
owner: &AttemptOwner,
cancellation: &AttemptCancellation,
operation: &'static str,
) -> Option<QueryError> {
let error = cancellation.check(operation).err()?;
let _ = owner.commit_query_error(&error);
Some(error)
}
fn commit_adapter_error(owner: &AttemptOwner, error: &Error) {
if error.code() == ErrorCode::Cancelled {
let _ = owner.commit_terminal(RuntimeTerminalOutcome::Cancelled);
} else if let Some(native) = error.source_error().and_then(|source| source.downcast_ref::<QueryError>()) {
let _ = owner.commit_query_error(native);
} else {
let _ = owner.commit_failure_summary(AttemptFailureSummary::datafusion_fallback());
}
}
pub struct AttemptStream {
inner: Option<SendableRecordBatchStream>,
schema: SchemaRef,
owner: AttemptOwner,
state: Arc<AttemptExecutionState>,
physical_start_recorded: bool,
terminal: bool,
}
impl AttemptStream {
fn new(inner: SendableRecordBatchStream, owner: AttemptOwner, state: Arc<AttemptExecutionState>) -> Self {
let schema = inner.schema();
Self { inner: Some(inner), schema, owner, state, physical_start_recorded: false, terminal: false }
}
pub fn sealed_graph_claim_calibration_snapshot(&self) -> Option<SealedAttemptGraphClaimCalibrationSnapshot> {
self.state.sealed_graph_claim_calibration_snapshot()
}
fn commit(&self, outcome: RuntimeTerminalOutcome) {
let _ = self.owner.commit_terminal(outcome);
}
fn commit_datafusion_error(&self, error: &DataFusionError) {
commit_datafusion_error(&self.owner, error, &self.state.cancellation);
}
fn finish_terminal(&mut self, outcome: RuntimeTerminalOutcome) {
self.terminal = true;
self.commit(outcome);
drop(self.inner.take());
}
fn finish_cancelled(&mut self) {
self.terminal = true;
let _ = commit_effective_cancellation(
&self.owner,
&self.state.cancellation,
"algeon_datafusion.attempt.result_drain",
);
drop(self.inner.take());
}
fn cancelled_error(&self) -> DataFusionError {
let operation = "algeon_datafusion.attempt.result_drain";
match self.state.cancellation.check(operation) {
Err(error) => {
DataFusionError::from(Error::from_native(operation, "execution attempt was cancelled", error))
},
Ok(()) => {
DataFusionError::from(Error::new(ErrorCode::Cancelled, operation, "execution attempt was cancelled"))
},
}
}
}
impl RecordBatchStream for AttemptStream {
fn schema(&self) -> SchemaRef {
Arc::clone(&self.schema)
}
}
impl Stream for AttemptStream {
type Item = DataFusionResult<RecordBatch>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.terminal {
return Poll::Ready(None);
}
if !this.state.handle.is_terminal()
&& this.state.cancellation.check("algeon_datafusion.attempt.result_drain").is_err()
{
this.finish_cancelled();
return Poll::Ready(Some(Err(this.cancelled_error())));
}
if !this.physical_start_recorded {
this.physical_start_recorded = true;
if this.state.handle.disposition() == Some(AttemptDisposition::CpuFallback) {
let _ = this.state.handle.record_physical_execution_started();
}
}
let poll = catch_unwind(AssertUnwindSafe(|| {
Pin::new(this.inner.as_mut().expect("live attempt stream retains its inner stream")).poll_next(cx)
}));
match poll {
Err(panic) => {
this.finish_terminal(RuntimeTerminalOutcome::Panicked);
resume_unwind(panic);
},
Ok(Poll::Ready(None)) => {
this.finish_terminal(RuntimeTerminalOutcome::Succeeded);
Poll::Ready(None)
},
Ok(Poll::Ready(Some(Err(error)))) => {
this.terminal = true;
this.commit_datafusion_error(&error);
drop(this.inner.take());
Poll::Ready(Some(Err(error)))
},
Ok(other) => other,
}
}
}
impl Drop for AttemptStream {
fn drop(&mut self) {
if !self.state.handle.is_terminal() {
if std::thread::panicking() {
self.finish_terminal(RuntimeTerminalOutcome::Panicked);
} else if self.state.cancellation.is_cancelled() || self.state.cancellation.is_expired() {
self.finish_cancelled();
} else {
self.state.cancellation.cancel();
self.finish_terminal(RuntimeTerminalOutcome::Dropped);
}
}
}
}
impl std::fmt::Debug for AttemptStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AttemptStream").field("attempt_id", &self.owner.attempt_id()).finish_non_exhaustive()
}
}
#[cfg(feature = "server")]
impl AlgeonAttempt {
pub(crate) fn planning_context(
&self,
ctx: &SessionContext,
execution: InvocationExecution,
) -> DataFusionResult<SessionContext> {
self.check_startable("algeon_datafusion.attempt.flight_planning")?;
let _ = self.state.handle.record_planning_started();
self.invocation.set_execution(execution);
Ok(self.invocation_context(ctx))
}
pub(crate) fn planning_failed(&self, error: &DataFusionError) {
self.commit_datafusion_error(error);
}
pub(crate) fn planning_failed_adapter(&self, error: &Error) {
let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(owner) = owner.as_ref()
&& commit_effective_cancellation(
owner,
&self.state.cancellation,
"algeon_datafusion.attempt.terminal_error",
)
.is_none()
{
commit_adapter_error(owner, error);
}
}
pub(crate) fn physical_planning_finished(&self, plan: &dyn ExecutionPlan) {
let _ = self.state.handle.set_disposition(disposition_for(classify_final_plan(plan)));
let _ = self.state.handle.record_planning_finished();
}
pub(crate) fn cancel_before_stream(&self) {
self.state.cancellation.cancel();
self.commit(RuntimeTerminalOutcome::Cancelled);
}
pub(crate) fn complete_without_result_stream(&self) {
self.commit(RuntimeTerminalOutcome::Succeeded);
}
}
#[cfg(all(test, not(feature = "cugraph")))]
mod tests {
use std::sync::Arc;
use arrow::{
datatypes::{DataType, Field, Schema},
record_batch::RecordBatch,
};
use datafusion::{
datasource::MemTable,
execution::SessionStateBuilder,
prelude::{SessionConfig, SessionContext},
};
use futures::StreamExt;
use super::*;
use crate::{
backend::{AlgeonGpuDeviceProfile, AlgeonGpuQueuePolicy, GpuDeviceMemoryLimit, InstalledTestGpuBackend},
cugraph_sql::CugraphSqlConfig,
error::ErrorCode,
native_exec::{AlgeonNativeExecutionConfig, SourceChunking},
planner::AlgeonGpuExecutionMode,
session::AlgeonSessionStateBuilderExt,
};
#[tokio::test]
async fn feature_disabled_cugraph_rejection_stays_typed_through_an_attempt_stream() {
let backend = InstalledTestGpuBackend::with_config_and_execution_mode(
AlgeonNativeExecutionConfig::new(
SourceChunking::try_new(64 * 1024 * 1024, 2, 3, 1).expect("valid source chunking"),
),
AlgeonGpuDeviceProfile::new(0)
.with_memory_limit(GpuDeviceMemoryLimit::Explicit { limit_bytes: 256 * 1024 * 1024 }),
AlgeonGpuQueuePolicy::default(),
AlgeonGpuExecutionMode::NativeRequired,
);
let builder = backend
.backend()
.install_on(
SessionStateBuilder::new_with_default_features()
.with_config(SessionConfig::new().with_target_partitions(1)),
)
.expect("feature-off cuGraph SQL backend install must succeed");
let state = builder
.try_with_cugraph_sql(CugraphSqlConfig::default())
.expect("feature-off cuGraph SQL must install on an attempt-capable backend")
.build();
let ctx = SessionContext::new_with_state(state);
let edges = Arc::new(Schema::new(vec![
Field::new("src", DataType::Int64, false),
Field::new("dst", DataType::Int64, false),
]));
let empty_batch = RecordBatch::new_empty(Arc::clone(&edges));
ctx.register_table("edges", Arc::new(MemTable::try_new(edges, vec![vec![empty_batch]]).unwrap()))
.expect("feature-off source schema must register");
let attempt = backend.backend().begin_attempt(AttemptOptions::default()).unwrap();
let mut stream = attempt
.sql(&ctx, "SELECT * FROM cugraph_pagerank(edges => (SELECT src, dst FROM edges))")
.await
.expect("feature-off execution must fail from the returned result stream");
let error = stream
.next()
.await
.expect("feature-off stream must emit one terminal rejection")
.expect_err("feature-off cuGraph execution must reject");
let typed = error.find_algeon_error().expect("attempt stream must retain the typed feature rejection");
assert_eq!(typed.code(), ErrorCode::UnsupportedOperator);
assert_eq!(typed.operation(), "algeon_datafusion.cugraph_sql.execute");
assert_eq!(typed.fact_value("reason"), Some("required_feature_disabled"));
assert_eq!(typed.fact_value("required_feature"), Some("cugraph"));
assert_eq!(typed.fact_value("host_fallback"), Some("disabled"));
drop(stream);
let _report = attempt.completion().await;
}
}