Skip to content

feat: share the session RuntimeEnv across the FFI boundary - #24733

Draft
timsaucer wants to merge 2 commits into
apache:mainfrom
timsaucer:ffi-runtime-env
Draft

feat: share the session RuntimeEnv across the FFI boundary#24733
timsaucer wants to merge 2 commits into
apache:mainfrom
timsaucer:ffi-runtime-env

Conversation

@timsaucer

@timsaucer timsaucer commented Aug 27, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Rationale for this change

A table provider shared over FFI is asked to scan by a session in another library, and the ExecutionPlan it returns is later executed with a TaskContext produced by that same session. Providers backed by remote storage build their own ObjectStore and register it on the session during scan, then read through it at execution time.

Both ForeignSession::runtime_env and the FFI_TaskContext conversion built a default RuntimeEnv. Planning and execution happen on opposite sides of the boundary, so the registration was never visible where it was needed and the scan failed with:

No suitable object store found for s3://bucket/. See `RuntimeEnv::register_object_store`.

Registering the store on the consumer's session instead did not help, because the plan executing on the provider's side reconstructed its own default environment.

The same gap meant foreign plans allocated from a fresh UnboundedMemoryPool, so datafusion.execution.memory_limit was silently ignored for them and their allocations were invisible to the session's accounting.

What changes are included in this PR?

RuntimeEnv is a plain struct whose field list depends on enabled features (parquet_encryption adds one), so passing an Arc<RuntimeEnv> as an opaque pointer would be undefined behaviour between libraries built with different feature sets. Each component crosses on its own terms instead:

Component How it crosses
ObjectStoreRegistry Shared, via FFI_ObjectStoreRegistry
MemoryPool Shared, via FFI_MemoryPool
DiskManager Configuration copied; each side keeps its own
CacheManager Configuration copied; each side keeps its own

New in datafusion_ffi::execution:

  • FFI_RuntimeEnv / FFI_RuntimeConfig
  • FFI_MemoryPool / ForeignMemoryPool / FFI_TryGrowResult
  • FFI_ObjectStore / FFI_ObjectStoreRegistry and their Foreign* wrappers, covering every required ObjectStore method plus the get_ranges and list_with_offset overrides

A few details worth a reviewer's attention:

  • Local fast path. A provider that registers its own store and then reads through it does not pay for any of this. The store round trips through the foreign registry but FFI_ObjectStore::as_local recovers the original Arc<dyn ObjectStore>, so no data crosses the boundary. Making that work needs a side table keyed on the wrapper's address, because ObjectStore has no Any supertrait and a dyn ObjectStore cannot be tested for a concrete type. A one-line upstream change to object_store would replace it with a downcast; I plan to propose that separately.
  • Error variants are preserved, not flattened to a message. Optimistic concurrency control built on AlreadyExists and conditional reads built on NotModified / Precondition depend on the discriminant, and delta-style commit protocols would silently lose their concurrency control otherwise. ResourcesExhausted is preserved for the same reason: a dozen spilling operators match on it to decide whether to spill rather than fail the query.
  • Byte transfer is zero-copy in both directions via Bytes::from_owner.
  • GetResultPayload::File is not forwarded. A raw file descriptor is not portable, so payloads always cross as a byte stream.
  • Extensions are dropped on all options structs. They are TypeId-keyed and TypeId is not stable across separately compiled libraries, so the contents cannot be interpreted on the far side even in principle.

Are these changes tested?

Yes. 212 tests pass.

The important one is test_object_store_crosses_ffi_boundary in datafusion/ffi/tests/ffi_integration.rs, which is a genuine cross-library test: the table provider is compiled into the cdylib and loaded with libloading, so the two sides have distinct library markers. It registers an object store on the session during scan and reads it back during execute, and also reports the memory pool limit it observes so the host can assert its limit reached the foreign plan.

I verified that test actually catches the bug by reverting each fix independently. Both are load bearing, and each reproduces the reported error:

No suitable object store found for ffitest://ffi-object-store/.
See `RuntimeEnv::register_object_store`.

Unit tests cover error-variant round trips for every object_store::Error variant, PutMode::Create and copy_if_not_exists surfacing AlreadyExists through the wrapper, memory limits being enforced and reservations released across the boundary, and the local fast path returning the original store.

Are there any user-facing changes?

Yes, including breaking changes to public APIs. Documented in the 56.0.0 upgrade guide:

  • FFI_TaskContext gained a runtime_env field, changing its ABI. All FFI providers and consumers must be rebuilt together.
  • impl From<Arc<TaskContext>> for FFI_TaskContext is removed. It could not supply a tokio runtime handle, producing a context whose object stores could not be polled from a foreign executor. Use FFI_TaskContext::new.
  • datafusion-ffi now depends on object_store directly and exposes its types, so providers and consumers must agree on the object_store version too.

Behavioural changes worth calling out: plans shared over FFI are now bounded by the session's memory limit and may return ResourcesExhausted where they previously did not, and DiskManager is not shared, so spilling can use up to twice max_temp_directory_size across the two sides.

A `Session` shared over FFI now carries its `RuntimeEnv`, so a table
provider shared over FFI reaches the object stores and memory budget of
the session executing it.

`ForeignSession::runtime_env` and the `FFI_TaskContext` conversion both
built a default `RuntimeEnv`. A provider that registered its object
store on the session during `TableProvider::scan` could not reach that
store when the resulting plan was executed, failing with "No suitable
object store found", and foreign plans ran against an unbounded memory
pool regardless of `datafusion.execution.memory_limit`.

`RuntimeEnv` is a plain struct whose field list depends on enabled
features, so it is not passed as an opaque pointer. Its components cross
individually: `ObjectStoreRegistry` and `MemoryPool` are shared as trait
objects, while `DiskManager` and `CacheManager` have their configuration
copied and each side builds its own.

A store used within the library that created it stays on the local fast
path: it round trips through the foreign registry but is recovered as
the original `Arc<dyn ObjectStore>`, so reads never cross the boundary.

Object store error variants are preserved across the boundary rather
than flattened to a message, so optimistic concurrency control built on
`AlreadyExists` and conditional reads built on `NotModified` and
`Precondition` keep working. `ResourcesExhausted` is likewise preserved
so spilling operators still spill instead of failing the query.

Breaking changes:

* `FFI_TaskContext` gained a `runtime_env` field, changing its ABI.
* `impl From<Arc<TaskContext>> for FFI_TaskContext` is removed; it could
  not supply a tokio runtime handle. Use `FFI_TaskContext::new`.

Part of apache#19277.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation ffi Changes to the ffi crate labels Aug 27, 2026
@timsaucer timsaucer added the api change Changes the API exposed to users of the crate label Aug 27, 2026
@timsaucer timsaucer self-assigned this Aug 27, 2026
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion-ffi v55.0.0 (current)
       Built [  48.171s] (current)
     Parsing datafusion-ffi v55.0.0 (current)
      Parsed [   0.057s] (current)
    Building datafusion-ffi v55.0.0 (baseline)
       Built [  41.444s] (baseline)
     Parsing datafusion-ffi v55.0.0 (baseline)
      Parsed [   0.046s] (baseline)
    Checking datafusion-ffi v55.0.0 -> v55.0.0 (no change; assume patch)
     Checked [   0.338s] 223 checks: 221 pass, 1 fail, 1 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field FFI_TaskContext.runtime_env in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution/task_ctx.rs:70
  field ForeignLibraryModule.create_object_store_table in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:146

--- warning repr_c_plain_struct_fields_reordered: struct fields reordered in repr(C) struct ---

Description:
A public repr(C) struct had its fields reordered. This can change the struct's memory layout, possibly breaking FFI use cases that depend on field position and order.
        ref: https://doc.rust-lang.org/reference/type-layout.html#reprc-structs
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/repr_c_plain_struct_fields_reordered.ron

Failed in:
  FFI_TaskContext.release moved from position 7 to 8, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution/task_ctx.rs:73
  FFI_TaskContext.private_data moved from position 8 to 9, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution/task_ctx.rs:77
  FFI_TaskContext.library_marker_id moved from position 9 to 10, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution/task_ctx.rs:82

     Summary semver requires new major version: 1 major and 0 minor checks failed
     Warning produced 1 major and 0 minor level warnings
    Finished [  92.633s] datafusion-ffi

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Aug 27, 2026
@codecov-commenter

codecov-commenter commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.35355% with 191 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.48%. Comparing base (2a7a1e3) to head (f509f50).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/ffi/src/execution/memory_pool.rs 88.43% 28 Missing and 6 partials ⚠️
datafusion/ffi/src/execution/object_store/mod.rs 82.22% 0 Missing and 32 partials ⚠️
datafusion/ffi/src/execution/object_store/store.rs 90.55% 27 Missing and 2 partials ⚠️
...atafusion/ffi/src/execution/object_store/stream.rs 90.90% 16 Missing and 5 partials ⚠️
datafusion/ffi/src/execution/runtime_env.rs 90.47% 9 Missing and 11 partials ⚠️
...afusion/ffi/src/execution/object_store/registry.rs 90.81% 13 Missing and 4 partials ⚠️
datafusion/ffi/src/execution/object_store/error.rs 92.30% 15 Missing and 1 partial ⚠️
datafusion/ffi/src/execution/object_store/types.rs 97.08% 11 Missing ⚠️
...fusion/ffi/src/execution/object_store/multipart.rs 95.49% 0 Missing and 5 partials ⚠️
datafusion/ffi/src/execution/task_ctx.rs 76.47% 3 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24733      +/-   ##
==========================================
+ Coverage   81.44%   81.48%   +0.03%     
==========================================
  Files        1120     1131      +11     
  Lines      401605   404340    +2735     
  Branches   401605   404340    +2735     
==========================================
+ Hits       327098   329480    +2382     
- Misses      55348    55590     +242     
- Partials    19159    19270     +111     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate auto detected api change Auto detected API change documentation Improvements or additions to documentation ffi Changes to the ffi crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants