| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| //! Support for exercising the `TaskContext` a codec is handed when it decodes. | |
| //! | |
| //! A codec exported over FFI carries a `TaskContextProvider`, and the decode | |
| //! callbacks in `datafusion-ffi` resolve it to a `TaskContext` before calling | |
| //! into the codec. Nothing in the example codecs read anything out of that | |
| //! context, so which session it belongs to was untestable: the token | |
| //! registries they use are keyed by an integer and ignore the registry. | |
| //! | |
| //! The codecs can now be asked to resolve a named scalar function from the | |
| //! context they are given on every decode, which makes the answer observable. | |
| //! Because the codecs take their provider from the session they are installed | |
| //! on, a function registered on the host with `register_udf` resolves. | |
| use std::sync::Mutex; | |
| use std::sync::atomic::{AtomicUsize, Ordering}; | |
| use datafusion::execution::TaskContext; | |
| use datafusion_common::error::Result as DataFusionResult; | |
| use datafusion_common::plan_err; | |
| /// What the codecs record about the `TaskContext` their decode callbacks run | |
| /// against. | |
| #[derive(Debug, Default)] | |
| pub(crate) struct TaskContextProbe { | |
| resolutions: AtomicUsize, | |
| last_session_id: Mutex<Option<String>>, | |
| } | |
| impl TaskContextProbe { | |
| /// Successful `require_udf_on_decode` lookups since construction. | |
| pub(crate) fn resolutions(&self) -> usize { | |
| self.resolutions.load(Ordering::SeqCst) | |
| } | |
| /// Session id of the most recent decode callback, or `None` if the codec | |
| /// has not been asked to decode anything yet. | |
| /// | |
| /// Recorded on every decode, so a test can tell *which* session the | |
| /// callback was bound to rather than only that some session resolved a | |
| /// name. A `SessionContext` that derives a fork must keep reporting the id | |
| /// it reports from `session_id()`. | |
| pub(crate) fn last_session_id(&self) -> Option<String> { | |
| self.last_session_id | |
| .lock() | |
| .expect("task context probe mutex poisoned") | |
| .clone() | |
| } | |
| } | |
| /// Resolves `required` against `ctx`, the context the decode callback was given. | |
| /// | |
| /// Records `ctx`'s session id either way. `Ok(())` when nothing was requested. | |
| /// Otherwise the name must be present in the context's scalar function | |
| /// registry, and `probe` counts each success so a test can tell a resolved | |
| /// lookup from a skipped one. | |
| pub(crate) fn resolve_required_udf( | |
| required: Option<&str>, | |
| ctx: &TaskContext, | |
| probe: &TaskContextProbe, | |
| ) -> DataFusionResult<()> { | |
| // Unconditional: the session id is worth observing even when the caller | |
| // asked for no function. | |
| *probe | |
| .last_session_id | |
| .lock() | |
| .expect("task context probe mutex poisoned") = Some(ctx.session_id().to_string()); | |
| let Some(name) = required else { | |
| return Ok(()); | |
| }; | |
| if ctx.scalar_functions().contains_key(name) { | |
| probe.resolutions.fetch_add(1, Ordering::SeqCst); | |
| return Ok(()); | |
| } | |
| // A fresh SessionContext still carries every built-in, so report the count | |
| // rather than the whole registry. | |
| plan_err!( | |
| "datafusion-ffi-example: decode could not resolve scalar function '{name}' \ | |
| in the task context it was handed (session '{}', {} scalar functions registered)", | |
| ctx.session_id(), | |
| ctx.scalar_functions().len() | |
| ) | |
| } |