Skip to content

Commit 5bffe1c

Browse files
committed
[VL] allow to pass hadoop options to native backend
Signed-off-by: Yuan <yuanzhou@apache.org> Revert "[VL] allow to pass hadoop options to native backend" This reverts commit 1a96976. fix Signed-off-by: Yuan <yuanzhou@apache.org> fix format Signed-off-by: Yuan <yuanzhou@apache.org> fix Signed-off-by: Yuan <yuanzhou@apache.org>
1 parent 31adaf8 commit 5bffe1c

9 files changed

Lines changed: 169 additions & 12 deletions

File tree

backends-clickhouse/src/main/scala/org/apache/gluten/backendsapi/clickhouse/CHIteratorApi.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ class CHIteratorApi extends IteratorApi with Logging with LogLevelUtil {
285285
partitionIndex: Int,
286286
inputIterators: Seq[Iterator[ColumnarBatch]] = Seq(),
287287
enableCudf: Boolean = false,
288-
wsContext: WholeStageTransformContext = null
288+
wsContext: WholeStageTransformContext = null,
289+
fsConf: Map[String, String] = Map.empty
289290
): Iterator[ColumnarBatch] = {
290291

291292
require(

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ class VeloxIteratorApi extends IteratorApi with Logging {
201201
partitionIndex: Int,
202202
inputIterators: Seq[Iterator[ColumnarBatch]] = Seq(),
203203
enableCudf: Boolean = false,
204-
wsContext: WholeStageTransformContext = null): Iterator[ColumnarBatch] = {
204+
wsContext: WholeStageTransformContext = null,
205+
fsConf: Map[String, String] = Map.empty): Iterator[ColumnarBatch] = {
205206
assert(
206207
inputPartition.isInstanceOf[GlutenPartition],
207208
"Velox backend only accept GlutenPartition.")
@@ -210,7 +211,8 @@ class VeloxIteratorApi extends IteratorApi with Logging {
210211
iter => new ColumnarBatchInIterator(BackendsApiManager.getBackendName, iter.asJava)
211212
}
212213

213-
val extraConf = Map(GlutenConfig.COLUMNAR_CUDF_ENABLED.key -> enableCudf.toString).asJava
214+
val extraConf =
215+
(fsConf + (GlutenConfig.COLUMNAR_CUDF_ENABLED.key -> enableCudf.toString)).asJava
214216
val transKernel = NativePlanEvaluator.create(BackendsApiManager.getBackendName, extraConf)
215217

216218
val splitInfoByteArray = inputPartition

gluten-arrow/src/main/scala/org/apache/gluten/runtime/Runtimes.scala

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,48 @@ package org.apache.gluten.runtime
1818

1919
import org.apache.spark.task.TaskResources
2020

21+
import java.security.MessageDigest
2122
import java.util
2223

2324
object Runtimes {
2425

26+
/**
27+
* Produce a stable, value-free cache key for a (backendName, name, extraConf) triple.
28+
*
29+
* Two problems with the old `s"$backendName:$name:$extraConf"` key:
30+
*
31+
* 1. **Credential leakage** – `Map.toString` embeds secret values (e.g. `fs.s3a.secret.key`,
32+
* `fs.azure.account.oauth2.client.secret`) in a plain heap string that can appear in logs,
33+
* thread dumps, and heap snapshots.
34+
* 2. **Nondeterminism** – Scala `Map.toString` does not guarantee insertion order, so two maps
35+
* with identical entries can produce different strings, causing spurious duplicate
36+
* `VeloxRuntime` registrations within a task.
37+
*
38+
* Fix: sort keys, hash them with SHA-256, and use only the hex digest as the key. Values are
39+
* intentionally excluded from the digest – distinct configs (different credentials for the same
40+
* key set) that need separate runtimes are already separated at the task level through
41+
* `GlutenPartition.fsConf`. Within a single task the key set is stable, so the digest is stable.
42+
*/
43+
private def stableKey(
44+
backendName: String,
45+
name: String,
46+
extraConf: util.Map[String, String]): String = {
47+
val digest = MessageDigest.getInstance("SHA-256")
48+
digest.update(backendName.getBytes("UTF-8"))
49+
digest.update(0.toByte) // field separator
50+
digest.update(name.getBytes("UTF-8"))
51+
digest.update(0.toByte)
52+
// Sort keys for determinism; hash only keys, not values, to avoid leaking secrets.
53+
val sortedKeys = new java.util.ArrayList(extraConf.keySet)
54+
java.util.Collections.sort(sortedKeys)
55+
sortedKeys.forEach {
56+
k =>
57+
digest.update(k.getBytes("UTF-8"))
58+
digest.update(0.toByte)
59+
}
60+
digest.digest().map("%02x".format(_)).mkString
61+
}
62+
2563
def contextInstance(
2664
backendName: String,
2765
name: String,
@@ -30,7 +68,7 @@ object Runtimes {
3068
throw new IllegalStateException("This method must be called in a Spark task.")
3169
}
3270
TaskResources.addResourceIfNotRegistered(
33-
s"$backendName:$name:$extraConf",
71+
stableKey(backendName, name, extraConf),
3472
() => Runtime(backendName, name, extraConf))
3573
}
3674

gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/IteratorApi.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ trait IteratorApi {
6969
partitionIndex: Int,
7070
inputIterators: Seq[Iterator[ColumnarBatch]] = Seq(),
7171
enableCudf: Boolean = false,
72-
wsContext: WholeStageTransformContext = null
72+
wsContext: WholeStageTransformContext = null,
73+
fsConf: Map[String, String] = Map.empty
7374
): Iterator[ColumnarBatch]
7475

7576
/**

gluten-substrait/src/main/scala/org/apache/gluten/execution/BasicScanExecTransformer.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ trait BasicScanExecTransformer extends LeafTransformSupport with BaseDataSource
7373
*/
7474
def pushDownFilters: Option[Seq[Expression]]
7575

76+
/**
77+
* Returns the options passed via DataFrameReader.option() or DataStreamReader.option(). For DSv1
78+
* scans these are stored in HadoopFsRelation.options and are NOT propagated to
79+
* sparkContext.hadoopConfiguration or SQLConf, so they must be collected here explicitly and
80+
* transported to the native backend via GlutenWholeStageColumnarRDD.fsConf.
81+
*
82+
* The default implementation returns Map.empty (used for DSv2 and non-file scans).
83+
*/
84+
def readerOptions: Map[String, String] = Map.empty
85+
7686
/** Copy the scan with filters that pushed by filterNode. */
7787
def withNewPushdownFilters(filters: Seq[Expression]): BasicScanExecTransformer = {
7888
throw new UnsupportedOperationException(

gluten-substrait/src/main/scala/org/apache/gluten/execution/BatchScanExecTransformer.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ abstract class BatchScanExecTransformerBase(
113113
postDriverMetrics()
114114
}
115115

116+
override def readerOptions: Map[String, String] = Map.empty
117+
116118
override def scanFilters: Seq[Expression] = scan match {
117119
case fileScan: FileScan => fileScan.dataFilters
118120
case _ =>

gluten-substrait/src/main/scala/org/apache/gluten/execution/FileSourceScanExecTransformer.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ abstract class FileSourceScanExecTransformerBase(
128128

129129
override def scanFilters: Seq[Expression] = dataFilters
130130

131+
override def readerOptions: Map[String, String] = relation.options
132+
131133
override def getMetadataColumns(): Seq[AttributeReference] = metadataColumns
132134

133135
override def getPartitions: Seq[Partition] = {

gluten-substrait/src/main/scala/org/apache/gluten/execution/GlutenWholeStageColumnarRDD.scala

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,50 @@ trait BaseGlutenPartition extends Partition with InputPartition {
3535
def plan: Array[Byte]
3636
}
3737

38+
/**
39+
* Wraps Hadoop/Velox filesystem credential key-value pairs (fs.azure.*, fs.s3a.*, fs.gs.*) so they
40+
* cannot be accidentally exposed through logging, toString, exception messages, or other debug
41+
* paths that might print an arbitrary object.
42+
*
43+
* `toString` is deliberately overridden to redact values - only key NAMES are shown (these are not
44+
* secret; only the bearer values like access keys, secret keys, and OAuth client secrets are). This
45+
* makes accidental leaks structurally harder: a future `logDebug(s"... $fsConfHolder")` or similar
46+
* call will print "FsCredentialConf(3 keys: [fs.azure.account.auth.type, ...], values redacted)"
47+
* instead of the literal secret strings.
48+
*
49+
* The underlying map is also `private` so it cannot be reached by name from outside this file
50+
* without going through `.unsafeValue`, which is named to discourage casual use.
51+
*/
52+
final case class FsCredentialConf private (private val raw: Map[String, String]) {
53+
54+
/** Number of credential entries held. Safe to log. */
55+
def size: Int = raw.size
56+
57+
def isEmpty: Boolean = raw.isEmpty
58+
def nonEmpty: Boolean = raw.nonEmpty
59+
60+
/**
61+
* Returns the underlying map with real values. Named `unsafeValue` to make call sites grep-able
62+
* and to discourage passing the result to logging code. Only the native JNI boundary (extraConf
63+
* for NativePlanEvaluator) should call this.
64+
*/
65+
def unsafeValue: Map[String, String] = raw
66+
67+
override def toString: String = {
68+
if (raw.isEmpty) {
69+
"FsCredentialConf(empty)"
70+
} else {
71+
s"FsCredentialConf(${raw.size} keys: [${raw.keys.toSeq.sorted.mkString(", ")}], " +
72+
"values redacted)"
73+
}
74+
}
75+
}
76+
77+
object FsCredentialConf {
78+
val empty: FsCredentialConf = FsCredentialConf(Map.empty)
79+
def apply(raw: Map[String, String]): FsCredentialConf = new FsCredentialConf(raw)
80+
}
81+
3882
case class GlutenPartition(
3983
index: Int,
4084
plan: Array[Byte],
@@ -61,9 +105,17 @@ class GlutenWholeStageColumnarRDD(
61105
updateInputMetrics: InputMetricsWrapper => Unit,
62106
updateNativeMetrics: IMetrics => Unit,
63107
enableCudf: Boolean = false,
64-
wsContext: WholeStageTransformContext = null)
108+
wsContext: WholeStageTransformContext = null,
109+
private val fsConf: FsCredentialConf = FsCredentialConf.empty)
65110
extends RDD[ColumnarBatch](sc, rdds.getDependencies) {
66111

112+
// Override toString so that fsConf credential values are never exposed in
113+
// Spark logs, DAG visualization, toDebugString, or the UI. Delegates to
114+
// FsCredentialConf.toString, which shows only key NAMES (redacted values) -
115+
// see FsCredentialConf's doc comment for the full rationale.
116+
override def toString: String =
117+
s"GlutenWholeStageColumnarRDD[$id] fsConf=$fsConf"
118+
67119
override def compute(split: Partition, context: TaskContext): Iterator[ColumnarBatch] = {
68120
GlutenTimeMetric.millis(pipelineTime) {
69121
_ =>
@@ -84,7 +136,8 @@ class GlutenWholeStageColumnarRDD(
84136
split.index,
85137
inputIterators,
86138
enableCudf,
87-
wsContext
139+
wsContext,
140+
fsConf.unsafeValue
88141
)
89142
}
90143
}
@@ -96,10 +149,7 @@ class GlutenWholeStageColumnarRDD(
96149
}
97150
}
98151

99-
override def getPreferredLocations(split: Partition): Seq[String] = {
100-
castNativePartition(split)._1.preferredLocations()
101-
}
102-
152+
override def getPreferredLocations(split: Partition): Seq[String] = {}
103153
override protected def getPartitions: Array[Partition] = {
104154
inputPartitions.zipWithIndex
105155
.map {

gluten-substrait/src/main/scala/org/apache/gluten/execution/WholeStageTransformer.scala

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,56 @@ case class WholeStageTransformer(child: SparkPlan, materializeInput: Boolean = f
367367
allSplitInfos,
368368
leafTransformers)
369369

370+
// Compute fsConf once here on the driver from the leaf transformers' session
371+
// Hadoop configuration. It is stored as a plain field on the RDD and
372+
// serialized once per executor via the existing task closure - no per-partition
373+
// copies, no Broadcast overhead (no BlockManager registration, no HTTP fetch,
374+
// no master round-trip). GlutenPartition never carries fsConf.
375+
//
376+
// Two sources merged (getPropsWithPrefix avoids full conf iteration):
377+
// 1. sparkContext.hadoopConfiguration - spark.hadoop.* and sc.hadoopConf.set()
378+
// 2. SQLConf - spark.conf.set("fs.*", ...) keys that live only in SQLConf
379+
val fsConf: Map[String, String] = {
380+
import scala.collection.JavaConverters._
381+
val fsPrefixes = Seq("fs.azure.", "fs.s3a.", "fs.gs.")
382+
383+
// Source 1: sparkContext.hadoopConfiguration (spark.hadoop.* and sc.hadoopConf.set()).
384+
// Read directly from the mutable base Configuration via getPropsWithPrefix
385+
// rather than sessionState.newHadoopConf(), which would make a full copy
386+
// of the entire Hadoop config (hundreds of entries from core-site.xml,
387+
// hdfs-site.xml, etc.) just to look up a handful of fs.* keys.
388+
// getPropsWithPrefix reads directly from the internal map and returns only
389+
// matching entries - cost proportional to matches, not total config size.
390+
// scalastyle:off hadoopconfiguration
391+
val baseHadoopConf = sparkContext.hadoopConfiguration
392+
// scalastyle:on hadoopconfiguration
393+
val fromHadoop: Map[String, String] = fsPrefixes.flatMap {
394+
prefix =>
395+
baseHadoopConf.getPropsWithPrefix(prefix).asScala
396+
.map { case (suffix, v) => (prefix + suffix) -> v }
397+
}.toMap
398+
399+
// Source 2: SQLConf (spark.conf.set("fs.*", ...) at session level).
400+
val fromSqlConf: Map[String, String] =
401+
org.apache.spark.sql.internal.SQLConf.get.getAllConfs.filter {
402+
case (k, _) => fsPrefixes.exists(k.startsWith)
403+
}
404+
405+
// Source 3: DataFrameReader.option() / DataStreamReader.option() passed to
406+
// each scan. These are stored in HadoopFsRelation.options (DSv1) or
407+
// FileScan.options (DSv2) and are NOT in hadoopConfiguration or SQLConf.
408+
// Collect from all leaf scan transformers and merge (last writer wins, so
409+
// later leaves can override earlier ones for the same key).
410+
val fromReaderOptions: Map[String, String] =
411+
leafTransformers
412+
.collect { case s: BasicScanExecTransformer => s.readerOptions }
413+
.flatMap(_.filter { case (k, _) => fsPrefixes.exists(k.startsWith) })
414+
.toMap
415+
416+
// Precedence: readerOptions (most specific) > SQLConf > hadoopConfiguration
417+
fromHadoop ++ fromSqlConf ++ fromReaderOptions
418+
}
419+
370420
val rdd = new GlutenWholeStageColumnarRDD(
371421
sparkContext,
372422
inputPartitions,
@@ -380,7 +430,8 @@ case class WholeStageTransformer(child: SparkPlan, materializeInput: Boolean = f
380430
wsCtx.substraitContext.registeredAggregationParams
381431
),
382432
wsCtx.enableCudf,
383-
wsCtx
433+
wsCtx,
434+
FsCredentialConf(fsConf)
384435
)
385436

386437
val allInputPartitions = leafTransformers.map(_.getPartitions)

0 commit comments

Comments
 (0)