1515// specific language governing permissions and limitations
1616// under the License.
1717
18- use std:: collections:: HashMap ;
1918use 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 ;
2523use datafusion:: execution:: TaskContext ;
2624use datafusion:: logical_expr:: ScalarUDF ;
2725use datafusion:: physical_plan:: ExecutionPlan ;
28- use datafusion_ffi:: execution_plan:: ForeignExecutionPlan ;
2926use datafusion_ffi:: proto:: physical_extension_codec:: FFI_PhysicalExtensionCodec ;
3027use datafusion_proto:: physical_plan:: {
3128 DefaultPhysicalExtensionCodec , PhysicalExtensionCodec , PhysicalProtoConverterExtension ,
@@ -34,26 +31,9 @@ use datafusion_python_util::{ffi_task_context_provider_from_pycapsule, get_tokio
3431use pyo3:: prelude:: * ;
3532use pyo3:: types:: PyCapsule ;
3633
34+ use crate :: foreign_plan_workaround;
3735use 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 ) ]
5838pub ( crate ) struct PhysicalCallCounters {
5939 pub encode_udf : AtomicUsize ,
@@ -69,9 +49,9 @@ pub(crate) struct PhysicalCallCounters {
6949/// owning cdylib can restore their concrete Rust type after the plan travels
7050/// through the independent query-planner and datafusion-python libraries.
7151///
72- /// See [`execution_plans `] for the token lifecycle, which is narrower than it
73- /// looks: a decode consumes its token, so the same encoded plan cannot be
74- /// decoded twice.
52+ /// See [`crate::foreign_plan_workaround `] for the token lifecycle, which is
53+ /// narrower than it looks: a decode consumes its token, so the same encoded
54+ /// plan cannot be decoded twice.
7555struct CountingPhysicalExtensionCodec {
7656 inner : DefaultPhysicalExtensionCodec ,
7757 counters : Arc < PhysicalCallCounters > ,
@@ -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,42 +94,13 @@ impl PhysicalExtensionCodec for CountingPhysicalExtensionCodec {
12294 buf : & mut Vec < u8 > ,
12395 proto_converter : & dyn PhysicalProtoConverterExtension ,
12496 ) -> Result < ( ) > {
125- // `DataSourceExec` is this library's own node. The `ForeignExecutionPlan`
126- // arm is a workaround, not a pattern to copy, and it is load-bearing:
127- // a host physical optimizer rule that runs during a foreign planner's
128- // `create_physical_plan` -- `EnsureCooperative` always does -- hands the
129- // library back a `ForeignExecutionPlan` wrapping the host's
130- // `CooperativeExec`. That type has no reachable `try_to_proto`, so
131- // nothing can encode it natively and `FFI_QueryPlanner` must serialize
132- // the plan it returns. Claiming it here is what lets those plans
133- // round-trip at all.
134- //
135- // The cost is that this codec also claims every *other* library's
136- // nodes, since that is the type any node arrives as once it has crossed
137- // the boundary -- see `extension_codec_order`. Narrowing this to
138- // `DataSourceExec` alone makes 31 tests in
139- // `datafusion-ffi-query-planner-example` fail with the error above.
140- //
141- // A library whose planner controls its own physical optimizer rules
142- // never sees a foreign node and needs no such arm.
143- //
144- // Both halves are upstream defects, tracked together in
145- // https://github.com/apache/datafusion/issues/25152: `FFI_PlanProperties`
146- // carries no `scheduling_type`, so `EnsureCooperative` reads every
147- // foreign leaf as non-cooperative and wraps it, and the resulting
148- // `ForeignExecutionPlan` then has no way to serialize itself. Fixing
149- // either one retires this arm.
150- if node. is :: < DataSourceExec > ( ) || node. is :: < ForeignExecutionPlan > ( ) {
97+ // See [`crate::foreign_plan_workaround`] for why the
98+ // `ForeignExecutionPlan` arm exists and what retires it.
99+ if foreign_plan_workaround:: claims ( & node) {
151100 self . counters
152101 . encode_execution_plan
153102 . fetch_add ( 1 , Ordering :: SeqCst ) ;
154- let id = NEXT_EXECUTION_PLAN_ID . fetch_add ( 1 , Ordering :: SeqCst ) ;
155- execution_plans ( )
156- . lock ( )
157- . map_err ( |err| DataFusionError :: Internal ( err. to_string ( ) ) ) ?
158- . insert ( id, node) ;
159- buf. extend_from_slice ( EXECUTION_PLAN_TOKEN ) ;
160- buf. extend_from_slice ( & id. to_le_bytes ( ) ) ;
103+ foreign_plan_workaround:: park ( node, buf) ?;
161104 return Ok ( ( ) ) ;
162105 }
163106 self . inner . try_encode ( node, buf, proto_converter)
0 commit comments