Skip to content

Commit 736c0c9

Browse files
committed
docs(example): quarantine foreign plan workaround
Move the temporary execution-plan token registry into a deliberately named module and document its process-local behavior and removal criteria. Signed-off-by: Akash Kumar <116457960+akashchamp@users.noreply.github.com>
1 parent eeaddab commit 736c0c9

3 files changed

Lines changed: 98 additions & 45 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! # NOT A PATTERN
19+
//!
20+
//! **Blocked on:** <https://github.com/apache/datafusion/issues/25152>
21+
//!
22+
//! **Delete when:** `FFI_PlanProperties` carries `scheduling_type`, or
23+
//! `ForeignExecutionPlan` gains a reachable `try_to_proto`.
24+
//!
25+
//! **Copying this will:** claim every other library's plan nodes, and produce
26+
//! payloads that decode only in the writing process, exactly once each.
27+
//!
28+
//! A provider owns `DataSourceExec`, but a host-added execution decorator can
29+
//! wrap that scan in `ForeignExecutionPlan`. This workaround claims the opaque
30+
//! wrapper because the native encoder cannot reach its underlying plan. It
31+
//! parks the plan in a process-local registry rather than serializing it.
32+
//!
33+
//! The `DataSourceExec` arm could use durable metadata and does not, because
34+
//! the registry must exist for the `ForeignExecutionPlan` arm regardless.
35+
//! Splitting the two arms across wire formats costs real code and removes
36+
//! nothing; this module keeps the temporary compromise obvious.
37+
38+
use std::collections::HashMap;
39+
use std::sync::atomic::{AtomicU64, Ordering};
40+
use std::sync::{Arc, Mutex, OnceLock};
41+
42+
use datafusion::common::{DataFusionError, Result};
43+
use datafusion::datasource::source::DataSourceExec;
44+
use datafusion::physical_plan::ExecutionPlan;
45+
use datafusion_ffi::execution_plan::ForeignExecutionPlan;
46+
47+
const EXECUTION_PLAN_TOKEN: &[u8] = b"DFPYEXEP";
48+
static NEXT_EXECUTION_PLAN_ID: AtomicU64 = AtomicU64::new(1);
49+
static EXECUTION_PLANS: OnceLock<Mutex<HashMap<u64, Arc<dyn ExecutionPlan>>>> = OnceLock::new();
50+
51+
fn execution_plans() -> &'static Mutex<HashMap<u64, Arc<dyn ExecutionPlan>>> {
52+
EXECUTION_PLANS.get_or_init(|| Mutex::new(HashMap::new()))
53+
}
54+
55+
fn token_id(buf: &[u8]) -> Option<u64> {
56+
let id: [u8; 8] = buf.strip_prefix(EXECUTION_PLAN_TOKEN)?.try_into().ok()?;
57+
Some(u64::from_le_bytes(id))
58+
}
59+
60+
pub(crate) fn claims(node: &Arc<dyn ExecutionPlan>) -> bool {
61+
node.is::<DataSourceExec>() || node.is::<ForeignExecutionPlan>()
62+
}
63+
64+
pub(crate) fn park(node: Arc<dyn ExecutionPlan>, buf: &mut Vec<u8>) -> Result<()> {
65+
let id = NEXT_EXECUTION_PLAN_ID.fetch_add(1, Ordering::SeqCst);
66+
execution_plans()
67+
.lock()
68+
.map_err(|err| DataFusionError::Internal(err.to_string()))?
69+
.insert(id, node);
70+
buf.extend_from_slice(EXECUTION_PLAN_TOKEN);
71+
buf.extend_from_slice(&id.to_le_bytes());
72+
Ok(())
73+
}
74+
75+
pub(crate) fn take(buf: &[u8]) -> Result<Option<Arc<dyn ExecutionPlan>>> {
76+
let Some(id) = token_id(buf) else {
77+
return Ok(None);
78+
};
79+
let plan = execution_plans()
80+
.lock()
81+
.map_err(|err| DataFusionError::Internal(err.to_string()))?
82+
.remove(&id)
83+
.ok_or_else(|| {
84+
DataFusionError::Internal(format!(
85+
"Unknown datafusion-ffi-example execution plan token {id}"
86+
))
87+
})?;
88+
Ok(Some(plan))
89+
}

‎examples/datafusion-ffi-example/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use crate::window_udf::MyRankUDF;
3333
pub(crate) mod aggregate_udf;
3434
pub(crate) mod catalog_provider;
3535
pub(crate) mod config;
36+
pub(crate) mod foreign_plan_workaround;
3637
pub(crate) mod logical_extension_codec;
3738
pub(crate) mod name_only_codec;
3839
pub(crate) mod physical_extension_codec;

‎examples/datafusion-ffi-example/src/physical_extension_codec.rs‎

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,14 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use std::collections::HashMap;
1918
use std::fmt;
20-
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
21-
use std::sync::{Arc, Mutex, OnceLock};
19+
use std::sync::Arc;
20+
use std::sync::atomic::{AtomicUsize, Ordering};
2221

23-
use datafusion::common::{DataFusionError, Result};
24-
use datafusion::datasource::source::DataSourceExec;
22+
use datafusion::common::Result;
2523
use datafusion::execution::TaskContext;
2624
use datafusion::logical_expr::ScalarUDF;
2725
use datafusion::physical_plan::ExecutionPlan;
28-
use datafusion_ffi::execution_plan::ForeignExecutionPlan;
2926
use datafusion_ffi::proto::physical_extension_codec::FFI_PhysicalExtensionCodec;
3027
use datafusion_proto::physical_plan::{
3128
DefaultPhysicalExtensionCodec, PhysicalExtensionCodec, PhysicalProtoConverterExtension,
@@ -34,26 +31,9 @@ use datafusion_python_util::{ffi_task_context_provider_from_pycapsule, get_tokio
3431
use pyo3::prelude::*;
3532
use pyo3::types::PyCapsule;
3633

34+
use crate::foreign_plan_workaround;
3735
use crate::required_udf::{TaskContextProbe, resolve_required_udf};
3836

39-
const EXECUTION_PLAN_TOKEN: &[u8] = b"DFPYEXEP";
40-
static NEXT_EXECUTION_PLAN_ID: AtomicU64 = AtomicU64::new(1);
41-
static EXECUTION_PLANS: OnceLock<Mutex<HashMap<u64, Arc<dyn ExecutionPlan>>>> = OnceLock::new();
42-
43-
/// Execution-plan counterpart of the logical codec's provider registry, with
44-
/// the same lifecycle: encoding inserts, decoding removes, so a decode
45-
/// consumes its token and an encode that is never decoded leaks. See
46-
/// [`crate::logical_extension_codec`] for why that is acceptable here and not
47-
/// in a real codec.
48-
fn execution_plans() -> &'static Mutex<HashMap<u64, Arc<dyn ExecutionPlan>>> {
49-
EXECUTION_PLANS.get_or_init(|| Mutex::new(HashMap::new()))
50-
}
51-
52-
fn token_id(buf: &[u8]) -> Option<u64> {
53-
let id: [u8; 8] = buf.strip_prefix(EXECUTION_PLAN_TOKEN)?.try_into().ok()?;
54-
Some(u64::from_le_bytes(id))
55-
}
56-
5737
#[derive(Debug, Default)]
5838
pub(crate) struct PhysicalCallCounters {
5939
pub encode_udf: AtomicUsize,
@@ -99,19 +79,11 @@ impl PhysicalExtensionCodec for CountingPhysicalExtensionCodec {
9979
proto_converter: &dyn PhysicalProtoConverterExtension,
10080
) -> Result<Arc<dyn ExecutionPlan>> {
10181
resolve_required_udf(self.required_udf.as_deref(), ctx, &self.counters.task_ctx)?;
102-
if let Some(id) = token_id(buf) {
82+
if let Some(plan) = foreign_plan_workaround::take(buf)? {
10383
self.counters
10484
.decode_execution_plan
10585
.fetch_add(1, Ordering::SeqCst);
106-
return execution_plans()
107-
.lock()
108-
.map_err(|err| DataFusionError::Internal(err.to_string()))?
109-
.remove(&id)
110-
.ok_or_else(|| {
111-
DataFusionError::Internal(format!(
112-
"Unknown datafusion-ffi-example execution plan token {id}"
113-
))
114-
});
86+
return Ok(plan);
11587
}
11688
self.inner.try_decode(buf, inputs, ctx, proto_converter)
11789
}
@@ -122,20 +94,11 @@ impl PhysicalExtensionCodec for CountingPhysicalExtensionCodec {
12294
buf: &mut Vec<u8>,
12395
proto_converter: &dyn PhysicalProtoConverterExtension,
12496
) -> Result<()> {
125-
// The provider owns DataSourceExec. A ForeignExecutionPlan can wrap a
126-
// host-added execution decorator around that scan; retaining the opaque
127-
// wrapper preserves its original library identity without downcasting it.
128-
if node.is::<DataSourceExec>() || node.is::<ForeignExecutionPlan>() {
97+
if foreign_plan_workaround::claims(&node) {
12998
self.counters
13099
.encode_execution_plan
131100
.fetch_add(1, Ordering::SeqCst);
132-
let id = NEXT_EXECUTION_PLAN_ID.fetch_add(1, Ordering::SeqCst);
133-
execution_plans()
134-
.lock()
135-
.map_err(|err| DataFusionError::Internal(err.to_string()))?
136-
.insert(id, node);
137-
buf.extend_from_slice(EXECUTION_PLAN_TOKEN);
138-
buf.extend_from_slice(&id.to_le_bytes());
101+
foreign_plan_workaround::park(node, buf)?;
139102
return Ok(());
140103
}
141104
self.inner.try_encode(node, buf, proto_converter)

0 commit comments

Comments
 (0)