Skip to content

Commit cf9c88b

Browse files
committed
Add reflection-based auto-registration for ServerSidePlanningClientFactory
Enable automatic discovery and registration of ServerSidePlanningClientFactory using reflection with Class.forName(). When delta-iceberg JAR is on the classpath, the IcebergRESTCatalogPlanningClientFactory is automatically loaded and registered. Uses lazy initialization with double-checked locking to maintain Spark Connect compatibility and avoid eager initialization issues. Changes: - Replace ServiceLoader with Class.forName() reflection in ServerSidePlanningClient.scala - Use proper DeltaLogging instead of System.err.println for warnings - Include serverSidePlanning package in delta-iceberg JAR (build.sbt) - Update JarSuite to verify serverSidePlanning classes are included in JAR
1 parent bb66898 commit cf9c88b

File tree

3 files changed

+70
-9
lines changed

3 files changed

+70
-9
lines changed

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,8 @@ lazy val testDeltaIcebergJar = (project in file("testDeltaIcebergJar"))
11051105
val deltaIcebergSparkIncludePrefixes = Seq(
11061106
// We want everything from this package
11071107
"org/apache/spark/sql/delta/icebergShaded",
1108+
// Server-side planning support for Unity Catalog
1109+
"org/apache/spark/sql/delta/serverSidePlanning",
11081110

11091111
// We only want the files in this project from this package. e.g. we want to exclude
11101112
// org/apache/spark/sql/delta/commands/convert/ConvertTargetFile.class (from delta-spark project).

spark/src/main/scala/org/apache/spark/sql/delta/serverSidePlanning/ServerSidePlanningClient.scala

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.spark.sql.delta.serverSidePlanning
1818

1919
import org.apache.spark.sql.SparkSession
20+
import org.apache.spark.sql.delta.metering.DeltaLogging
2021
import org.apache.spark.sql.sources.Filter
2122

2223
/**
@@ -70,41 +71,97 @@ private[serverSidePlanning] trait ServerSidePlanningClientFactory {
7071
}
7172

7273
/**
73-
* Registry for client factories. Can be configured for testing or to provide
74-
* production implementations (e.g., IcebergRESTCatalogPlanningClientFactory).
74+
* Registry for client factories. Automatically discovers and registers implementations
75+
* using reflection-based auto-discovery on first access to the factory.
7576
*
76-
* By default, no factory is registered. Production code should register an appropriate
77-
* factory implementation before attempting to create clients.
77+
* When delta-iceberg JAR is on the classpath, IcebergRESTCatalogPlanningClientFactory
78+
* is automatically registered via reflection with a hardcoded class name. Manual registration
79+
* using setFactory() is only needed for testing or to override the auto-discovered factory.
7880
*/
79-
private[serverSidePlanning] object ServerSidePlanningClientFactory {
81+
private[serverSidePlanning] object ServerSidePlanningClientFactory extends DeltaLogging {
8082
@volatile private var registeredFactory: Option[ServerSidePlanningClientFactory] = None
83+
@volatile private var serviceLoaderAttempted: Boolean = false
84+
85+
// ========== REFLECTION-BASED AUTO-REGISTRATION ==========
86+
// Lazy initialization - only runs when getFactory() is called and no factory is set.
87+
// Uses reflection to load the hardcoded IcebergRESTCatalogPlanningClientFactory class.
88+
private def tryAutoRegisterFactory(): Unit = {
89+
// Double-checked locking pattern to ensure initialization happens only once
90+
if (!serviceLoaderAttempted) {
91+
synchronized {
92+
if (!serviceLoaderAttempted) {
93+
serviceLoaderAttempted = true
94+
95+
try {
96+
// Use reflection to load the Iceberg factory class
97+
val clazz = Class.forName(
98+
"org.apache.spark.sql.delta.serverSidePlanning.IcebergRESTCatalogPlanningClientFactory")
99+
val factory = clazz.getConstructor().newInstance()
100+
.asInstanceOf[ServerSidePlanningClientFactory]
101+
registeredFactory = Some(factory)
102+
} catch {
103+
case _: ClassNotFoundException =>
104+
// delta-iceberg not on classpath, no factory available
105+
// This is fine - server-side planning just won't be available
106+
case e: Exception =>
107+
// Unexpected error during reflection - log but don't fail
108+
logWarning(s"Failed to load server-side planning factory: ${e.getMessage}")
109+
}
110+
}
111+
}
112+
}
113+
}
114+
// ========== END REFLECTION-BASED AUTO-REGISTRATION ==========
81115

82116
/**
83-
* Set a factory for production use or testing.
117+
* Set a factory, overriding any auto-registered factory.
118+
* Primarily useful for testing or providing custom implementations.
84119
*/
85120
private[serverSidePlanning] def setFactory(factory: ServerSidePlanningClientFactory): Unit = {
86121
registeredFactory = Some(factory)
87122
}
88123

89124
/**
90-
* Clear the registered factory.
125+
* Clear the registered factory. Primarily useful for testing to reset state between tests.
126+
* This also resets the ServiceLoader discovery state, allowing it to be re-triggered on
127+
* the next getFactory() call.
91128
*/
92129
private[serverSidePlanning] def clearFactory(): Unit = {
93130
registeredFactory = None
131+
serviceLoaderAttempted = false
94132
}
95133

96134
/**
97135
* Get the currently registered factory.
98-
* Throws IllegalStateException if no factory has been registered.
136+
* Throws IllegalStateException if no factory has been registered (either via ServiceLoader
137+
* auto-discovery or explicit setFactory() call).
99138
*/
100139
def getFactory(): ServerSidePlanningClientFactory = {
140+
// Try auto-registration if not already attempted and no factory is manually set
141+
if (registeredFactory.isEmpty) {
142+
tryAutoRegisterFactory()
143+
}
144+
101145
registeredFactory.getOrElse {
102146
throw new IllegalStateException(
103147
"No ServerSidePlanningClientFactory has been registered. " +
104-
"Call ServerSidePlanningClientFactory.setFactory() to register an implementation.")
148+
"Ensure delta-iceberg JAR is on the classpath for auto-registration, " +
149+
"or call ServerSidePlanningClientFactory.setFactory() to register manually.")
105150
}
106151
}
107152

153+
/**
154+
* Check if a factory is currently registered (either via ServiceLoader or setFactory()).
155+
* Useful for testing or conditional logic.
156+
*/
157+
def isFactoryRegistered(): Boolean = registeredFactory.isDefined
158+
159+
/**
160+
* Get information about the currently registered factory for debugging/logging.
161+
* Returns None if no factory is registered.
162+
*/
163+
def getFactoryInfo(): Option[String] = registeredFactory.map(_.getClass.getName)
164+
108165
/**
109166
* Convenience method to create a client from metadata using the registered factory.
110167
*/

testDeltaIcebergJar/src/test/scala/JarSuite.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class JarSuite extends AnyFunSuite {
3232
"scala/",
3333
// e.g. org/apache/spark/sql/delta/icebergShaded/IcebergTransactionUtils.class
3434
"org/apache/spark/sql/delta/icebergShaded/",
35+
// Server-side planning support for Unity Catalog (FGAC)
36+
"org/apache/spark/sql/delta/serverSidePlanning/",
3537
// We explicitly include all the /delta/commands/convert classes we want, to ensure we don't
3638
// accidentally pull in some from delta-spark package.
3739
"org/apache/spark/sql/delta/commands/convert/IcebergFileManifest",

0 commit comments

Comments
 (0)