@@ -18,23 +18,17 @@ package org.apache.spark.sql.delta.catalog
1818
1919import java .net .URI
2020import java .util .concurrent .atomic .AtomicLong
21- import java .util .function .Supplier
2221
2322import scala .jdk .CollectionConverters ._
2423
2524import 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 }
3126import io .delta .storage .commit .uccommitcoordinator .UCDeltaModels .TableInfo
3227import io .delta .storage .commit .uccommitcoordinator .exceptions .{
3328 CredentialFetchFailedException ,
3429 UnsupportedTableFormatException ,
3530 NoSuchTableException => StorageNoSuchTableException
3631}
37- import org .apache .hadoop .conf .Configuration
3832
3933import org .apache .spark .internal .{Logging , MDC }
4034import 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
152147object 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