This is an automated email from the ASF dual-hosted git repository.

marin-ma pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new a5859848ed [GLUTEN-12719][VL] Fix ClassCastException in 
ColumnarShuffleReader when Celeborn shuffle falls back to local 
ColumnarShuffleManager (#12721)
a5859848ed is described below

commit a5859848ed285d173314c1732518dac0495ecf46
Author: Jiaan Geng <[email protected]>
AuthorDate: Fri Aug 7 22:29:46 2026 +0800

    [GLUTEN-12719][VL] Fix ClassCastException in ColumnarShuffleReader when 
Celeborn shuffle falls back to local ColumnarShuffleManager (#12721)
---
 .../spark/shuffle/ColumnarShuffleReader.scala      | 24 +++---
 .../spark/shuffle/ColumnarShuffleReaderSuite.scala | 88 ++++++++++++++++++++++
 2 files changed, 103 insertions(+), 9 deletions(-)

diff --git 
a/backends-velox/src/main/scala/org/apache/spark/shuffle/ColumnarShuffleReader.scala
 
b/backends-velox/src/main/scala/org/apache/spark/shuffle/ColumnarShuffleReader.scala
index 77b041fd29..c22daa09a7 100644
--- 
a/backends-velox/src/main/scala/org/apache/spark/shuffle/ColumnarShuffleReader.scala
+++ 
b/backends-velox/src/main/scala/org/apache/spark/shuffle/ColumnarShuffleReader.scala
@@ -73,9 +73,15 @@ class ColumnarShuffleReader[K, C](
 
   /** Read the combined key-values for this reduce task */
   override def read(): Iterator[Product2[K, C]] = {
-    val recordIter = dep match {
-      // If the dependency is a ColumnarShuffleDependency, we use the columnar 
serializer.
-      case columnarDep: ColumnarShuffleDependency[K, _, C] =>
+    // Dispatch on the concrete serializer instance type rather than the 
dependency type.
+    // A ColumnarShuffleDependency does not always carry Gluten's 
ColumnarBatchSerializerInstance:
+    // when the CelebornShuffleManager falls back to the local 
ColumnarShuffleManager (celeborn
+    // service unavailable / fallback policy), the dependency was already 
bound to the Celeborn
+    // serializer at plan build time, so testing the dependency type would 
wrongly take the
+    // columnar path and crash with a ClassCastException.
+    val recordIter = dep.serializer.newInstance() match {
+      // Gluten's own columnar serializer deserializes all streams in one 
batch.
+      case columnarSerializer: ColumnarBatchSerializerInstance =>
         val shuffleBlockFetcherIterator =
           SparkShimLoader.getSparkShims.getShuffleBlockFetcherIterator(
             ShuffleBlockFetcherIteratorParams(
@@ -98,15 +104,17 @@ class ColumnarShuffleReader[K, C](
               readMetrics,
               fetchContinuousBlocksInBatch
             ))
-        columnarDep.serializer
-          .newInstance()
-          .asInstanceOf[ColumnarBatchSerializerInstance]
+        columnarSerializer
           .deserializeStreams(
             shuffleBlockFetcherIterator,
             shuffleBlockFetcherIterator.onComplete,
             executionMode)
           .asKeyValueIterator
-      case _ =>
+      case serializerInstance =>
+        // The dependency's serializer is not Gluten's 
ColumnarBatchSerializerInstance. This
+        // covers the CelebornShuffleManager fallback case (dependency bound 
to the Celeborn
+        // serializer) as well as row-based dependencies. Fall back to the 
per-stream path,
+        // which the standard SerializerInstance (including the Celeborn one) 
supports.
         val wrappedStreams = new ShuffleBlockFetcherIterator(
           context,
           blockManager.blockStoreClient,
@@ -128,8 +136,6 @@ class ColumnarShuffleReader[K, C](
           fetchContinuousBlocksInBatch
         ).toCompletionIterator
 
-        val serializerInstance = dep.serializer.newInstance()
-
         // Create a key/value iterator for each stream
         wrappedStreams.flatMap {
           case (blockId, wrappedStream) =>
diff --git 
a/backends-velox/src/test/scala/org/apache/spark/shuffle/ColumnarShuffleReaderSuite.scala
 
b/backends-velox/src/test/scala/org/apache/spark/shuffle/ColumnarShuffleReaderSuite.scala
new file mode 100644
index 0000000000..7209dab178
--- /dev/null
+++ 
b/backends-velox/src/test/scala/org/apache/spark/shuffle/ColumnarShuffleReaderSuite.scala
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.shuffle
+
+import org.apache.gluten.execution.CPUStageMode
+import org.apache.gluten.vectorized.{ColumnarBatchSerializerInstance, 
NativePartitioning}
+
+import org.apache.spark.{HashPartitioner, TaskContext}
+import org.apache.spark.executor.TempShuffleReadMetrics
+import org.apache.spark.serializer.{JavaSerializer, Serializer}
+import org.apache.spark.sql.execution.metric.SQLMetric
+import org.apache.spark.sql.test.SharedSparkSession
+
+/**
+ * White-box regression test for the Celeborn-to-local shuffle fallback 
ClassCastException.
+ *
+ * When [[org.apache.spark.shuffle.gluten.celeborn.CelebornShuffleManager]] 
falls back to the local
+ * [[org.apache.spark.shuffle.sort.ColumnarShuffleManager]] (Celeborn service 
unavailable or a
+ * fallback policy is applied), the read side dispatches to Gluten's local
+ * [[ColumnarShuffleReader]]. The dependency is still a 
[[ColumnarShuffleDependency]], but its
+ * serializer was bound at plan build time to the Celeborn serializer, which 
is a plain
+ * `SerializerInstance` and not a [[ColumnarBatchSerializerInstance]].
+ *
+ * Before the fix, `read()` matched on the dependency type and hard-cast the 
serializer with
+ * `asInstanceOf[ColumnarBatchSerializerInstance]`, throwing 
`ClassCastException` before any block
+ * was fetched. After the fix, `read()` matches on the serializer instance 
type and routes a
+ * non-columnar serializer through the per-stream `deserializeStream` path.
+ *
+ * The cast happens before the block-fetch iterator is constructed, so an 
empty `blocksByAddress` is
+ * enough to distinguish the two behaviors without a real Celeborn cluster, 
network, or on-disk
+ * shuffle data.
+ */
+class ColumnarShuffleReaderSuite extends SharedSparkSession {
+
+  private def newColumnarDep(
+      serializer: Serializer): ColumnarShuffleDependency[Int, Int, Int] = {
+    val rdd = spark.sparkContext.parallelize(Seq((0, 0)), 1)
+    new ColumnarShuffleDependency[Int, Int, Int](
+      rdd,
+      new HashPartitioner(1),
+      serializer = serializer,
+      nativePartitioning = new NativePartitioning("hash", 1),
+      metrics = Map.empty[String, SQLMetric]
+    )
+  }
+
+  private def readWith(serializer: Serializer): Seq[Product2[Int, Int]] = {
+    val dep = newColumnarDep(serializer)
+    val handle = new BaseShuffleHandle(dep.shuffleId, dep)
+    val reader = new ColumnarShuffleReader[Int, Int](
+      handle,
+      // Empty input: the serializer cast happens before any block is fetched, 
so no real
+      // shuffle data is needed to exercise the dispatch decision.
+      Iterator.empty,
+      TaskContext.empty(),
+      new TempShuffleReadMetrics(),
+      CPUStageMode
+    )
+    reader.read().toSeq
+  }
+
+  test(
+    "read() tolerates a non-ColumnarBatchSerializerInstance on a 
ColumnarShuffleDependency " +
+      "(Celeborn -> local fallback) instead of throwing ClassCastException") {
+    // A plain row-based serializer stands in for the Celeborn serializer left 
on the
+    // dependency after CelebornShuffleManager falls back to the local 
ColumnarShuffleManager.
+    val nonColumnarSerializer = new JavaSerializer(spark.sparkContext.getConf)
+    
assert(!nonColumnarSerializer.newInstance().isInstanceOf[ColumnarBatchSerializerInstance])
+
+    // Must not throw ClassCastException; empty input yields no rows.
+    val rows = readWith(nonColumnarSerializer)
+    assert(rows.isEmpty)
+  }
+}


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

Reply via email to