Skip to content

Commit cb57a6b

Browse files
committed
[TEST][VL] Forward Hadoop filesystem configuration per runtime
Collect user-provided filesystem settings once per Spark session and carry them through whole-stage RDDs and native writer paths without adding credentials to the Substrait plan. Create per-runtime Velox connector configuration, use credential-safe runtime identity, and redact sensitive filesystem and UGI settings. Preserve legacy overloads and constructors for backend compatibility.
1 parent dd512bc commit cb57a6b

21 files changed

Lines changed: 1053 additions & 51 deletions

File tree

backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxBackend.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package org.apache.gluten.backendsapi.velox
1818

1919
import org.apache.gluten.GlutenBuildInfo._
2020
import org.apache.gluten.backendsapi._
21-
import org.apache.gluten.config.{GlutenConfig, VeloxConfig}
21+
import org.apache.gluten.config.{GlutenConfig, HadoopConfContributor, VeloxConfig}
2222
import org.apache.gluten.exception.GlutenNotSupportException
2323
import org.apache.gluten.execution.ValidationResult
2424
import org.apache.gluten.execution.WriteFilesExecTransformer
@@ -52,7 +52,7 @@ import org.apache.hadoop.fs.Path
5252

5353
import scala.util.control.Breaks.breakable
5454

55-
class VeloxBackend extends SubstraitBackend {
55+
class VeloxBackend extends SubstraitBackend with HadoopConfContributor {
5656
import VeloxBackend._
5757

5858
override def name(): String = VeloxBackend.BACKEND_NAME
@@ -72,6 +72,7 @@ class VeloxBackend extends SubstraitBackend {
7272
override def settings(): BackendSettingsApi = VeloxBackendSettings
7373
override def convFuncOverride(): ConventionFunc.Override = new ConvFunc()
7474
override def costers(): Seq[LongCoster] = Seq(LegacyCoster, RoughCoster)
75+
override def interestedPrefixes(): Set[String] = Set("fs.")
7576
}
7677

7778
object VeloxBackend {

backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxIteratorApi.scala

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,30 @@ class VeloxIteratorApi extends IteratorApi with Logging {
202202
inputIterators: Seq[Iterator[ColumnarBatch]] = Seq(),
203203
enableCudf: Boolean = false,
204204
wsContext: WholeStageTransformContext = null): Iterator[ColumnarBatch] = {
205+
genFirstStageIterator(
206+
inputPartition,
207+
context,
208+
pipelineTime,
209+
updateInputMetrics,
210+
updateNativeMetrics,
211+
partitionIndex,
212+
inputIterators,
213+
enableCudf,
214+
wsContext,
215+
Map.empty)
216+
}
217+
218+
override def genFirstStageIterator(
219+
inputPartition: BaseGlutenPartition,
220+
context: TaskContext,
221+
pipelineTime: SQLMetric,
222+
updateInputMetrics: InputMetricsWrapper => Unit,
223+
updateNativeMetrics: IMetrics => Unit,
224+
partitionIndex: Int,
225+
inputIterators: Seq[Iterator[ColumnarBatch]],
226+
enableCudf: Boolean,
227+
wsContext: WholeStageTransformContext,
228+
fsConf: Map[String, String]): Iterator[ColumnarBatch] = {
205229
assert(
206230
inputPartition.isInstanceOf[GlutenPartition],
207231
"Velox backend only accept GlutenPartition.")
@@ -210,7 +234,9 @@ class VeloxIteratorApi extends IteratorApi with Logging {
210234
iter => new ColumnarBatchInIterator(BackendsApiManager.getBackendName, iter.asJava)
211235
}
212236

213-
val extraConf = Map(GlutenConfig.COLUMNAR_CUDF_ENABLED.key -> enableCudf.toString).asJava
237+
val extraConf = VeloxIteratorApi
238+
.buildExtraConf(fsConf, enableCudf, supportsValueStreamDynamicFilter = true)
239+
.asJava
214240
val transKernel = NativePlanEvaluator.create(BackendsApiManager.getBackendName, extraConf)
215241

216242
val splitInfoByteArray = inputPartition
@@ -262,11 +288,36 @@ class VeloxIteratorApi extends IteratorApi with Logging {
262288
materializeInput: Boolean,
263289
enableCudf: Boolean = false,
264290
supportsValueStreamDynamicFilter: Boolean = true): Iterator[ColumnarBatch] = {
265-
val extraConfMap = mutable.Map(GlutenConfig.COLUMNAR_CUDF_ENABLED.key -> enableCudf.toString)
266-
if (!supportsValueStreamDynamicFilter) {
267-
extraConfMap(VeloxConfig.VALUE_STREAM_DYNAMIC_FILTER_ENABLED.key) = "false"
268-
}
269-
val extraConf = extraConfMap.asJava
291+
genFinalStageIterator(
292+
context,
293+
inputIterators,
294+
sparkConf,
295+
rootNode,
296+
pipelineTime,
297+
updateNativeMetrics,
298+
partitionIndex,
299+
materializeInput,
300+
enableCudf,
301+
supportsValueStreamDynamicFilter,
302+
Map.empty
303+
)
304+
}
305+
306+
override def genFinalStageIterator(
307+
context: TaskContext,
308+
inputIterators: Seq[Iterator[ColumnarBatch]],
309+
sparkConf: SparkConf,
310+
rootNode: PlanNode,
311+
pipelineTime: SQLMetric,
312+
updateNativeMetrics: IMetrics => Unit,
313+
partitionIndex: Int,
314+
materializeInput: Boolean,
315+
enableCudf: Boolean,
316+
supportsValueStreamDynamicFilter: Boolean,
317+
fsConf: Map[String, String]): Iterator[ColumnarBatch] = {
318+
val extraConf = VeloxIteratorApi
319+
.buildExtraConf(fsConf, enableCudf, supportsValueStreamDynamicFilter)
320+
.asJava
270321
val transKernel = NativePlanEvaluator.create(BackendsApiManager.getBackendName, extraConf)
271322
val columnarNativeIterator =
272323
inputIterators.map {
@@ -303,6 +354,21 @@ class VeloxIteratorApi extends IteratorApi with Logging {
303354
}
304355

305356
object VeloxIteratorApi {
357+
private[gluten] def buildExtraConf(
358+
fsConf: Map[String, String],
359+
enableCudf: Boolean,
360+
supportsValueStreamDynamicFilter: Boolean): Map[String, String] = {
361+
val dynamicFilterConf =
362+
if (supportsValueStreamDynamicFilter) {
363+
Map.empty[String, String]
364+
} else {
365+
Map(VeloxConfig.VALUE_STREAM_DYNAMIC_FILTER_ENABLED.key -> "false")
366+
}
367+
val controlConf =
368+
Map(GlutenConfig.COLUMNAR_CUDF_ENABLED.key -> enableCudf.toString) ++ dynamicFilterConf
369+
fsConf.filter { case (key, _) => key.startsWith("spark.hadoop.fs.") } ++ controlConf
370+
}
371+
306372
// lookup table to translate '0' -> 0 ... 'F'/'f' -> 15
307373
private val unhexDigits = {
308374
val array = Array.fill[Byte](128)(-1)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.gluten.backendsapi.velox
18+
19+
import org.apache.gluten.config.{GlutenConfig, VeloxConfig}
20+
21+
import org.scalatest.funsuite.AnyFunSuite
22+
23+
class VeloxHadoopConfTransportSuite extends AnyFunSuite {
24+
private val cudfKey = GlutenConfig.COLUMNAR_CUDF_ENABLED.key
25+
private val dynamicFilterKey = VeloxConfig.VALUE_STREAM_DYNAMIC_FILTER_ENABLED.key
26+
27+
test("extra conf keeps filesystem credentials, drops other settings, overrides control keys") {
28+
val fsConf = Map(
29+
"spark.hadoop.fs.s3a.access.key" -> "s3-access-key",
30+
"spark.hadoop.fs.gs.auth.service.account.private.key" -> "gcs-key",
31+
"spark.hadoop.fs.oss.endpoint" -> "unknown-scheme-endpoint",
32+
"spark.sql.session.timeZone" -> "UTC",
33+
"spark.gluten.ugi.tokens" -> "delegation-token",
34+
cudfKey -> "true",
35+
dynamicFilterKey -> "true"
36+
)
37+
38+
val merged = VeloxIteratorApi.buildExtraConf(
39+
fsConf,
40+
enableCudf = false,
41+
supportsValueStreamDynamicFilter = false)
42+
43+
assert(merged("spark.hadoop.fs.s3a.access.key") == "s3-access-key")
44+
assert(merged("spark.hadoop.fs.gs.auth.service.account.private.key") == "gcs-key")
45+
assert(merged("spark.hadoop.fs.oss.endpoint") == "unknown-scheme-endpoint")
46+
assert(!merged.contains("spark.sql.session.timeZone"))
47+
assert(!merged.contains("spark.gluten.ugi.tokens"))
48+
assert(merged(cudfKey) == "false")
49+
assert(merged(dynamicFilterKey) == "false")
50+
}
51+
52+
test("extra conf enables CUDF without injecting an enabled dynamic filter") {
53+
val merged = VeloxIteratorApi.buildExtraConf(
54+
Map.empty,
55+
enableCudf = true,
56+
supportsValueStreamDynamicFilter = true)
57+
58+
assert(merged(cudfKey) == "true")
59+
assert(!merged.contains(dynamicFilterKey))
60+
assert(!merged.keys.exists(_.startsWith("spark.hadoop.fs.")))
61+
}
62+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.gluten.execution
18+
19+
import org.apache.spark.SparkFunSuite
20+
import org.apache.spark.sql.test.SharedSparkSession
21+
22+
class VeloxHadoopConfCollectorSuite extends SparkFunSuite with SharedSparkSession {
23+
test("collect caches the first Hadoop configuration snapshot for each Spark session") {
24+
val key = "spark.hadoop.fs.s3a.gluten.collector.cached"
25+
val session = spark.newSession()
26+
session.conf.set(key, "first-snapshot")
27+
28+
val firstCollected = HadoopConfCollector.collect(session)
29+
session.conf.set(key, "changed-after-first-collect")
30+
val secondCollected = HadoopConfCollector.collect(session)
31+
32+
assert(secondCollected eq firstCollected)
33+
assert(secondCollected(key) == "first-snapshot")
34+
}
35+
36+
test("collect keeps configuration values isolated between Spark sessions") {
37+
val key = "spark.hadoop.fs.s3a.gluten.collector.session"
38+
val first = spark.newSession()
39+
val second = spark.newSession()
40+
first.conf.set(key, "first-session")
41+
second.conf.set(key, "second-session")
42+
43+
val firstCollected = HadoopConfCollector.collect(first)
44+
val secondCollected = HadoopConfCollector.collect(second)
45+
46+
assert(firstCollected(key) == "first-session")
47+
assert(secondCollected(key) == "second-session")
48+
}
49+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.gluten.runtime
18+
19+
import org.apache.spark.SparkFunSuite
20+
21+
import java.util
22+
23+
class RuntimesSuite extends SparkFunSuite {
24+
test("resource ID neither exposes nor fingerprints filesystem configuration values") {
25+
val secrets = linkedMap(
26+
"spark.hadoop.fs.s3a.access.key" -> "AKIA-super-secret-access-key",
27+
"spark.hadoop.fs.s3a.secret.key" -> "super-secret-credential-value")
28+
val resourceId = Runtimes.resourceId("velox", "context", secrets)
29+
30+
assert(!resourceId.contains("AKIA-super-secret-access-key"))
31+
assert(!resourceId.contains("super-secret-credential-value"))
32+
33+
// Rotating only the credential values must not change the runtime identity.
34+
val rotated = linkedMap(
35+
"spark.hadoop.fs.s3a.access.key" -> "different-access-key",
36+
"spark.hadoop.fs.s3a.secret.key" -> "different-credential-value")
37+
assert(Runtimes.resourceId("velox", "context", rotated) == resourceId)
38+
}
39+
40+
test("resource ID is independent of map insertion order") {
41+
val first = linkedMap("alpha" -> "one", "beta" -> "two")
42+
val reversed = linkedMap("beta" -> "two", "alpha" -> "one")
43+
44+
assert(Runtimes.resourceId("velox", "context", first) ==
45+
Runtimes.resourceId("velox", "context", reversed))
46+
}
47+
48+
test("resource ID distinguishes non-filesystem configuration values") {
49+
val first = linkedMap("spark.gluten.sql.columnar.backend.velox.cudf" -> "false")
50+
val second = linkedMap("spark.gluten.sql.columnar.backend.velox.cudf" -> "true")
51+
52+
assert(Runtimes.resourceId("velox", "context", first) !=
53+
Runtimes.resourceId("velox", "context", second))
54+
}
55+
56+
test("resource ID uses unambiguous key and value encoding") {
57+
val delimiterInKey = linkedMap("alpha=beta" -> "gamma")
58+
val delimiterInValue = linkedMap("alpha" -> "beta=gamma")
59+
60+
assert(Runtimes.resourceId("velox", "context", delimiterInKey) !=
61+
Runtimes.resourceId("velox", "context", delimiterInValue))
62+
}
63+
64+
private def linkedMap(entries: (String, String)*): util.Map[String, String] = {
65+
val result = new util.LinkedHashMap[String, String]()
66+
entries.foreach { case (key, value) => result.put(key, value) }
67+
result
68+
}
69+
}

0 commit comments

Comments
 (0)