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

zhouyuan 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 2496e4c083 [VL] Offload Kafka micro-batch scans (#13237)
2496e4c083 is described below

commit 2496e4c083eebdd86a5a9ecd175511b19463c87d
Author: Reema <[email protected]>
AuthorDate: Thu Nov 5 15:19:47 2026 +0300

    [VL] Offload Kafka micro-batch scans (#13237)
---
 .github/workflows/velox_kafka.yml                  |  24 ++--
 backends-velox/pom.xml                             |  19 +++
 ...org.apache.gluten.component.VeloxKafkaComponent |   0
 .../gluten/component/VeloxKafkaComponent.scala     |  37 ++++++
 .../kafka/VeloxGlutenKafkaScanSuite.scala          | 129 +++++++++++++++++++++
 .../gluten/backendsapi/velox/VeloxBackend.scala    |  19 ++-
 .../execution/VeloxKafkaScanValidationSuite.scala  |  66 +++++++++++
 7 files changed, 281 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/velox_kafka.yml 
b/.github/workflows/velox_kafka.yml
index b900636493..c4821a34a6 100644
--- a/.github/workflows/velox_kafka.yml
+++ b/.github/workflows/velox_kafka.yml
@@ -141,18 +141,18 @@ jobs:
         run: |
           ./build/mvn -B -ntp install -Pbackends-velox -Pspark-3.5 
-Pscala-2.12 \
             -Pjava-17 -Pkafka -Pspark-ut -Pfast-build -DskipTests
-      # TODO: Re-enable when VeloxKafkaScanValidationSuite is added.
-      # - name: Check Kafka schema validation without a broker
-      #   run: |
-      #     ./build/mvn -B -ntp test -pl gluten-ut/spark35 -Pbackends-velox 
-Pspark-3.5 \
-      #       -Pscala-2.12 -Pjava-17 -Pspark-ut -Pfast-build \
-      #       
-Dsuites=org.apache.gluten.execution.VeloxKafkaScanValidationSuite
-      # TODO: Re-enable when VeloxGlutenKafkaScanSuite is added in #13237.
-      # - name: Check native micro-batch reads and headers fallback
-      #   run: |
-      #     ./build/mvn -B -ntp test -pl backends-velox -Pbackends-velox 
-Pspark-3.5 \
-      #       -Pscala-2.12 -Pjava-17 -Pkafka -Pfast-build \
-      #       
-Dsuites=org.apache.gluten.execution.kafka.VeloxGlutenKafkaScanSuite
+
+      - name: Check Kafka schema validation without a broker
+        run: |
+          ./build/mvn -B -ntp test -pl gluten-ut/spark35 -Pbackends-velox 
-Pspark-3.5 \
+            -Pscala-2.12 -Pjava-17 -Pspark-ut -Pfast-build \
+            -Dsuites=org.apache.gluten.execution.VeloxKafkaScanValidationSuite
+
+      - name: Check native micro-batch reads and headers fallback
+        run: |
+          ./build/mvn -B -ntp test -pl backends-velox -Pbackends-velox 
-Pspark-3.5 \
+            -Pscala-2.12 -Pjava-17 -Pkafka -Pfast-build \
+            
-Dsuites=org.apache.gluten.execution.kafka.VeloxGlutenKafkaScanSuite
       - name: Upload test results
         if: always()
         uses: actions/upload-artifact@v7
diff --git a/backends-velox/pom.xml b/backends-velox/pom.xml
index 028dc79a44..fff0f1fdfe 100644
--- a/backends-velox/pom.xml
+++ b/backends-velox/pom.xml
@@ -878,5 +878,24 @@
         </dependency>
       </dependencies>
     </profile>
+    <profile>
+      <id>kafka</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.gluten</groupId>
+          <artifactId>gluten-kafka</artifactId>
+          <version>${project.version}</version>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.spark</groupId>
+          <artifactId>spark-sql-kafka-0-10_${scala.binary.version}</artifactId>
+          <version>${spark.version}</version>
+          <scope>provided</scope>
+        </dependency>
+      </dependencies>
+    </profile>
   </profiles>
 </project>
diff --git 
a/backends-velox/src-kafka/main/resources/META-INF/gluten-components/org.apache.gluten.component.VeloxKafkaComponent
 
b/backends-velox/src-kafka/main/resources/META-INF/gluten-components/org.apache.gluten.component.VeloxKafkaComponent
new file mode 100644
index 0000000000..e69de29bb2
diff --git 
a/backends-velox/src-kafka/main/scala/org/apache/gluten/component/VeloxKafkaComponent.scala
 
b/backends-velox/src-kafka/main/scala/org/apache/gluten/component/VeloxKafkaComponent.scala
new file mode 100644
index 0000000000..e2f46946d8
--- /dev/null
+++ 
b/backends-velox/src-kafka/main/scala/org/apache/gluten/component/VeloxKafkaComponent.scala
@@ -0,0 +1,37 @@
+/*
+ * 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.gluten.component
+
+import org.apache.gluten.backendsapi.velox.VeloxBackend
+import org.apache.gluten.execution.OffloadKafkaScan
+import org.apache.gluten.extension.injector.Injector
+
+import org.apache.spark.util.SparkReflectionUtil
+
+class VeloxKafkaComponent extends Component {
+  override def name(): String = "velox-kafka"
+
+  override def dependencies(): Seq[Class[_ <: Component]] = 
classOf[VeloxBackend] :: Nil
+
+  override def isRuntimeCompatible: Boolean = {
+    
SparkReflectionUtil.isClassPresent("org.apache.spark.sql.kafka010.KafkaSourceProvider")
+  }
+
+  override def injectRules(injector: Injector): Unit = {
+    OffloadKafkaScan.inject(injector)
+  }
+}
diff --git 
a/backends-velox/src-kafka/test/scala/org/apache/gluten/execution/kafka/VeloxGlutenKafkaScanSuite.scala
 
b/backends-velox/src-kafka/test/scala/org/apache/gluten/execution/kafka/VeloxGlutenKafkaScanSuite.scala
new file mode 100644
index 0000000000..f9f20ceaea
--- /dev/null
+++ 
b/backends-velox/src-kafka/test/scala/org/apache/gluten/execution/kafka/VeloxGlutenKafkaScanSuite.scala
@@ -0,0 +1,129 @@
+/*
+ * 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.gluten.execution.kafka
+
+import org.apache.gluten.execution.{MicroBatchScanExecTransformer, 
VeloxWholeStageTransformerSuite}
+
+import org.apache.spark.SparkConf
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.streaming.StreamingQueryWrapper
+import org.apache.spark.sql.streaming.Trigger
+
+import org.apache.kafka.clients.admin.{AdminClient, AdminClientConfig, 
NewTopic}
+
+import java.time.Duration
+import java.util.{Collections, Properties, UUID}
+import java.util.concurrent.TimeUnit
+
+/**
+ * Requires ENABLE_KAFKA in the native build and a broker at 
KAFKA_BOOTSTRAP_SERVERS (default:
+ * localhost:9092). Run with the kafka Maven profile.
+ */
+class VeloxGlutenKafkaScanSuite extends VeloxWholeStageTransformerSuite {
+  override protected val resourcePath: String = "/tpch-data-parquet"
+  override protected val fileFormat: String = "parquet"
+
+  private val kafkaBootstrapServers =
+    sys.env.getOrElse("KAFKA_BOOTSTRAP_SERVERS", "localhost:9092")
+
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf
+      .set("spark.shuffle.manager", 
"org.apache.spark.shuffle.sort.ColumnarShuffleManager")
+      .set("spark.sql.shuffle.partitions", "2")
+  }
+
+  private def withTopic(func: String => Unit): Unit = {
+    val topic = "velox_kafka_" + UUID.randomUUID().toString.replace("-", "")
+    val props = new Properties()
+    props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, 
kafkaBootstrapServers)
+    props.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, "30000")
+    val adminClient = AdminClient.create(props)
+    try {
+      adminClient
+        .createTopics(Collections.singletonList(new NewTopic(topic, 2, 
1.toShort)))
+        .all()
+        .get(30, TimeUnit.SECONDS)
+      try {
+        func(topic)
+      } finally {
+        
adminClient.deleteTopics(Collections.singletonList(topic)).all().get(30, 
TimeUnit.SECONDS)
+      }
+    } finally {
+      adminClient.close(Duration.ofSeconds(5))
+    }
+  }
+
+  private def checkStreamingRead(includeHeaders: Boolean): Unit = {
+    withTopic {
+      topic =>
+        withTempDir {
+          dir =>
+            spark
+              .range(1000)
+              .selectExpr("cast(id as string) as value")
+              .write
+              .format("kafka")
+              .option("kafka.bootstrap.servers", kafkaBootstrapServers)
+              .option("topic", topic)
+              .save()
+
+            val stream = spark.readStream
+              .format("kafka")
+              .option("kafka.bootstrap.servers", kafkaBootstrapServers)
+              .option("subscribe", topic)
+              .option("startingOffsets", "earliest")
+              .option("includeHeaders", includeHeaders.toString)
+              .load()
+            // Keep headers in the fallback query so column pruning cannot 
hide the unsupported type.
+            val result = if (includeHeaders) {
+              stream.selectExpr("cast(cast(value as string) as int) as id", 
"headers")
+            } else {
+              stream.selectExpr("cast(cast(value as string) as int) as id")
+            }
+            val streamQuery = result.writeStream
+              .format("parquet")
+              .option("checkpointLocation", dir.getCanonicalPath + 
"/checkpoint")
+              .trigger(Trigger.AvailableNow())
+              .start(dir.getCanonicalPath + "/data")
+
+            try {
+              assert(streamQuery.awaitTermination(60000), "Kafka micro-batch 
did not finish")
+              val scans = streamQuery
+                .asInstanceOf[StreamingQueryWrapper]
+                .streamingQuery
+                .lastExecution
+                .executedPlan
+                .collect { case p: MicroBatchScanExecTransformer => p }
+              assert(scans.size == (if (includeHeaders) 0 else 1))
+              checkAnswer(
+                spark.read.parquet(dir.getCanonicalPath + 
"/data").select("id"),
+                (0 until 1000).map(Row(_)))
+            } finally {
+              streamQuery.stop()
+            }
+        }
+    }
+  }
+
+  test("kafka micro-batch scan offloads and returns every record") {
+    checkStreamingRead(includeHeaders = false)
+  }
+
+  test("kafka micro-batch scan with headers falls back and returns every 
record") {
+    checkStreamingRead(includeHeaders = true)
+  }
+}
diff --git 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
index 5426471f37..2e75321156 100644
--- 
a/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
+++ 
b/backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala
@@ -27,7 +27,7 @@ import 
org.apache.gluten.extension.columnar.cost.{LegacyCoster, LongCoster, Roug
 import org.apache.gluten.extension.columnar.transition.{Convention, 
ConventionFunc}
 import org.apache.gluten.substrait.rel.LocalFilesNode
 import org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat
-import 
org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat.{DwrfReadFormat, 
OrcReadFormat, ParquetReadFormat}
+import 
org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat.{DwrfReadFormat, 
KafkaReadFormat, OrcReadFormat, ParquetReadFormat}
 import org.apache.gluten.utils._
 
 import org.apache.spark.sql.catalyst.catalog.BucketSpec
@@ -48,6 +48,8 @@ import org.apache.spark.sql.types._
 import org.apache.hadoop.conf.Configuration
 import org.apache.hadoop.fs.Path
 
+import java.util.Locale
+
 import scala.util.control.Breaks.breakable
 
 class VeloxBackend extends SubstraitBackend {
@@ -100,6 +102,17 @@ object VeloxBackendSettings extends BackendSettingsApi {
   // unit: SECONDS, default 1 day
   val GLUTEN_VELOX_BROADCAST_CACHE_EXPIRED_TIME_DEFAULT: Int = 86400
 
+  // Columns produced by the native Kafka reader. Kafka headers are not 
supported.
+  private val kafkaColumns = Map(
+    "key" -> BinaryType,
+    "value" -> BinaryType,
+    "topic" -> StringType,
+    "partition" -> IntegerType,
+    "offset" -> LongType,
+    "timestamp" -> TimestampType,
+    "timestamptype" -> IntegerType
+  )
+
   override def primaryBatchType: Convention.BatchType = VeloxBatchType
 
   override def supportTimestampNtz: Boolean = true
@@ -188,6 +201,10 @@ object VeloxBackendSettings extends BackendSettingsApi {
             validateTypes(fieldTypeValidator, fields)
               .orElse(validateTypes(schemaTypeValidator, dataSchema.fields))
           }
+        case KafkaReadFormat =>
+          fields
+            .find(f => 
!kafkaColumns.get(f.name.toLowerCase(Locale.ROOT)).contains(f.dataType))
+            .map(f => s"Unsupported Kafka column ${f.name}: 
${f.dataType.simpleString}.")
         case _ => Some(s"Unsupported file format $format.")
       }
     }
diff --git 
a/gluten-ut/spark35/src/test/backends-velox/org/apache/gluten/execution/VeloxKafkaScanValidationSuite.scala
 
b/gluten-ut/spark35/src/test/backends-velox/org/apache/gluten/execution/VeloxKafkaScanValidationSuite.scala
new file mode 100644
index 0000000000..6bd56f608f
--- /dev/null
+++ 
b/gluten-ut/spark35/src/test/backends-velox/org/apache/gluten/execution/VeloxKafkaScanValidationSuite.scala
@@ -0,0 +1,66 @@
+/*
+ * 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.gluten.execution
+
+import org.apache.gluten.backendsapi.velox.VeloxBackendSettings
+import 
org.apache.gluten.substrait.rel.LocalFilesNode.ReadFileFormat.KafkaReadFormat
+
+import org.apache.spark.SparkFunSuite
+import org.apache.spark.sql.types._
+
+import org.apache.hadoop.conf.Configuration
+
+/** Schema validation must also work without Kafka jars, a broker, or a 
Kafka-enabled native build. */
+class VeloxKafkaScanValidationSuite extends SparkFunSuite {
+  private val kafkaFields = Array(
+    StructField("key", BinaryType),
+    StructField("value", BinaryType),
+    StructField("topic", StringType),
+    StructField("partition", IntegerType),
+    StructField("offset", LongType),
+    StructField("timestamp", TimestampType),
+    StructField("timestampType", IntegerType))
+
+  private def validate(fields: Array[StructField]): ValidationResult = {
+    VeloxBackendSettings.validateScanExec(
+      KafkaReadFormat,
+      fields,
+      StructType(fields),
+      Seq.empty,
+      Map.empty,
+      new Configuration(false),
+      Set(KafkaReadFormat))
+  }
+
+  test("native Kafka scan accepts the Spark Kafka schema and pruned columns") {
+    assert(validate(kafkaFields).ok())
+    assert(validate(Array(kafkaFields(4), kafkaFields(1))).ok())
+    assert(validate(Array.empty[StructField]).ok())
+  }
+
+  test("native Kafka scan rejects headers, unknown columns, and incompatible 
types") {
+    val headers = StructField(
+      "headers",
+      ArrayType(StructType(Seq(StructField("key", StringType), 
StructField("value", BinaryType)))))
+    Seq(headers, StructField("unknown", StringType), StructField("value", 
StringType)).foreach {
+      field =>
+        val result = validate(kafkaFields :+ field)
+        assert(!result.ok())
+        assert(result.reason().contains(s"Unsupported Kafka column 
${field.name}"))
+    }
+  }
+}


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

Reply via email to