Repository navigation
FFI_QueryPlanner captures its codecs by value, so a nested foreign planner keeps stale codecs #24762
Description
Is your feature request related to a problem or challenge?
This is a follow on to #24106.
FFI_QueryPlanner stores logical_codec and physical_codec by value, and create_physical_plan_with_session_runtime uses self.logical_codec / self.physical_codec both to build the FFI_SessionRef it hands out and to decode the physical plan that comes back. The codecs a planner serializes with are therefore fixed at the moment it is exported.
#24723 addressed one consequence of that: new_with_ffi_codecs now unwraps an existing ForeignQueryPlanner and replaces its codecs, so a host can rebind an installed planner when its codecs change. That rebind reaches exactly one layer, which is not enough once planners are layered.
Concrete case, from wiring this up in datafusion-python (apache/datafusion-python#1677):
- The host installs planner
P, exported by library C. While exporting,Presolves a fallback plannerQfrom library D by callingQ's own export hook, handing it the session's codecs as they are at that moment.Pkeeps the resultingArc<dyn QueryPlanner>— aForeignQueryPlanner(Q_ffi)— in its private data. - The host later installs a new extension codec, and rebuilds the installed planner through
new_with_ffi_codecs. That unwrapsForeignQueryPlanner(P_ffi)and swapsP's codecs. Q's codecs are untouched. WhenPdelegates, the hop into library D serializes with whatever codecsQwas imported with.
Neither side can repair it:
- The host cannot reach
Q. It sits behindP'screate_physical_planfunction pointer with no handle exposed.new_with_ffi_codecsunwraps one layer by construction. - Library C cannot re-derive codecs at plan time.
create_physical_planreceives&dyn Session, andSessionexposestask_ctx()but no logical or physical codec accessor, so it cannot pick up the host's current codecs. This is the same observation as the "Derive the provider or codec fromSession" alternative already listed in Add an FFI extension codec bundle #24106.
The stale codecs stay usable rather than dangling — each carries its own FFI_TaskContextProvider — so the symptom is a hop quietly serializing with an older codec rather than an error, which makes it easy to miss.
Worth noting this only arises when the fallback lives in a different library. impl From<&FFI_QueryPlanner> for Arc<dyn QueryPlanner + Send + Sync> short-circuits on a matching library_marker_id and returns Arc::clone(planner.inner()), so a same-library fallback holds no codecs at all and serializes nothing — the behavior test_ffi_query_planner_local_bypass covers. A three-library test in datafusion-python confirmed a same-library fallback produces zero additional codec traffic, which is exactly why the problem does not reproduce there.
Describe the solution you'd like
Some way for a planner to resolve the host's current codecs at plan time rather than at export time. Rough options:
- Expose the serialization environment on
Session, or onFFI_SessionRef, socreate_physical_plancan take codecs from the session it is handed and pass them down to anything it delegates to. Overlaps with Expand Session trait to parity with SessionState #23678 and with the bundle proposed in Add an FFI extension codec bundle #24106. - Make the rebind recursive: give
FFI_QueryPlannera way to hand new codecs to a planner it wraps, sonew_with_ffi_codecspropagates rather than replacing a single layer. This needs a hook on the producing side, since the wrapped planner is opaque to the consumer. - Document the limitation and state the ordering rule — install codecs before a layered planner.
The bundle in #24106 does not fix this on its own: a bundle held by value is still a snapshot taken at export time. It would make the thing that goes stale one object instead of two, which probably makes (1) or (2) easier to implement.
Describe alternatives you've considered
- Have the host re-install the outer planner after installing a codec. That re-runs the planner's export hook, which re-imports the fallback against the current codecs. It works, but it requires the fallback to be a live object rather than a previously captured capsule, requires the host to have retained the planner, and the requirement is invisible to anyone who has not hit the problem.
- Accept and document it. This is what datafusion-python does today.
Additional context
Relevant code is all in datafusion/ffi/src/query_planner.rs: FFI_QueryPlanner, new_with_ffi_codecs, create_physical_plan_with_session_runtime, and ForeignQueryPlanner.
Related: #24722 / #24723 (one-layer rebind), #24106 (FFI extension codec bundle), #23678 (expand Session to parity with SessionState).
Downstream: apache/datafusion-python#1677, which documents the constraint under "Rebinding a planner's codecs is one level deep" in docs/source/contributor-guide/ffi.md.
Activity
docs: scope the planner codec rebind to one layer
Add FFI query planner support (#1677)