James Willis created SPARK-59432:
------------------------------------

             Summary: Support free-threaded (no-GIL) CPython in 
pyspark-client-rust
                 Key: SPARK-59432
                 URL: https://issues.apache.org/jira/browse/SPARK-59432
             Project: Spark
          Issue Type: New Feature
          Components: Connect
    Affects Versions: connect-rust-4.2.0
            Reporter: James Willis


h2. Background

{{pyspark-client-rust}} is a PyO3 extension. On free-threaded CPython it does 
not currently work GIL-off, for two independent reasons:
 # The {{#[pymodule]}} in {{crates/pyspark-rs/src/lib.rs}} does not declare 
{{{}gil_used = false{}}}. CPython re-enables the GIL process-wide when it 
imports an extension module that has not opted in, so even on a free-threaded 
interpreter the GIL is silently switched back on.
 # The published wheels are {{{}cp39-abi3{}}}. The stable ABI (abi3) and the 
free-threaded ABI are mutually exclusive, so no wheel is selectable on a 
free-threaded interpreter at all.

h2. Why this matters

A pure-Rust (tonic) Spark Connect client is uniquely able to run with the GIL 
disabled: unlike the classic PySpark client, it pulls in no {{{}grpcio{}}}, 
which otherwise re-enables the GIL on import (grpc/grpc#38762). Free-threading 
lets a Python application drive Spark Connect with real thread parallelism on 
the client side.
h2. Proposed change
 # Declare {{#[pymodule(gil_used = false)]}} on {{_pyspark 
}}({{{}crates/pyspark-rs/src/lib.rs{}}}). This is inert on GIL-enabled builds, 
so it is safe to ship on its own.
 # Add a free-threaded wheel build leg (cp313t / cp314t) to 
{{{}release.yml{}}}, in addition to the existing abi3 leg.
 # Add a smoke test that imports {{pyspark_client_rust}} on a free-threaded 
interpreter and asserts {{{}sys._is_gil_enabled() is False{}}}.

h2. Safety

The crate is already thread-safe, so {{gil_used = false}} is a truthful 
assertion:
 * The global tokio runtime is a {{OnceLock<Runtime>}} (multi-threaded), and 
every blocking call is wrapped in {{py.detach(|| block_on(...))}} (GIL released 
while blocking).
 * All process-global state is behind {{OnceLock}} / {{Mutex}} / atomics (e.g. 
the active-session {{{}OnceLock<Mutex<Option<SparkSession>>>{}}}).
 * Every {{#[pyclass]}} is {{Sync}} (PyO3's free-threaded requirement, enforced 
at compile time): the core {{SparkSession}} is built from {{Arc}} / 
{{Arc<Mutex<..>> }}/ atomics; stored {{Py<T>}} handles are {{{}Send + Sync{}}}; 
the response stream's {{tonic::Streaming}} is made {{Sync}} via 
{{{}sync_wrapper::SyncWrapper{}}}.
 * No {{Rc}} / {{RefCell}} / {{Cell}} / {{static mut}} anywhere in the 
extension.

Behavioral note (not a soundness issue): under free-threading, two Python 
threads invoking a {{&mut self}} method on the *same* object instance get a 
{{RuntimeError: Already borrowed}} rather than corrupting state. That surface 
is small (consume-once builders, {{{}ResponseStream::_{_}next{_}_{}}}) and this 
is the documented free-threaded contract.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to