1use std::{
23 any::Any,
24 future::Future,
25 panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
26 pin::Pin,
27 sync::{
28 Arc, Mutex, OnceLock,
29 atomic::{AtomicBool, Ordering},
30 },
31 task::{Context, Poll},
32 time::Instant,
33};
34
35use arrow::{datatypes::SchemaRef, record_batch::RecordBatch};
36use datafusion::{
37 common::{DataFusionError, Result as DataFusionResult},
38 execution::{SessionState, SessionStateBuilder, context::TaskContext},
39 physical_plan::{
40 ExecutionPlan, RecordBatchStream, SendableRecordBatchStream, execute_stream as execute_physical_stream,
41 },
42 prelude::{DataFrame, SessionContext},
43};
44use futures::{FutureExt, Stream, future::BoxFuture};
45#[cfg(test)]
46use query_planning::admission::AttemptCapabilityEvidence;
47use query_planning::error::QueryError;
48use query_runtime::{
49 attempt::{
50 AttemptAdmissionBinding, AttemptAdmissionRejection, AttemptCompletionFactory, AttemptHandle, AttemptOwner,
51 BeginAttemptRequest,
52 },
53 observability::AttemptFailureSummary,
54 runtime::GpuBackendSession,
55};
56pub use query_runtime::{
57 attempt::{AttemptBackendCountersSnapshot, AttemptCancellation, AttemptCompletion},
58 observability::{
59 AttemptCleanupDisposition, AttemptDisposition, AttemptId, AttemptQueryId, AttemptTraceContext,
60 QueryAttemptReport, RuntimeObservationConfig, RuntimeTerminalOutcome, SealedRuntimeObservationSnapshot,
61 graph_observation::GraphMeasuredPeakRow,
62 },
63};
64
65#[cfg(test)]
66use crate::native_exec::admission::{QueryAdmissionSource, QueryAdmissionTicket};
67pub use crate::native_exec::{
68 GraphClaimCalibrationExecution, GraphClaimCalibrationNonPhysicalReason, SealedAttemptGraphClaimCalibrationSnapshot,
69};
70#[cfg(feature = "server")]
71use crate::planner::native::InvocationExecution;
72use crate::{
73 backend::AlgeonGpuBackend,
74 error::{DataFusionErrorExt, Error, ErrorCode, Result},
75 final_plan_observation::NativePlanIndex,
76 native_exec::{
77 SealedAttemptMetricsTarget,
78 admission::{AttemptTicketBinding, NativeAdmissionExec, task_context_with_query_admission_ticket},
79 transparent_root_wrapper_input,
80 },
81 native_report::NativeDataFusionExecutionMetrics,
82 planner::{
83 FinalPlanCoverage, FinalPlanDisposition, NativePlanningDiagnostics, classify_final_plan, native::Invocation,
84 },
85};
86
87#[derive(Debug, Clone)]
93pub struct AttemptOptions {
94 pub query_id: Option<String>,
96 pub trace_context: Option<String>,
99 pub deadline: Option<Instant>,
106 pub observation: RuntimeObservationConfig,
108}
109
110impl Default for AttemptOptions {
111 fn default() -> Self {
112 Self {
113 query_id: None,
114 trace_context: None,
115 deadline: None,
116 observation: RuntimeObservationConfig::native_execution(),
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
124#[non_exhaustive]
125pub struct SealedAttemptNativeExecutionSnapshot {
126 pub attempt_id: AttemptId,
128 pub coverage: FinalPlanCoverage,
130 pub terminal_host_materialization_count: usize,
132 pub intermediate_device_to_host_materialization_count: usize,
135 pub metrics: NativeDataFusionExecutionMetrics,
137}
138
139#[derive(Debug)]
151struct AttemptExecutionState {
152 handle: AttemptHandle,
155 admission_rejection: AttemptAdmissionRejection,
156 admission_binding: AttemptAdmissionBinding,
157 query_id: Option<AttemptQueryId>,
158 query_cap_bytes: Option<u64>,
161 cancellation: AttemptCancellation,
162 sealed_metrics_target: Mutex<Option<(SealedAttemptMetricsTarget, FinalPlanCoverage)>>,
166 result_stream_started: AtomicBool,
169 sealed_graph_claim_calibration: OnceLock<SealedAttemptGraphClaimCalibrationSnapshot>,
172 sealed_native_execution: OnceLock<SealedAttemptNativeExecutionSnapshot>,
173}
174
175impl AttemptExecutionState {
176 fn set_sealed_metrics_target(&self, index: NativePlanIndex) {
177 let Some(target) = index.into_sealed_metrics_target() else { return };
178 let mut slot = self.sealed_metrics_target.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
179 *slot = Some(target);
183 }
184
185 fn publish_sealed_metrics(&self, report: &QueryAttemptReport) {
186 let target = self.sealed_metrics_target.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take();
187 if let Some((target, observation)) = target {
188 let (snapshot, metrics) = target.record(report);
189 let intermediate_device_to_host_materialization_count =
190 metrics.device_to_host_materializations.saturating_sub(observation.host_materialization_count());
191 let _ = self.sealed_graph_claim_calibration.set(snapshot);
192 let _ = self.sealed_native_execution.set(SealedAttemptNativeExecutionSnapshot {
193 attempt_id: report.attempt_id,
194 coverage: observation,
195 terminal_host_materialization_count: observation.host_materialization_count(),
196 intermediate_device_to_host_materialization_count,
197 metrics,
198 });
199 }
200 }
201
202 fn sealed_graph_claim_calibration_snapshot(&self) -> Option<SealedAttemptGraphClaimCalibrationSnapshot> {
203 self.sealed_graph_claim_calibration.get().cloned()
204 }
205
206 fn sealed_native_execution_snapshot(&self) -> Option<SealedAttemptNativeExecutionSnapshot> {
207 self.sealed_native_execution.get().cloned()
208 }
209}
210
211#[derive(Debug, Clone)]
214pub(crate) struct InvocationAttempt {
215 state: Arc<AttemptExecutionState>,
216}
217
218impl InvocationAttempt {
219 fn new(state: Arc<AttemptExecutionState>) -> Self {
220 Self { state }
221 }
222
223 pub(crate) fn query_id(&self) -> Option<&AttemptQueryId> {
226 self.state.query_id.as_ref()
227 }
228
229 pub(crate) fn cancellation(&self) -> &AttemptCancellation {
231 &self.state.cancellation
232 }
233
234 #[cfg(feature = "cugraph")]
238 pub(crate) fn attempt_id(&self) -> AttemptId {
239 self.state.handle.attempt_id()
240 }
241
242 pub(crate) fn ticket_binding(&self) -> AttemptTicketBinding {
248 AttemptTicketBinding {
249 handle: self.state.handle.clone(),
250 admission_rejection: self.state.admission_rejection.clone(),
251 admission_binding: self.state.admission_binding.clone(),
252 cancellation: self.state.cancellation.clone(),
253 requested_query_cap_bytes: self.state.query_cap_bytes,
254 }
255 }
256
257 pub(crate) fn native_admission_planned(&self, plan: &dyn ExecutionPlan) {
264 let index = NativePlanIndex::build(plan);
265 let _ = self.state.handle.set_disposition(disposition_for(index.disposition()));
266 let _ = self.state.handle.record_planning_finished();
267 if !self.state.result_stream_started.load(Ordering::Acquire) {
268 self.state.set_sealed_metrics_target(index);
269 }
270 }
271}
272
273pub struct AlgeonAttempt {
282 owner: Mutex<Option<AttemptOwner>>,
286 state: Arc<AttemptExecutionState>,
287 invocation: Invocation,
291 completion_factory: AttemptCompletionFactory,
295 session: GpuBackendSession,
298}
299
300impl std::fmt::Debug for AlgeonAttempt {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 f.debug_struct("AlgeonAttempt").field("attempt_id", &self.attempt_id()).finish()
303 }
304}
305
306impl AlgeonGpuBackend {
307 pub fn begin_attempt(&self, options: AttemptOptions) -> Result<AlgeonAttempt> {
309 self.begin_attempt_with_cap(options, None)
310 }
311
312 pub fn begin_attempt_with_query_cap_bytes(
318 &self,
319 options: AttemptOptions,
320 query_cap_bytes: u64,
321 ) -> Result<AlgeonAttempt> {
322 self.begin_attempt_with_cap(options, Some(query_cap_bytes))
323 }
324
325 fn begin_attempt_with_cap(&self, options: AttemptOptions, query_cap_bytes: Option<u64>) -> Result<AlgeonAttempt> {
326 AlgeonAttempt::begin(self.session_capability(), options, query_cap_bytes)
327 }
328
329 pub fn take_abandoned_attempt_reports(&self, max: usize) -> Vec<Arc<QueryAttemptReport>> {
333 self.session_capability().take_abandoned_attempt_reports(max)
334 }
335
336 pub fn attempt_counters_snapshot(&self) -> AttemptBackendCountersSnapshot {
338 self.session_capability().attempt_counters_snapshot()
339 }
340}
341
342impl AlgeonAttempt {
343 fn begin(session: &GpuBackendSession, options: AttemptOptions, query_cap_bytes: Option<u64>) -> Result<Self> {
344 let query_id = options.query_id.map(AttemptQueryId::new).transpose().map_err(|error| {
345 Error::from_native("algeon_datafusion.attempt.begin", "invalid attempt query correlation", error)
346 })?;
347 let trace_context = options.trace_context.map(AttemptTraceContext::new).transpose().map_err(|error| {
348 Error::from_native("algeon_datafusion.attempt.begin", "invalid attempt trace correlation", error)
349 })?;
350 let state_query_id = query_id.clone();
351 let begin = session
352 .begin_attempt(BeginAttemptRequest {
353 query_id,
354 trace_context,
355 deadline: options.deadline,
356 observation: options.observation,
357 })
358 .map_err(|error| {
359 Error::from_native("algeon_datafusion.attempt.begin", "failed to begin execution attempt", error)
360 })?;
361 let state = Arc::new(AttemptExecutionState {
362 handle: begin.owner.handle(),
363 admission_rejection: begin.owner.admission_rejection(),
364 admission_binding: begin.owner.admission_binding(),
365 query_id: state_query_id,
366 query_cap_bytes,
367 cancellation: begin.owner.cancellation(),
368 sealed_metrics_target: Mutex::new(None),
369 result_stream_started: AtomicBool::new(false),
370 sealed_graph_claim_calibration: OnceLock::new(),
371 sealed_native_execution: OnceLock::new(),
372 });
373 let completion_factory = begin.completion;
377 let metrics_state = Arc::clone(&state);
378 begin.owner.register_sealed_report_sink(move |report| {
379 metrics_state.publish_sealed_metrics(&report);
380 });
381 Ok(Self {
382 owner: Mutex::new(Some(begin.owner)),
383 invocation: Invocation::for_attempt(InvocationAttempt::new(Arc::clone(&state))),
384 state,
385 completion_factory,
386 session: session.clone(),
387 })
388 }
389
390 pub fn attempt_id(&self) -> AttemptId {
392 self.state.handle.attempt_id()
393 }
394
395 pub fn completion(&self) -> AttemptCompletion {
407 self.completion_factory.completion()
408 }
409
410 pub fn sealed_graph_claim_calibration_snapshot(&self) -> Option<SealedAttemptGraphClaimCalibrationSnapshot> {
417 self.state.sealed_graph_claim_calibration_snapshot()
418 }
419
420 pub fn sealed_native_execution_snapshot(&self) -> Option<SealedAttemptNativeExecutionSnapshot> {
424 self.state.sealed_native_execution_snapshot()
425 }
426
427 pub fn cancellation(&self) -> AttemptCancellation {
432 self.state.cancellation.clone()
433 }
434
435 pub fn planning_diagnostics(&self) -> Option<NativePlanningDiagnostics> {
440 self.invocation.planning_diagnostics()
441 }
442
443 pub fn register_sealed_report_observer<F>(&self, observer: F)
451 where F: Fn(Option<Arc<QueryAttemptReport>>, SealedRuntimeObservationSnapshot) + Send + Sync + 'static {
452 self.register_sealed_execution_observer(move |report, snapshot, _native_execution| observer(report, snapshot));
453 }
454
455 pub(crate) fn register_sealed_execution_observer<F>(&self, observer: F)
459 where F: Fn(
460 Option<Arc<QueryAttemptReport>>,
461 SealedRuntimeObservationSnapshot,
462 Option<SealedAttemptNativeExecutionSnapshot>,
463 ) + Send
464 + Sync
465 + 'static {
466 let factory = self.completion_factory.clone();
467 let state = Arc::clone(&self.state);
468 self.state.admission_binding.register_seal_observer(move |snapshot| {
469 let report = factory.try_report();
470 if report.is_none() {
471 tracing::error!("attempt observation sealed before its immutable report was published");
478 }
479 observer(report, snapshot, state.sealed_native_execution_snapshot());
480 });
481 }
482
483 fn commit(&self, outcome: RuntimeTerminalOutcome) {
488 let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
489 if let Some(owner) = owner.as_ref() {
490 let _ = owner.commit_terminal(outcome);
491 }
492 }
493
494 fn commit_datafusion_error(&self, error: &DataFusionError) {
495 let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
496 if let Some(owner) = owner.as_ref() {
497 commit_datafusion_error(owner, error, &self.state.cancellation);
498 }
499 }
500
501 fn take_owner(&self) -> DataFusionResult<AttemptOwner> {
509 let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take().ok_or_else(|| {
510 DataFusionError::from(Error::new(
511 ErrorCode::Internal,
512 "algeon_datafusion.attempt.execute",
513 "execution attempt already has a result stream",
514 ))
515 })?;
516 self.state.result_stream_started.store(true, Ordering::Release);
517 Ok(owner)
518 }
519
520 fn invocation_state(&self, state: SessionState) -> SessionState {
521 let mut builder = SessionStateBuilder::new_from_existing(state);
522 self.invocation.install(builder.config().get_or_insert_with(Default::default));
523 builder.build()
524 }
525
526 fn invocation_context(&self, ctx: &SessionContext) -> SessionContext {
527 SessionContext::new_with_state(self.invocation_state(ctx.state()))
528 }
529
530 fn cancelled_error(&self, operation: &'static str) -> DataFusionError {
531 let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
532 let error = match owner.as_ref() {
533 Some(owner) => commit_effective_cancellation(owner, &self.state.cancellation, operation),
534 None => self.state.cancellation.check(operation).err(),
535 }
536 .expect("cancellation was checked after it was requested");
537 DataFusionError::from(Error::from_native(operation, "execution attempt was cancelled", error))
538 }
539
540 fn fail_if_backend_closing(&self, operation: &'static str) -> DataFusionResult<()> {
544 if !self.session.is_closing() {
545 return Ok(());
546 }
547 let error =
548 Error::new(ErrorCode::Cancelled, operation, "backend is closing; the attempt cannot start planning")
549 .with_fact("reason", "backend_closing");
550 let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
552 if let Some(owner) = owner.as_ref() {
553 let _ = owner.commit_failure_summary(AttemptFailureSummary::backend_closing());
554 }
555 Err(DataFusionError::from(error))
556 }
557
558 pub fn plan_sql<'a>(
564 &'a self,
565 ctx: &'a SessionContext,
566 sql: &'a str,
567 ) -> BoxFuture<'a, DataFusionResult<Arc<dyn ExecutionPlan>>> {
568 Box::pin(async move {
569 let invocation = self.invocation_context(ctx);
570 self.plan_logical("algeon_datafusion.attempt.plan_sql", async {
571 invocation.sql(sql).await?.create_physical_plan().await
572 })
573 .await
574 })
575 }
576
577 pub fn sql<'a>(
579 &'a self,
580 ctx: &'a SessionContext,
581 sql: &'a str,
582 ) -> impl Future<Output = DataFusionResult<AttemptStream>> + Send + 'a {
583 self.plan_sql(ctx, sql).map(move |plan| plan.and_then(|plan| self.execute_physical_plan(plan, ctx)))
584 }
585
586 pub async fn execute_dataframe(&self, df: DataFrame) -> DataFusionResult<AttemptStream> {
591 let operation = "algeon_datafusion.attempt.execute_dataframe";
592 let (state, logical) = df.into_parts();
593 let invocation = SessionContext::new_with_state(self.invocation_state(state));
594 let plan = self
595 .plan_logical(operation, async { DataFrame::new(invocation.state(), logical).create_physical_plan().await })
596 .await?;
597 self.start_stream(plan, invocation.task_ctx())
598 }
599
600 pub fn execute_physical_plan(
603 &self,
604 plan: Arc<dyn ExecutionPlan>,
605 ctx: &SessionContext,
606 ) -> DataFusionResult<AttemptStream> {
607 self.check_startable("algeon_datafusion.attempt.execute")?;
608 if self.state.handle.disposition().is_none() {
609 let _ = self.state.handle.set_disposition(disposition_for(classify_final_plan(plan.as_ref())));
610 }
611 let invocation = self.invocation_context(ctx);
612 self.start_stream(plan, invocation.task_ctx())
613 }
614
615 fn check_startable(&self, operation: &'static str) -> DataFusionResult<()> {
618 if self.cancellation().check(operation).is_err() {
619 return Err(self.cancelled_error(operation));
620 }
621 self.fail_if_backend_closing(operation)
622 }
623
624 async fn plan_logical(
628 &self,
629 operation: &'static str,
630 planning: impl Future<Output = DataFusionResult<Arc<dyn ExecutionPlan>>>,
631 ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
632 self.check_startable(operation)?;
633 let _ = self.state.handle.record_planning_started();
634 let plan = match AssertUnwindSafe(planning).catch_unwind().await {
638 Ok(Ok(plan)) => plan,
639 Ok(Err(error)) => {
640 self.commit_datafusion_error(&error);
641 return Err(error);
642 },
643 Err(panic) => {
644 self.commit(RuntimeTerminalOutcome::Panicked);
645 resume_unwind(panic);
646 },
647 };
648 if self.cancellation().check(operation).is_err() {
651 return Err(self.cancelled_error(operation));
652 }
653 let _ = self.state.handle.set_disposition(disposition_for(classify_final_plan(plan.as_ref())));
654 let _ = self.state.handle.record_planning_finished();
655 Ok(plan)
656 }
657
658 fn start_stream(
661 &self,
662 plan: Arc<dyn ExecutionPlan>,
663 task_context: Arc<TaskContext>,
664 ) -> DataFusionResult<AttemptStream> {
665 let owner = self.take_owner()?;
669 self.state.set_sealed_metrics_target(NativePlanIndex::build(plan.as_ref()));
670 let task_context = self.task_context_with_attempt_ticket(plan.as_ref(), task_context);
671 match execute_physical_stream(plan, task_context) {
672 Ok(inner) => Ok(AttemptStream::new(inner, owner, Arc::clone(&self.state))),
673 Err(error) => {
674 commit_datafusion_error(&owner, &error, &self.state.cancellation);
675 Err(error)
676 },
677 }
678 }
679
680 fn task_context_with_attempt_ticket(
684 &self,
685 plan: &dyn ExecutionPlan,
686 task_context: Arc<TaskContext>,
687 ) -> Arc<TaskContext> {
688 let Some(admission) = native_admission_plan(plan) else { return task_context };
689 let (ticket, _) = self
690 .invocation
691 .attempt_admission_ticket(admission.source(), Some(admission.evidence()))
692 .expect("an attempt's invocation carries its attempt");
693 task_context_with_query_admission_ticket(task_context, ticket)
694 }
695}
696
697pub(crate) fn execute_in_internal_attempt(
703 session: &GpuBackendSession,
704 plan: &dyn ExecutionPlan,
705 task_context: Arc<TaskContext>,
706 execute: impl FnOnce(Arc<TaskContext>) -> DataFusionResult<SendableRecordBatchStream>,
707) -> DataFusionResult<SendableRecordBatchStream> {
708 let attempt = AlgeonAttempt::begin(session, AttemptOptions::default(), None).map_err(DataFusionError::from)?;
709 let index = NativePlanIndex::build(plan);
710 let _ = attempt.state.handle.set_disposition(disposition_for(index.disposition()));
711 let owner = attempt.take_owner()?;
712 attempt.state.set_sealed_metrics_target(index);
713 let mut session_config = task_context.session_config().clone();
714 attempt.invocation.install(&mut session_config);
715 let task_context = Arc::new(TaskContext::new(
716 task_context.task_id(),
717 task_context.session_id(),
718 session_config,
719 task_context.scalar_functions().clone(),
720 task_context.higher_order_functions().clone(),
721 task_context.aggregate_functions().clone(),
722 task_context.window_functions().clone(),
723 task_context.runtime_env(),
724 ));
725 match execute(task_context) {
726 Ok(inner) => Ok(Box::pin(AttemptStream::new(inner, owner, Arc::clone(&attempt.state)))),
727 Err(error) => {
728 commit_datafusion_error(&owner, &error, &attempt.state.cancellation);
729 Err(error)
730 },
731 }
732}
733
734#[cfg(test)]
738pub(crate) fn attempt_ticket_for_test(
739 source: QueryAdmissionSource,
740 evidence: Option<AttemptCapabilityEvidence>,
741) -> (AlgeonAttempt, Arc<QueryAdmissionTicket>) {
742 attempt_ticket_with_options_for_test(source, evidence, AttemptOptions::default())
743}
744
745#[cfg(test)]
747pub(crate) fn attempt_ticket_with_options_for_test(
748 source: QueryAdmissionSource,
749 evidence: Option<AttemptCapabilityEvidence>,
750 options: AttemptOptions,
751) -> (AlgeonAttempt, Arc<QueryAdmissionTicket>) {
752 let attempt = AlgeonAttempt::begin(source.session(), options, None).expect("a test attempt begins");
753 attempt.state.handle.set_disposition(AttemptDisposition::Native).expect("a new attempt accepts a disposition");
754 let (ticket, _) = attempt
755 .invocation
756 .attempt_admission_ticket(source, evidence)
757 .expect("an attempt's invocation carries its attempt");
758 (attempt, ticket)
759}
760
761fn native_admission_plan(plan: &dyn ExecutionPlan) -> Option<&NativeAdmissionExec> {
762 let mut current = plan;
767 loop {
768 if let Some(admission) = (current as &dyn Any).downcast_ref::<NativeAdmissionExec>() {
769 return Some(admission);
770 }
771 let Some(input) = transparent_root_wrapper_input(current) else {
772 break;
773 };
774 current = input.as_ref();
775 }
776 current.children().into_iter().find_map(|child| native_admission_plan(child.as_ref()))
777}
778
779fn disposition_for(disposition: FinalPlanDisposition) -> AttemptDisposition {
780 match disposition {
781 FinalPlanDisposition::DataFusion => AttemptDisposition::CpuFallback,
782 FinalPlanDisposition::Native => AttemptDisposition::Native,
783 FinalPlanDisposition::Mixed => AttemptDisposition::PartialNative,
784 }
785}
786
787fn commit_datafusion_error(owner: &AttemptOwner, error: &DataFusionError, cancellation: &AttemptCancellation) {
791 if commit_effective_cancellation(owner, cancellation, "algeon_datafusion.attempt.terminal_error").is_some() {
792 return;
793 }
794 match error.find_algeon_error() {
795 Some(error) => commit_adapter_error(owner, error),
796 None => {
797 let _ = owner.commit_failure_summary(AttemptFailureSummary::datafusion_fallback());
798 },
799 }
800}
801
802fn commit_effective_cancellation(
806 owner: &AttemptOwner,
807 cancellation: &AttemptCancellation,
808 operation: &'static str,
809) -> Option<QueryError> {
810 let error = cancellation.check(operation).err()?;
811 let _ = owner.commit_query_error(&error);
812 Some(error)
813}
814
815fn commit_adapter_error(owner: &AttemptOwner, error: &Error) {
819 if error.code() == ErrorCode::Cancelled {
820 let _ = owner.commit_terminal(RuntimeTerminalOutcome::Cancelled);
821 } else if let Some(native) = error.source_error().and_then(|source| source.downcast_ref::<QueryError>()) {
822 let _ = owner.commit_query_error(native);
823 } else {
824 let _ = owner.commit_failure_summary(AttemptFailureSummary::datafusion_fallback());
825 }
826}
827
828pub struct AttemptStream {
834 inner: Option<SendableRecordBatchStream>,
837 schema: SchemaRef,
838 owner: AttemptOwner,
841 state: Arc<AttemptExecutionState>,
842 physical_start_recorded: bool,
843 terminal: bool,
846}
847
848impl AttemptStream {
849 fn new(inner: SendableRecordBatchStream, owner: AttemptOwner, state: Arc<AttemptExecutionState>) -> Self {
850 let schema = inner.schema();
851 Self { inner: Some(inner), schema, owner, state, physical_start_recorded: false, terminal: false }
852 }
853
854 pub fn sealed_graph_claim_calibration_snapshot(&self) -> Option<SealedAttemptGraphClaimCalibrationSnapshot> {
858 self.state.sealed_graph_claim_calibration_snapshot()
859 }
860
861 fn commit(&self, outcome: RuntimeTerminalOutcome) {
862 let _ = self.owner.commit_terminal(outcome);
863 }
864
865 fn commit_datafusion_error(&self, error: &DataFusionError) {
866 commit_datafusion_error(&self.owner, error, &self.state.cancellation);
867 }
868
869 fn finish_terminal(&mut self, outcome: RuntimeTerminalOutcome) {
873 self.terminal = true;
874 self.commit(outcome);
875 drop(self.inner.take());
877 }
878
879 fn finish_cancelled(&mut self) {
882 self.terminal = true;
883 let _ = commit_effective_cancellation(
884 &self.owner,
885 &self.state.cancellation,
886 "algeon_datafusion.attempt.result_drain",
887 );
888 drop(self.inner.take());
891 }
892
893 fn cancelled_error(&self) -> DataFusionError {
894 let operation = "algeon_datafusion.attempt.result_drain";
895 match self.state.cancellation.check(operation) {
896 Err(error) => {
897 DataFusionError::from(Error::from_native(operation, "execution attempt was cancelled", error))
898 },
899 Ok(()) => {
900 DataFusionError::from(Error::new(ErrorCode::Cancelled, operation, "execution attempt was cancelled"))
901 },
902 }
903 }
904}
905
906impl RecordBatchStream for AttemptStream {
907 fn schema(&self) -> SchemaRef {
908 Arc::clone(&self.schema)
909 }
910}
911
912impl Stream for AttemptStream {
913 type Item = DataFusionResult<RecordBatch>;
914
915 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
916 let this = self.get_mut();
917 if this.terminal {
918 return Poll::Ready(None);
919 }
920 if !this.state.handle.is_terminal()
921 && this.state.cancellation.check("algeon_datafusion.attempt.result_drain").is_err()
922 {
923 this.finish_cancelled();
927 return Poll::Ready(Some(Err(this.cancelled_error())));
928 }
929 if !this.physical_start_recorded {
930 this.physical_start_recorded = true;
931 if this.state.handle.disposition() == Some(AttemptDisposition::CpuFallback) {
935 let _ = this.state.handle.record_physical_execution_started();
936 }
937 }
938 let poll = catch_unwind(AssertUnwindSafe(|| {
941 Pin::new(this.inner.as_mut().expect("live attempt stream retains its inner stream")).poll_next(cx)
942 }));
943 match poll {
944 Err(panic) => {
945 this.finish_terminal(RuntimeTerminalOutcome::Panicked);
946 resume_unwind(panic);
947 },
948 Ok(Poll::Ready(None)) => {
949 this.finish_terminal(RuntimeTerminalOutcome::Succeeded);
950 Poll::Ready(None)
951 },
952 Ok(Poll::Ready(Some(Err(error)))) => {
953 this.terminal = true;
954 this.commit_datafusion_error(&error);
955 drop(this.inner.take());
959 Poll::Ready(Some(Err(error)))
960 },
961 Ok(other) => other,
962 }
963 }
964}
965
966impl Drop for AttemptStream {
967 fn drop(&mut self) {
968 if !self.state.handle.is_terminal() {
969 if std::thread::panicking() {
974 self.finish_terminal(RuntimeTerminalOutcome::Panicked);
975 } else if self.state.cancellation.is_cancelled() || self.state.cancellation.is_expired() {
976 self.finish_cancelled();
977 } else {
978 self.state.cancellation.cancel();
982 self.finish_terminal(RuntimeTerminalOutcome::Dropped);
983 }
984 }
985 }
986}
987
988impl std::fmt::Debug for AttemptStream {
989 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
990 f.debug_struct("AttemptStream").field("attempt_id", &self.owner.attempt_id()).finish_non_exhaustive()
991 }
992}
993
994#[cfg(feature = "server")]
1003impl AlgeonAttempt {
1004 pub(crate) fn planning_context(
1011 &self,
1012 ctx: &SessionContext,
1013 execution: InvocationExecution,
1014 ) -> DataFusionResult<SessionContext> {
1015 self.check_startable("algeon_datafusion.attempt.flight_planning")?;
1016 let _ = self.state.handle.record_planning_started();
1017 self.invocation.set_execution(execution);
1018 Ok(self.invocation_context(ctx))
1019 }
1020
1021 pub(crate) fn planning_failed(&self, error: &DataFusionError) {
1028 self.commit_datafusion_error(error);
1029 }
1030
1031 pub(crate) fn planning_failed_adapter(&self, error: &Error) {
1039 let owner = self.owner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
1040 if let Some(owner) = owner.as_ref()
1041 && commit_effective_cancellation(
1042 owner,
1043 &self.state.cancellation,
1044 "algeon_datafusion.attempt.terminal_error",
1045 )
1046 .is_none()
1047 {
1048 commit_adapter_error(owner, error);
1049 }
1050 }
1051
1052 pub(crate) fn physical_planning_finished(&self, plan: &dyn ExecutionPlan) {
1055 let _ = self.state.handle.set_disposition(disposition_for(classify_final_plan(plan)));
1056 let _ = self.state.handle.record_planning_finished();
1057 }
1058
1059 pub(crate) fn cancel_before_stream(&self) {
1061 self.state.cancellation.cancel();
1062 self.commit(RuntimeTerminalOutcome::Cancelled);
1063 }
1064
1065 pub(crate) fn complete_without_result_stream(&self) {
1069 self.commit(RuntimeTerminalOutcome::Succeeded);
1070 }
1071}
1072
1073#[cfg(all(test, not(feature = "cugraph")))]
1074mod tests {
1075 use std::sync::Arc;
1076
1077 use arrow::{
1078 datatypes::{DataType, Field, Schema},
1079 record_batch::RecordBatch,
1080 };
1081 use datafusion::{
1082 datasource::MemTable,
1083 execution::SessionStateBuilder,
1084 prelude::{SessionConfig, SessionContext},
1085 };
1086 use futures::StreamExt;
1087
1088 use super::*;
1089 use crate::{
1090 backend::{AlgeonGpuDeviceProfile, AlgeonGpuQueuePolicy, GpuDeviceMemoryLimit, InstalledTestGpuBackend},
1091 cugraph_sql::CugraphSqlConfig,
1092 error::ErrorCode,
1093 native_exec::{AlgeonNativeExecutionConfig, SourceChunking},
1094 planner::AlgeonGpuExecutionMode,
1095 session::AlgeonSessionStateBuilderExt,
1096 };
1097
1098 #[tokio::test]
1099 async fn feature_disabled_cugraph_rejection_stays_typed_through_an_attempt_stream() {
1100 let backend = InstalledTestGpuBackend::with_config_and_execution_mode(
1101 AlgeonNativeExecutionConfig::new(
1102 SourceChunking::try_new(64 * 1024 * 1024, 2, 3, 1).expect("valid source chunking"),
1103 ),
1104 AlgeonGpuDeviceProfile::new(0)
1105 .with_memory_limit(GpuDeviceMemoryLimit::Explicit { limit_bytes: 256 * 1024 * 1024 }),
1106 AlgeonGpuQueuePolicy::default(),
1107 AlgeonGpuExecutionMode::NativeRequired,
1108 );
1109 let builder = backend
1110 .backend()
1111 .install_on(
1112 SessionStateBuilder::new_with_default_features()
1113 .with_config(SessionConfig::new().with_target_partitions(1)),
1114 )
1115 .expect("feature-off cuGraph SQL backend install must succeed");
1116 let state = builder
1117 .try_with_cugraph_sql(CugraphSqlConfig::default())
1118 .expect("feature-off cuGraph SQL must install on an attempt-capable backend")
1119 .build();
1120 let ctx = SessionContext::new_with_state(state);
1121 let edges = Arc::new(Schema::new(vec![
1122 Field::new("src", DataType::Int64, false),
1123 Field::new("dst", DataType::Int64, false),
1124 ]));
1125 let empty_batch = RecordBatch::new_empty(Arc::clone(&edges));
1126 ctx.register_table("edges", Arc::new(MemTable::try_new(edges, vec![vec![empty_batch]]).unwrap()))
1127 .expect("feature-off source schema must register");
1128
1129 let attempt = backend.backend().begin_attempt(AttemptOptions::default()).unwrap();
1130 let mut stream = attempt
1131 .sql(&ctx, "SELECT * FROM cugraph_pagerank(edges => (SELECT src, dst FROM edges))")
1132 .await
1133 .expect("feature-off execution must fail from the returned result stream");
1134 let error = stream
1135 .next()
1136 .await
1137 .expect("feature-off stream must emit one terminal rejection")
1138 .expect_err("feature-off cuGraph execution must reject");
1139 let typed = error.find_algeon_error().expect("attempt stream must retain the typed feature rejection");
1140 assert_eq!(typed.code(), ErrorCode::UnsupportedOperator);
1141 assert_eq!(typed.operation(), "algeon_datafusion.cugraph_sql.execute");
1142 assert_eq!(typed.fact_value("reason"), Some("required_feature_disabled"));
1143 assert_eq!(typed.fact_value("required_feature"), Some("cugraph"));
1144 assert_eq!(typed.fact_value("host_fallback"), Some("disabled"));
1145 drop(stream);
1146 let _report = attempt.completion().await;
1147 }
1148}