andygrove opened a new pull request, #5282:
URL: https://github.com/apache/datafusion-comet/pull/5282

   ## Which issue does this PR close?
   
   Closes #5281.
   
   ## Rationale for this change
   
   A `ScalaUDF` (or Java UDF) whose closure was captured by a class from a user 
jar (`--jars` / `spark.jars`) fails at execution time:
   
   ```
   java.lang.ClassCastException: cannot assign instance of 
java.lang.invoke.SerializedLambda to field
     org.apache.spark.sql.catalyst.expressions.ScalaUDF.f of type 
scala.Function1
        ...
        at 
org.apache.comet.udf.codegen.CometScalaUDFCodegen.lookupOrCompile(CometScalaUDFCodegen.scala:160)
        at 
org.apache.comet.udf.CometUdfBridge.evaluateInternal(CometUdfBridge.java:203)
   ```
   
   That `ClassCastException` is a **masked `ClassNotFoundException`**. When the 
deserializing classloader cannot resolve a lambda's *capturing class*, 
`ObjectInputStream` records the CNFE against the object handle, therefore skips 
`SerializedLambda.readResolve()`, and the raw `SerializedLambda` then fails the 
field-type check in `defaultCheckFieldValues`. The CNFE never surfaces.
   
   The classloader is wrong because of which thread runs the deserialization. 
`CometUdfBridge.evaluate` is invoked from a Tokio worker, which attaches to the 
JVM through JNI, and **an attached thread has no context classloader**. 
`lookupOrCompile` therefore fell back to `classOf[Expression].getClassLoader` — 
the loader that loaded spark-catalyst and Comet, which never contains user 
jars. Spark installs the executor's `MutableURLClassLoader` on task threads 
only.
   
   Measured by returning thread identity from inside a UDF body:
   
   | stage leaf | thread running the UDF | context classloader |
   | --- | --- | --- |
   | `CometNativeScan` | `Thread-28..32` (Tokio workers) | **null** |
   | shuffle read / join build side | `Executor task launch worker …` | 
`MutableURLClassLoader` |
   | native scan disabled | `Executor task launch worker …` | 
`MutableURLClassLoader` |
   
   So any plan whose stage leaf is a native scan (`CometNativeScan`, 
`CometIcebergNativeScan`) hits this on every UDF call. `CometUdfBridge` already 
propagates `TaskContext` across the JNI boundary for exactly this reason; the 
classloader was not propagated.
   
   This is invisible to Comet's existing test suites because they register UDFs 
from test classes already on the application classpath.
   
   ## What changes are included in this PR?
   
   The task thread's context classloader is now propagated to the Tokio worker, 
following the same path as `TaskContext`:
   
   - `CometExecIterator` captures 
`Thread.currentThread().getContextClassLoader` (it is constructed on the Spark 
task thread) and passes it to `createPlan`.
   - `Native.createPlan` gains a `classLoader` parameter; `jni_api.rs` holds it 
as a JNI global ref on `ExecutionContext` and hands it to the planner via 
`with_class_loader`.
   - `PhysicalPlanner` clones it into every `JvmScalarUdfExpr`, under the same 
`debug_assert` invariant already used for `task_context`.
   - `JvmScalarUdfExpr` passes it through the bridge call; the JNI signature 
gains `Ljava/lang/ClassLoader;`.
   - `CometUdfBridge.evaluate` installs it as the calling thread's context 
classloader for the duration of the call, restoring the prior value in 
`finally` — the same save-and-restore shape used for `TaskContext`.
   
   Installing it on the thread rather than passing it down to the deserializer 
fixes both affected lookups at once: the closure deserialization in 
`CometScalaUDFCodegen`, and the `Class.forName(udfClassName)` in 
`CometUdfBridge`, which had the same latent bug for a user-supplied `CometUDF` 
implementation shipped in a user jar. It also means user code inside a UDF that 
relies on the context classloader behaves as it does under Spark's own 
execution.
   
   One line was added to the "Behavior" section of 
`docs/source/user-guide/latest/scala_java_udfs.md`, alongside the existing 
`TaskContext.get()` guarantee.
   
   ## How are these changes tested?
   
   New suite `CometScalaUDFClassLoaderSuite`, which compiles a class holding a 
serializable `scala.Function1` lambda into a jar at test time and wires it in 
via `spark.executor.extraClassPath` — the local-mode equivalent of a `--jars` 
submission, since `LocalSchedulerBackend` feeds it into the executor's 
`MutableURLClassLoader`. Four tests:
   
   1. sanity — the class is loadable from task threads and raises 
`ClassNotFoundException` from `classOf[Expression].getClassLoader` (the old 
fallback), so the fixture reproduces the production classloader topology.
   1. the failing query: `SELECT hiddenUdf(s) FROM t` with a `CometNativeScan` 
leaf.
   1. the propagation itself: the UDF body reports whether the classloader 
installed on whatever thread invoked it can reach the user jar.
   1. control: the same UDF with the native scan disabled, which runs on the 
task thread and passed even before this change — isolating the failure to 
classloader propagation rather than class availability.
   
   The two new behavioral tests were confirmed to fail without the fix 
(reverting only the loader install), reporting `MISSING|Thread-53, 
MISSING|Thread-28, …` and naming the Tokio workers.
   
   Existing coverage run locally on Spark 4.1 / Scala 2.13: 
`CometCodegenSuite`, `CometCodegenHOFSuite`, `CometCodegenSourceSuite`, 
`CometCodegenFuzzSuite` plus the new suite (180 tests), and `CometExecSuite` 
(143 tests), which exercises the changed `createPlan` signature broadly. `cargo 
fmt --check` and `cargo clippy --all-targets -D warnings` are clean.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to