Skip to content

Commit 3484af3

Browse files
committed
Address comments
Signed-off-by: Yi Li <yi.li@databricks.com>
1 parent 316797b commit 3484af3

11 files changed

Lines changed: 192 additions & 145 deletions

File tree

spark/src/main/scala/org/apache/spark/sql/delta/catalog/AbstractDeltaCatalogClient.scala

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.apache.spark.sql.delta.catalog
1818

1919
import org.apache.spark.internal.Logging
2020
import org.apache.spark.sql.connector.catalog.{Identifier, Table}
21+
import org.apache.spark.sql.delta.coordinatedcommits.UCTokenBasedRestClientFactory
2122
import org.apache.spark.sql.util.CaseInsensitiveStringMap
2223

2324
/**
@@ -45,30 +46,26 @@ private[catalog] trait AbstractDeltaCatalogClientFactory {
4546
def fromCatalogOptions(
4647
catalogName: String,
4748
options: CaseInsensitiveStringMap,
48-
fallbackLoadTable: Identifier => Table): AbstractDeltaCatalogClient
49+
fallbackLoadTableFunc: Identifier => Table): AbstractDeltaCatalogClient
4950
}
5051

5152
private[catalog] object AbstractDeltaCatalogClient extends Logging {
5253

53-
private val UC_DELTA_REST_API_ENABLED_KEY: String = "deltaRestApi.enabled"
5454
private val UC_DELTA_CATALOG_CLIENT_IMPL_CLASS_NAME: String =
5555
"org.apache.spark.sql.delta.catalog.UCDeltaCatalogClientImpl"
5656

5757
/**
5858
* Returns a [[AbstractDeltaCatalogClient]] wrapped in [[Some]] when the catalog opted in via
5959
* `deltaRestApi.enabled`, else [[None]]. The concrete impl is loaded reflectively so
60-
* [[AbstractDeltaCatalog]] doesn't compile-depend on it.
61-
*
62-
* When opt-in is explicit but reflective loading fails (missing class, wrong type, missing
63-
* MODULE$ field, etc.), this throws [[IllegalStateException]] rather than silently degrading
64-
* to the legacy delegate. Following the [[deltaCatalogClient]] is `null` path when the user
65-
* configured the opposite would mask a misconfiguration.
60+
* [[AbstractDeltaCatalog]] doesn't compile-depend on it. If opt-in is explicit but reflective
61+
* loading fails, throws [[IllegalStateException]] rather than silently degrading.
6662
*/
6763
def fromCatalogOptionsIfEnabled(
6864
catalogName: String,
6965
options: CaseInsensitiveStringMap,
70-
fallbackLoadTable: Identifier => Table): Option[AbstractDeltaCatalogClient] = {
71-
if (!options.getBoolean(UC_DELTA_REST_API_ENABLED_KEY, false)) {
66+
fallbackLoadTableFunc: Identifier => Table): Option[AbstractDeltaCatalogClient] = {
67+
val key = UCTokenBasedRestClientFactory.DELTA_REST_API_ENABLED_KEY
68+
if (!options.getBoolean(key, false)) {
7269
return None
7370
}
7471
val factory = try {
@@ -79,11 +76,10 @@ private[catalog] object AbstractDeltaCatalogClient extends Logging {
7976
} catch {
8077
case e: Exception =>
8178
throw new IllegalStateException(
82-
s"Failed to load $UC_DELTA_CATALOG_CLIENT_IMPL_CLASS_NAME though " +
83-
s"'$UC_DELTA_REST_API_ENABLED_KEY' is true. Ensure the implementation JAR is on " +
84-
s"the classpath, or remove '$UC_DELTA_REST_API_ENABLED_KEY' from the catalog " +
85-
s"options to fall back to the legacy delegate.", e)
79+
s"Failed to load $UC_DELTA_CATALOG_CLIENT_IMPL_CLASS_NAME though '$key' is true. Ensure the implementation " +
80+
s"JAR is on the classpath, or remove '$key' from the catalog options to fall back " +
81+
"to the legacy delegate.", e)
8682
}
87-
Some(factory.fromCatalogOptions(catalogName, options, fallbackLoadTable))
83+
Some(factory.fromCatalogOptions(catalogName, options, fallbackLoadTableFunc))
8884
}
8985
}

spark/src/main/scala/org/apache/spark/sql/delta/catalog/UCDeltaCatalogClientImpl.scala

Lines changed: 67 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,17 @@ package org.apache.spark.sql.delta.catalog
1818

1919
import java.net.URI
2020
import java.util.concurrent.atomic.AtomicLong
21-
import java.util.function.Supplier
2221

2322
import scala.jdk.CollectionConverters._
2423

2524
import io.delta.storage.commit.{TableIdentifier => StorageTableIdentifier}
26-
import io.delta.storage.commit.uccommitcoordinator.{
27-
UCDeltaClient,
28-
UCDeltaModels,
29-
UCDeltaTokenBasedRestClient
30-
}
25+
import io.delta.storage.commit.uccommitcoordinator.{UCDeltaClient, UCDeltaModels}
3126
import io.delta.storage.commit.uccommitcoordinator.UCDeltaModels.TableInfo
3227
import io.delta.storage.commit.uccommitcoordinator.exceptions.{
3328
CredentialFetchFailedException,
3429
UnsupportedTableFormatException,
3530
NoSuchTableException => StorageNoSuchTableException
3631
}
37-
import org.apache.hadoop.conf.Configuration
3832

3933
import org.apache.spark.internal.{Logging, MDC}
4034
import org.apache.spark.sql.SparkSession
@@ -60,11 +54,12 @@ private[catalog] class UCDeltaCatalogClientImpl(
6054
catalogName: String,
6155
ucClient: UCDeltaClient,
6256
serverSidePlanningEnabled: Boolean = false,
63-
fallbackLoadTable: Identifier => Table = UCDeltaCatalogClientImpl.defaultFallbackLoadTable)
57+
fallbackLoadTableFunc: Identifier => Table
58+
= UCDeltaCatalogClientImpl.defaultFallbackLoadTableFunc)
6459
extends AbstractDeltaCatalogClient with Logging {
6560

6661
override def loadTable(ident: Identifier): Table = {
67-
UCDeltaCatalogClientImpl.LOAD_TABLE_INVOCATIONS.incrementAndGet()
62+
UCDeltaCatalogClientImpl.loadTableInvocationsCounter.incrementAndGet()
6863
val tid = toStorageTableIdent(ident)
6964
val info =
7065
try ucClient.loadTable(tid)
@@ -74,7 +69,7 @@ private[catalog] class UCDeltaCatalogClientImpl(
7469
logInfo(log"Table ${MDC(DeltaLogKeys.TABLE_NAME, ident)} is not in Delta format; " +
7570
log"falling back to the legacy catalog path. Cause: " +
7671
log"${MDC(DeltaLogKeys.EXCEPTION, e.getMessage)}")
77-
return fallbackLoadTable(ident)
72+
return fallbackLoadTableFunc(ident)
7873
case e: CredentialFetchFailedException if serverSidePlanningEnabled =>
7974
logWarning(log"Credential fetch failed for " +
8075
log"${MDC(DeltaLogKeys.TABLE_NAME, fullQualifiedTableName(tid))}; enabling " +
@@ -83,7 +78,7 @@ private[catalog] class UCDeltaCatalogClientImpl(
8378
enableServerSidePlanningConfig(ident)
8479
e.getTableInfoWithoutCredentials
8580
}
86-
UCDeltaCatalogClientImpl.SUCCESSFUL_DELTA_REST_API_LOADS.incrementAndGet()
81+
UCDeltaCatalogClientImpl.successfulDeltaRestApiLoadsCounter.incrementAndGet()
8782
toV1Table(ident, info)
8883
}
8984

@@ -150,93 +145,93 @@ private[catalog] class UCDeltaCatalogClientImpl(
150145
}
151146

152147
object UCDeltaCatalogClientImpl extends AbstractDeltaCatalogClientFactory with Logging {
148+
// Test-only instrumentation. The mutable counters are encapsulated so production code
149+
// can neither read nor write them; read access is exposed via the `*ForTesting` methods
150+
// below so cross-package integration tests (e.g. `io.sparkuctest.*`) don't need
151+
// reflection.
152+
153+
/** Bumped at every `loadTable` entry regardless of outcome. Read via the *ForTesting API. */
154+
private val loadTableInvocationsCounter: AtomicLong = new AtomicLong(0L)
155+
153156
/**
154-
* Test-only instrumentation. Bumped at every `loadTable` entry regardless of outcome.
155-
* Read by integration tests (e.g. {@code UCDeltaTableIntegrationBaseTest}) to verify the
156-
* Delta REST API path was exercised. Not part of any public API; do not depend on this
157-
* from production code. Kept public so cross-package integration tests
158-
* ({@code io.sparkuctest.*}) can read it without reflection.
157+
* Bumped only when `loadTable` returned a Delta table via the Delta REST API (no fallback,
158+
* no rethrow). Read via the *ForTesting API.
159159
*/
160-
val LOAD_TABLE_INVOCATIONS: AtomicLong = new AtomicLong(0L)
160+
private val successfulDeltaRestApiLoadsCounter: AtomicLong = new AtomicLong(0L)
161161

162162
/**
163-
* Test-only instrumentation. Bumped only when `loadTable` returned a Delta table via the
164-
* Delta REST API (no fallback, no rethrow). Read by integration tests to assert "Delta REST
165-
* actually served the load." Not part of any public API; do not depend on this from
166-
* production code. Kept public so cross-package integration tests
167-
* ({@code io.sparkuctest.*}) can read it without reflection.
163+
* Test-only read accessor for the `loadTable` invocation counter. Used by integration
164+
* tests to verify the Delta REST API code path ran. Not part of any public API; production
165+
* code must not depend on it.
168166
*/
169-
val SUCCESSFUL_DELTA_REST_API_LOADS: AtomicLong = new AtomicLong(0L)
167+
def loadTableInvocationsForTesting: Long = loadTableInvocationsCounter.get()
168+
169+
/**
170+
* Test-only read accessor for the count of `loadTable` calls served by the Delta REST API
171+
* (no fallback, no rethrow). Not part of any public API.
172+
*/
173+
def successfulDeltaRestApiLoadsForTesting: Long = successfulDeltaRestApiLoadsCounter.get()
170174

171-
private[catalog] val RenewCredentialEnabledKey: String = "renewCredential.enabled"
172-
private[catalog] val CredScopedFsEnabledKey: String = "credScopedFs.enabled"
173175
private[catalog] val ServerSidePlanningEnabledKey: String = "serverSidePlanning.enabled"
174176

175-
private[catalog] val defaultFallbackLoadTable: Identifier => Table = ident =>
177+
private[catalog] val defaultFallbackLoadTableFunc: Identifier => Table = ident =>
176178
throw new IllegalStateException(
177179
s"Non-Delta table $ident cannot be served via the Delta REST API path and no " +
178180
"fallback catalog was configured.")
179181

180182
/**
181-
* Builds a [[UCDeltaCatalogClientImpl]] from catalog options. The `deltaRestApi.enabled`
182-
* gate is the caller's responsibility
183-
* ([[AbstractDeltaCatalogClient.fromCatalogOptionsIfEnabled]]).
184-
* {@code fallbackLoadTable} is invoked when UC reports {@code UnsupportedTableFormatException}.
183+
* Builds a [[UCDeltaCatalogClientImpl]] from catalog options. The `deltaRestApi.enabled` gate
184+
* is the caller's responsibility ([[AbstractDeltaCatalogClient.fromCatalogOptionsIfEnabled]]).
185+
* `fallbackLoadTableFunc` is invoked when UC reports `UnsupportedTableFormatException`. UC client
186+
* construction is delegated to [[UCTokenBasedRestClientFactory]] with `renewCredential.enabled`
187+
* defaulted to `true` and `credScopedFs.enabled` defaulted to `false` when not set.
185188
*/
186189
override def fromCatalogOptions(
187190
catalogName: String,
188191
options: CaseInsensitiveStringMap,
189-
fallbackLoadTable: Identifier => Table
192+
fallbackLoadTableFunc: Identifier => Table
190193
): UCDeltaCatalogClientImpl = {
191-
val uri = Option(options.get("uri")).getOrElse(throw new IllegalArgumentException(
192-
s"'uri' is required when 'deltaRestApi.enabled' is true (catalog '$catalogName')"))
193-
val authConfigs = extractAuthConfigs(options, catalogName)
194-
val appVersions = UCTokenBasedRestClientFactory.defaultAppVersionsAsJava
195-
val renewCredEnabled = options.getBoolean(RenewCredentialEnabledKey, true)
196-
val credScopedFsEnabled = options.getBoolean(CredScopedFsEnabledKey, false)
194+
// Pre-flight: keep our user-facing errors instead of the factory's less specific ones.
195+
if (options.get(UriKey) == null) {
196+
throw new IllegalArgumentException(s"'$UriKey' is required (catalog '$catalogName')")
197+
}
198+
validateAuthConfigured(options, catalogName)
199+
200+
// `asCaseSensitiveMap()` preserves the user's original key case; `containsKey` is
201+
// case-insensitive so defaults don't create duplicate keys.
202+
val merged = new java.util.HashMap[String, String](options.asCaseSensitiveMap())
203+
Seq(
204+
UCTokenBasedRestClientFactory.DELTA_REST_API_ENABLED_KEY -> "true",
205+
UCTokenBasedRestClientFactory.RENEW_CREDENTIAL_ENABLED_KEY -> "true",
206+
UCTokenBasedRestClientFactory.CRED_SCOPED_FS_ENABLED_KEY -> "false"
207+
).foreach { case (k, v) => if (!options.containsKey(k)) merged.put(k, v) }
208+
val ucClient = UCTokenBasedRestClientFactory
209+
.createUCClient(new CaseInsensitiveStringMap(merged))
210+
.asInstanceOf[UCDeltaClient]
211+
197212
val sspEnabled = options.getBoolean(ServerSidePlanningEnabledKey, false)
198-
val hadoopConfSupplier: Supplier[Configuration] =
199-
() => SparkSession.getActiveSession
200-
.map(_.sparkContext.hadoopConfiguration)
201-
.getOrElse(new Configuration())
202-
val restClient = UCDeltaTokenBasedRestClient.create(
203-
uri,
204-
authConfigs,
205-
appVersions,
206-
renewCredEnabled,
207-
credScopedFsEnabled,
208-
hadoopConfSupplier)
209-
new UCDeltaCatalogClientImpl(catalogName, restClient, sspEnabled, fallbackLoadTable)
213+
new UCDeltaCatalogClientImpl(catalogName, ucClient, sspEnabled, fallbackLoadTableFunc)
210214
}
211215

216+
private val UriKey: String = "uri"
217+
private val AuthPrefix: String = "auth."
218+
private val LegacyTokenKey: String = "token"
219+
212220
/**
213-
* `auth.*` sub-keys (prefix stripped) feed `TokenProvider.create`. Legacy bare `token`
214-
* is translated to `{type=static, token=<value>}`, only when no `auth.*` is present.
221+
* Pre-flight: ensure at least one of `auth.*` or legacy `token` is present, so the user
222+
* sees a clear error (and catalog name) instead of the factory's internal failure when
223+
* `TokenProvider.create` is handed an empty config.
215224
*/
216-
private[catalog] def extractAuthConfigs(
225+
private[catalog] def validateAuthConfigured(
217226
options: CaseInsensitiveStringMap,
218-
catalogName: String): java.util.Map[String, String] = {
219-
val authConfigs = new java.util.HashMap[String, String]()
220-
val authPrefix = "auth."
221-
// CaseInsensitiveStringMap.entrySet() returns keys already lowercased.
222-
options.entrySet().asScala.foreach { e =>
223-
val key = e.getKey
224-
if (key.startsWith(authPrefix)) {
225-
authConfigs.put(key.substring(authPrefix.length), e.getValue)
226-
}
227-
}
228-
if (authConfigs.isEmpty) {
229-
Option(options.get("token")).foreach { tok =>
230-
authConfigs.put("type", "static")
231-
authConfigs.put("token", tok)
232-
}
233-
}
234-
if (authConfigs.isEmpty) {
227+
catalogName: String): Unit = {
228+
val hasAuthPrefix = options.entrySet().asScala.exists(_.getKey.startsWith(AuthPrefix))
229+
val hasLegacyToken = options.get(LegacyTokenKey) != null
230+
if (!hasAuthPrefix && !hasLegacyToken) {
235231
throw new IllegalArgumentException(
236232
s"auth configuration is required when 'deltaRestApi.enabled' is true " +
237-
s"(catalog '$catalogName'). Set either 'auth.type' (with the corresponding " +
238-
s"auth.* keys) or the legacy 'token' option.")
233+
s"(catalog '$catalogName'). Set either '${AuthPrefix}type' (with the corresponding " +
234+
s"$AuthPrefix* keys) or the legacy '$LegacyTokenKey' option.")
239235
}
240-
authConfigs
241236
}
242237
}

0 commit comments

Comments
 (0)