Skip to main content

algeon_datafusion/
attempt.rs

1//! Explicit execution-attempt adapter boundary.
2//!
3//! An [`AlgeonAttempt`] transitions across defined lifecycle phases: beginning in
4//! an admitted or rejected state, planning at most once into a result stream, and
5//! sealing with an immutable [`QueryAttemptReport`]. Disposition tracking is
6//! owned by the runtime, recording whether planning succeeded and how execution
7//! terminated. [`AttemptDisposition`] remains distinct from plan-time coverage
8//! to prevent runtime dependency on DataFusion adapter types. `Unplanned`
9//! identifies lifecycle termination prior to plan completion rather than
10//! physical plan structure. The adapter translates completed [`crate::planner::FinalPlanDisposition`]
11//! values directly into [`AttemptDisposition`] (`DataFusion` to `CpuFallback`,
12//! `Mixed` to `PartialNative`, `Native` to `Native`). When planning fails, the attempt
13//! seals as `Unplanned` without an assigned plan disposition.
14//!
15//! Attempt numbering, admission grants, lifecycle transitions, and report sealing
16//! are governed by the query runtime
17//! (`website/docs/design/admission.mdx#from-capability-validation-to-admission`).
18//! This module projects DataFusion planning and stream events onto `AttemptOwner`
19//! commits. The owner records the first terminal commit and ignores subsequent
20//! attempts; individual execution paths commit the specific outcome observed.
21
22use 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/// Inputs used to create an execution attempt before DataFusion planning.
88///
89/// [`Self::default`] enables bounded native observation using
90/// [`RuntimeObservationConfig::NATIVE_EXECUTION_MAX_RECORDS`] and
91/// [`RuntimeObservationConfig::NATIVE_EXECUTION_MAX_BYTES`].
92#[derive(Debug, Clone)]
93pub struct AttemptOptions {
94    /// Optional caller correlation copied into the terminal attempt report.
95    pub query_id: Option<String>,
96    /// Optional opaque distributed-trace correlation copied into attempt
97    /// observation and the terminal report.
98    pub trace_context: Option<String>,
99    /// Invocation-owner deadline in the current process's monotonic
100    /// [`Instant`] clock domain.
101    ///
102    /// Represents local process monotonic time rather than wall-clock or wire
103    /// timestamps. Invocations originating remotely must convert incoming
104    /// deadlines prior to beginning the attempt.
105    pub deadline: Option<Instant>,
106    /// Query-local runtime observation retention policy.
107    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/// Immutable native-plan and boundary evidence captured by the elected root
122/// at the same seal point as its attempt report.
123#[derive(Debug, Clone, PartialEq, Eq)]
124#[non_exhaustive]
125pub struct SealedAttemptNativeExecutionSnapshot {
126    /// Identity of the attempt whose terminal report sealed this snapshot.
127    pub attempt_id: AttemptId,
128    /// Coverage of the elected root's final physical plan, captured before execution.
129    pub coverage: FinalPlanCoverage,
130    /// Number of native wrapper output boundaries requiring host Arrow materialization.
131    pub terminal_host_materialization_count: usize,
132    /// Runtime device-to-host materializations beyond the planned wrapper output boundaries.
133    /// The count saturates at zero when the runtime count is smaller than the planned count.
134    pub intermediate_device_to_host_materialization_count: usize,
135    /// Native execution observations frozen at the terminal report's seal point.
136    pub metrics: NativeDataFusionExecutionMetrics,
137}
138
139/// Per-invocation state propagated through DataFusion configuration.
140///
141/// Kept private to carry invocation bindings through DataFusion planning
142/// without granting custom optimizer rules terminalization or observation-sealing
143/// authority.
144///
145/// Shared by [`AlgeonAttempt`], the [`InvocationAttempt`] inside the
146/// invocation's config extension, the [`AttemptStream`] that executes the
147/// plan, and the sealed-report sink registered at `begin`. The sink closure
148/// keeps it alive until the runtime publishes the report, which can happen
149/// after the caller dropped both the attempt and the stream.
150#[derive(Debug)]
151struct AttemptExecutionState {
152    /// Records lifecycle boundaries and reads terminal state. Terminal commits
153    /// go through the `AttemptOwner`, which one holder has at a time.
154    handle: AttemptHandle,
155    admission_rejection: AttemptAdmissionRejection,
156    admission_binding: AttemptAdmissionBinding,
157    query_id: Option<AttemptQueryId>,
158    /// Exact cap requested at `begin`, copied into the attempt's one admission
159    /// request. `None` leaves the service's configured cap in force.
160    query_cap_bytes: Option<u64>,
161    cancellation: AttemptCancellation,
162    /// The elected native root that may receive the fixed-cardinality
163    /// attempt metric after this attempt's report has sealed. The target is
164    /// consumed at seal, preventing subsequent wrappers from publishing duplicate rows.
165    sealed_metrics_target: Mutex<Option<(SealedAttemptMetricsTarget, FinalPlanCoverage)>>,
166    /// Set once terminal authority moves into the attempt's result
167    /// stream; planning after that cannot replace the executing target.
168    result_stream_started: AtomicBool,
169    /// Typed graph claim/peak rows frozen by the elected target at the same
170    /// seal point as the immutable attempt report.
171    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        // Planning supplies a fallback for paths such as CTAS; the actual
180        // execution root replaces it before terminal publication so a replan
181        // never projects into an unexecuted wrapper.
182        *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/// The attempt an [`Invocation`] belongs to, as planning and execution see it
212/// through the invocation's config extension.
213#[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    /// The correlation id selected by the attempt owner, if the caller
224    /// supplied one. This is planning metadata only.
225    pub(crate) fn query_id(&self) -> Option<&AttemptQueryId> {
226        self.state.query_id.as_ref()
227    }
228
229    /// The attempt's cancellation token observed by native work of this invocation.
230    pub(crate) fn cancellation(&self) -> &AttemptCancellation {
231        &self.state.cancellation
232    }
233
234    /// Engine-minted execution identity for this invocation. Graph execution
235    /// services use it only to link an intentional non-physical follower to
236    /// its physical leader in the sealed diagnostic projection.
237    #[cfg(feature = "cugraph")]
238    pub(crate) fn attempt_id(&self) -> AttemptId {
239        self.state.handle.attempt_id()
240    }
241
242    /// Create the opaque native-admission binding for this invocation.
243    ///
244    /// Only adapter-owned planner code can construct this binding. In
245    /// particular, the extension never exposes the raw `AttemptHandle` or
246    /// observation-seal authority to DataFusion extensions.
247    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    /// Records the native physical plan that first needs admission.
258    ///
259    /// CTAS may execute that plan from `execute_logical_plan` before the
260    /// adapter receives the outer DataFrame's physical plan. Establish the
261    /// attempt's final-plan disposition at ticket construction. The later
262    /// query-service grant then always has a valid lifecycle predecessor.
263    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
273/// Owner of one adapter invocation attempt.
274///
275/// One attempt executes one invocation into one result stream. The
276/// terminalization authority moves into that stream when execution begins;
277/// until then the attempt itself commits planning outcomes. A second
278/// [`sql`](Self::sql), [`execute_dataframe`](Self::execute_dataframe), or
279/// [`execute_physical_plan`](Self::execute_physical_plan) call finds no
280/// authority left and returns an error without executing again.
281pub struct AlgeonAttempt {
282    /// `Some` while this object still holds the terminalization authority.
283    /// `take_owner` moves it into the result stream; afterwards every
284    /// `commit*` method here is a no-op.
285    owner: Mutex<Option<AttemptOwner>>,
286    state: Arc<AttemptExecutionState>,
287    /// The invocation every plan of this attempt is planned and executed
288    /// under. Its config extension is how planner rules and execution nodes
289    /// reach the attempt without a reference to this object.
290    invocation: Invocation,
291    /// Creates counted completion receivers. Because the attempt holds only
292    /// the factory without an active receiver, uncollected reports are routed
293    /// to the abandoned-report queue.
294    completion_factory: AttemptCompletionFactory,
295    /// Read by the backend close check. Holding it keeps no GPU resource
296    /// alive.
297    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    /// Begins an attempt after validating opaque caller correlation fields.
308    pub fn begin_attempt(&self, options: AttemptOptions) -> Result<AlgeonAttempt> {
309        self.begin_attempt_with_cap(options, None)
310    }
311
312    /// Begins an attempt that requests an explicit immutable query memory cap.
313    ///
314    /// The specified cap passes via the attempt-bound admission ticket into
315    /// query-service admission. If available memory cannot accommodate the cap,
316    /// admission rejects the request without downsizing.
317    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    /// Removes and returns up to `max` reports whose completion receivers were
330    /// dropped before publication. Removed reports are cleared from the queue; the
331    /// backend retains no copy.
332    pub fn take_abandoned_attempt_reports(&self, max: usize) -> Vec<Arc<QueryAttemptReport>> {
333        self.session_capability().take_abandoned_attempt_reports(max)
334    }
335
336    /// Reads backend-wide attempt lifecycle counters without removing retained reports.
337    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        // The attempt object holds the minting factory and no receiver, so
374        // only caller-held receivers count toward the abandoned-report
375        // decision.
376        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    /// Returns this invocation's runtime-assigned identity, stable through terminal publication.
391    pub fn attempt_id(&self) -> AttemptId {
392        self.state.handle.attempt_id()
393    }
394
395    /// Returns an independent counted receiver for this attempt's immutable terminal report.
396    ///
397    /// Each call mints a distinct counted receiver. Receivers do not hold GPU
398    /// leases or delay resource cleanup. To observe terminal reports reliably across
399    /// all outcomes (planning failures, cancellation, stream errors, or normal EOF),
400    /// callers should obtain a receiver before planning starts and retain it until publication.
401    ///
402    /// When terminal publication occurs with zero active counted receivers, the sealed
403    /// report is routed best-effort to the backend's configured abandoned-report queue.
404    /// Receivers created after publication can still read the immutable report, but
405    /// do not retrieve an already-routed abandoned report.
406    pub fn completion(&self) -> AttemptCompletion {
407        self.completion_factory.completion()
408    }
409
410    /// Returns the immutable graph claim-versus-peak snapshot after this
411    /// attempt has sealed a native execution root.
412    ///
413    /// Returns `None` if the attempt has not sealed or if no native root
414    /// was elected. The snapshot is diagnostic only and cannot mutate
415    /// engine admission, ledger, or allocator state.
416    pub fn sealed_graph_claim_calibration_snapshot(&self) -> Option<SealedAttemptGraphClaimCalibrationSnapshot> {
417        self.state.sealed_graph_claim_calibration_snapshot()
418    }
419
420    /// The native-plan coverage and execution metrics the elected root sealed
421    /// with the report. `None` until the attempt seals, or when no native root
422    /// was elected.
423    pub fn sealed_native_execution_snapshot(&self) -> Option<SealedAttemptNativeExecutionSnapshot> {
424        self.state.sealed_native_execution_snapshot()
425    }
426
427    /// Clones the attempt's cancellation token, including its configured deadline.
428    ///
429    /// Requesting cancellation stops further native submission at execution boundaries;
430    /// it does not interrupt a CUDA operation already submitted.
431    pub fn cancellation(&self) -> AttemptCancellation {
432        self.state.cancellation.clone()
433    }
434
435    /// Native planning diagnostics of this attempt's latest planning pass.
436    ///
437    /// `None` means the attempt has not planned, or planned on a session
438    /// without the native optimizer rule.
439    pub fn planning_diagnostics(&self) -> Option<NativePlanningDiagnostics> {
440        self.invocation.planning_diagnostics()
441    }
442
443    /// Registers a read-only observer of the immutable report produced at the
444    /// attempt's sole observation seal point.
445    ///
446    /// The observer has no terminalization authority. The attempt owner
447    /// publishes the report before it delivers its sealed observation. The
448    /// callback can thus retain the exact completed report together with the
449    /// snapshot that embeds it, without a detached completion task.
450    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    /// Like [`Self::register_sealed_report_observer`], and also passes the native
456    /// execution snapshot to the observer. It is sealed by the report sink, which
457    /// runs before the owner delivers the sealed observation.
458    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                // `AttemptOwner::finish_with_footer` publishes the report
472                // before the owner exposes this sealed snapshot. If that engine
473                // invariant is ever violated, the observer receives `None` for
474                // the report, the coordinator stays fail-closed, and no
475                // fabricated projection is retained. The check runs in every
476                // build and logs an error.
477                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    /// Commits through the still-owned terminalization authority. Once the
484    /// authority moved into a result stream this is a no-op; the stream is the
485    /// only remaining committer, and the runtime refuses a second terminal
486    /// commit regardless.
487    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    /// Moves the terminalization authority into the result stream. An attempt
502    /// executes exactly one invocation.
503    ///
504    /// `result_stream_started` is stored after the take and never cleared.
505    /// `start_stream` sets the metrics target after this call. A planning
506    /// pass that observes the flag unset in between therefore cannot leave
507    /// its target in place.
508    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    /// Rejects planning when the backend began closing after attempt creation.
541    /// The attempt records an `unplanned` terminal outcome with `backend_closing`
542    /// failure evidence and produces no plan.
543    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        // Close and caller cancellation are independent typed causes.
551        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    /// Plans SQL in a fresh invocation-local session state.
559    ///
560    /// DataFusion executes a CTAS input while planning the statement; that
561    /// execution admits under this attempt and shares the grant with the
562    /// statement's result, which [`Self::execute_physical_plan`] runs.
563    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    /// Plans and executes SQL in this attempt's single invocation.
578    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    /// Plans and executes an existing `DataFrame` in this attempt's single
587    /// invocation. The frame's logical plan is replanned inside the
588    /// invocation-local session state so native fragments bind this attempt's
589    /// admission handle.
590    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    /// Executes an already-optimized physical plan in this attempt's single
601    /// invocation.
602    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    /// Fails, after committing the matching terminal outcome, when the attempt
616    /// is cancelled or past its deadline, or when the backend is closing.
617    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    /// Runs `planning` in this attempt's invocation and records the planning
625    /// boundary and the final plan's disposition. A planning error or panic
626    /// commits the attempt's terminal outcome.
627    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        // A panic inside DataFusion planning must still seal the report with
635        // `Panicked`; the payload is re-raised afterwards so the caller sees
636        // the original unwind.
637        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        // A cancellation requested during planning fails the attempt here,
649        // before a stream could start under a cancelled token.
650        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    /// Moves the terminalization authority into the one stream that executes
659    /// `plan`, then elects the plan's native root for the sealed metrics.
660    fn start_stream(
661        &self,
662        plan: Arc<dyn ExecutionPlan>,
663        task_context: Arc<TaskContext>,
664    ) -> DataFusionResult<AttemptStream> {
665        // Taking the owner first marks the stream as started, so the metrics
666        // target set next is the one the executing plan elects and no later
667        // planning pass replaces it.
668        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    /// Binds the invocation's admission ticket to `task_context` when `plan`
681    /// admits native work. For a plan planned outside this attempt, the
682    /// source and evidence its admission node holds mint the ticket.
683    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
697/// Runs `execute` as one internal attempt begun on `session`, for a native
698/// admission root executed without an attempt of its own (a plain DataFusion
699/// `collect`). `execute` receives a task context carrying the attempt's
700/// invocation; the returned stream commits the attempt's terminal outcome on
701/// end of stream, error, drop, or panic.
702pub(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/// Begins a native attempt on `source`'s backend and mints its invocation's
735/// ticket from `evidence`, for tests that drive a ticket without planning.
736/// The ticket admits at most once, under the returned attempt.
737#[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/// [`attempt_ticket_for_test`] for an attempt begun with `options`.
746#[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    // `NativeAdmissionExec` is usually the physical root, but DDL plans can
763    // execute it below a DataFusion operator, and a GPU function occurrence
764    // can wrap it. The invocation task context must carry the same attempt
765    // ticket before any of these shapes starts execution.
766    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
787/// Captures a returned DataFusion error by reference before reducing it to a
788/// closed terminal outcome. Only native errors contribute a bounded message;
789/// adapter-only errors use the deliberately text-free fallback.
790fn 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
802/// Commits an effective cancellation or deadline as the attempt's terminal
803/// error, so the report keeps its bounded reason, and returns that error.
804/// `None` means the attempt is neither cancelled nor past its deadline.
805fn 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
815/// Reduces an adapter error to its terminal commit: a `Cancelled` code commits
816/// `Cancelled`, a native source commits that error so its bounded reason
817/// reaches the report, and anything else commits the text-free fallback.
818fn 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
828/// Result stream that commits the attempt at EOF, error, cancellation, drop, or panic.
829///
830/// The stream owns the attempt's terminalization authority: success is terminal
831/// at EOF, while errors, drop, or panic commit terminal failure. Dropping the
832/// stream requests cooperative cancellation without blocking the executor thread.
833pub struct AttemptStream {
834    /// Kept only until the wrapper commits its terminal outcome. Dropping it
835    /// releases no grant: the attempt's terminal commit does that.
836    inner: Option<SendableRecordBatchStream>,
837    schema: SchemaRef,
838    /// The moved terminalization authority; this stream is the attempt's only
839    /// committer from here on.
840    owner: AttemptOwner,
841    state: Arc<AttemptExecutionState>,
842    physical_start_recorded: bool,
843    /// Set before the terminal commit on every path. A terminal stream yields
844    /// `Ready(None)` on later polls.
845    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    /// Returns the immutable graph claim-versus-peak snapshot once the owning
855    /// attempt has sealed. This is the same projection exposed by
856    /// [`AlgeonAttempt::sealed_graph_claim_calibration_snapshot`].
857    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    /// Commits the outer terminal result before destroying the physical
870    /// stream. The attempt's outcome is then the root stream's result, and the
871    /// teardown of its inputs does not reach a terminal state first.
872    fn finish_terminal(&mut self, outcome: RuntimeTerminalOutcome) {
873        self.terminal = true;
874        self.commit(outcome);
875        // Free the physical stream at the terminal commit, because this wrapper can outlive it.
876        drop(self.inner.take());
877    }
878
879    /// Finalizes an effective cancellation through its typed native error so
880    /// deadline expiry retains its bounded reason in the immutable report.
881    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        // Release the physical stream now so its resident result and host
889        // reservation do not wait for the caller to drop this wrapper.
890        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            // The cancellation error is the stream's final item. After `commit`
924            // the handle is terminal and cannot guard a later poll; the
925            // `terminal` flag does.
926            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            // CPU-only plans begin physical execution at first poll. Native
932            // plans record this boundary at admission, and a queued or rejected
933            // admission leaves it absent.
934            if this.state.handle.disposition() == Some(AttemptDisposition::CpuFallback) {
935                let _ = this.state.handle.record_physical_execution_started();
936            }
937        }
938        // A panic while polling the physical stream commits `Panicked` before
939        // the unwind continues to the caller.
940        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                // Release the physical stream now so its resident result and
956                // host reservation do not wait for the caller to drop this
957                // wrapper.
958                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            // Flight's unwind guard cancels the shared token while it records
970            // the coordinator-side panic. The immutable attempt report keeps
971            // the primary unwind outcome, and that cleanup cancellation does
972            // not downgrade it to `Cancelled`.
973            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                // The caller discarded the stream without requesting
979                // cancellation. Cancel so native work still running stops at
980                // its next boundary, and record `Dropped` as the cause.
981                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/// Server hooks for attempt phases that run before a result stream or without one.
995///
996/// An attempt commits one terminal outcome. While [`AlgeonAttempt`] still
997/// holds its terminalization authority, the committing hooks below commit
998/// through it; once that authority has moved into a result stream they
999/// commit nothing and the stream acts as committer. The attempt owner
1000/// rejects subsequent terminal commits with `DoubleTerminalization`, which these
1001/// hooks ignore to preserve the initial outcome.
1002#[cfg(feature = "server")]
1003impl AlgeonAttempt {
1004    /// Records that planning started and returns a session context bound to
1005    /// this attempt, whose planning copies `execution` into every GPU node.
1006    ///
1007    /// Fails, after committing the matching terminal outcome, when the
1008    /// attempt is already cancelled or past its deadline, or when the backend
1009    /// is closing.
1010    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    /// Commits the terminal outcome for a DataFusion planning error.
1022    ///
1023    /// An effective cancellation or deadline takes precedence over `error`.
1024    /// Otherwise an Algeon error carried by `error` is committed as by
1025    /// [`Self::planning_failed_adapter`], and any other error commits a
1026    /// text-free DataFusion fallback summary.
1027    pub(crate) fn planning_failed(&self, error: &DataFusionError) {
1028        self.commit_datafusion_error(error);
1029    }
1030
1031    /// Commits the terminal outcome for an Algeon DataFusion adapter planning
1032    /// error.
1033    ///
1034    /// An effective cancellation or deadline takes precedence over `error`.
1035    /// Otherwise a `Cancelled` error commits `Cancelled`, an error whose source
1036    /// is a native query error commits that error, and any other error commits
1037    /// a text-free DataFusion fallback summary.
1038    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    /// Records the final physical plan's execution disposition and the end of
1053    /// planning without committing a terminal outcome.
1054    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    /// Cancels the attempt's cancellation token and commits `Cancelled`.
1060    pub(crate) fn cancel_before_stream(&self) {
1061        self.state.cancellation.cancel();
1062        self.commit(RuntimeTerminalOutcome::Cancelled);
1063    }
1064
1065    /// Commits `Succeeded` for a request that finishes without a result
1066    /// stream, such as DDL. A CTAS input that ran during planning shares the
1067    /// attempt's grant, which the attempt releases once this outcome commits.
1068    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}