Skip to content

Commit 840b101

Browse files
committed
Add ServiceLoader auto-registration for ServerSidePlanningClientFactory
Enable automatic discovery and registration of ServerSidePlanningClientFactory implementations using Java's ServiceLoader mechanism. When delta-iceberg JAR is on the classpath, IcebergRESTCatalogPlanningClientFactory is automatically registered, eliminating the need for manual registration. Changes: - Add META-INF/services file for IcebergRESTCatalogPlanningClientFactory - Implement ServiceLoader discovery in ServerSidePlanningClientFactory - Include serverSidePlanning package in delta-iceberg JAR (build.sbt) - Add helper methods for factory registration introspection
1 parent bb66898 commit 840b101

File tree

3 files changed

+83
-8
lines changed

3 files changed

+83
-8
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).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.apache.spark.sql.delta.serverSidePlanning.IcebergRESTCatalogPlanningClientFactory

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

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,41 +70,113 @@ private[serverSidePlanning] trait ServerSidePlanningClientFactory {
7070
}
7171

7272
/**
73-
* Registry for client factories. Can be configured for testing or to provide
74-
* production implementations (e.g., IcebergRESTCatalogPlanningClientFactory).
73+
* Registry for client factories. Automatically discovers and registers implementations
74+
* using Java's ServiceLoader mechanism at first access.
7575
*
76-
* By default, no factory is registered. Production code should register an appropriate
77-
* factory implementation before attempting to create clients.
76+
* When delta-iceberg JAR is on the classpath, IcebergRESTCatalogPlanningClientFactory
77+
* is automatically registered via META-INF/services discovery. Manual registration
78+
* using setFactory() is only needed for testing or to override the auto-discovered factory.
7879
*/
7980
private[serverSidePlanning] object ServerSidePlanningClientFactory {
8081
@volatile private var registeredFactory: Option[ServerSidePlanningClientFactory] = None
8182

83+
// ========== SERVICE LOADER AUTO-REGISTRATION ==========
84+
// This block runs ONCE when ServerSidePlanningClientFactory is first accessed.
85+
// It uses Java's ServiceLoader to discover and register factories declared in
86+
// META-INF/services/org.apache.spark.sql.delta.serverSidePlanning.ServerSidePlanningClientFactory
87+
{
88+
import java.util.ServiceLoader
89+
import scala.jdk.CollectionConverters._
90+
91+
try {
92+
// Use ServiceLoader to discover all implementations on the classpath
93+
val loader = ServiceLoader.load(
94+
classOf[ServerSidePlanningClientFactory],
95+
Thread.currentThread().getContextClassLoader)
96+
97+
// Convert Java Iterator to Scala and collect all factories
98+
val factories = loader.iterator().asScala.toList
99+
100+
if (factories.nonEmpty) {
101+
// Use the first discovered factory
102+
// In practice, there should only be one (IcebergRESTCatalogPlanningClientFactory)
103+
registeredFactory = Some(factories.head)
104+
105+
// Optional: Log successful registration for debugging
106+
// Uncomment for visibility during development
107+
// System.err.println(s"[Delta] Auto-registered ${factories.head.getClass.getName} " +
108+
// s"via ServiceLoader (found ${factories.size} implementation(s))")
109+
110+
// Optional: Warn if multiple implementations found
111+
if (factories.size > 1) {
112+
// scalastyle:off println
113+
System.err.println(
114+
s"[Delta] Warning: Multiple ServerSidePlanningClientFactory implementations found. " +
115+
s"Using ${factories.head.getClass.getName}. " +
116+
s"Others: ${factories.tail.map(_.getClass.getName).mkString(", ")}")
117+
// scalastyle:on println
118+
}
119+
} else {
120+
// No factories discovered - delta-iceberg not on classpath
121+
// This is fine, server-side planning just won't be available
122+
// System.err.println("[Delta] No ServerSidePlanningClientFactory found, FGAC disabled")
123+
}
124+
125+
} catch {
126+
case e: Exception =>
127+
// Unexpected error during service loading - log but don't fail
128+
// Delta should still work for non-FGAC tables
129+
// scalastyle:off println
130+
System.err.println(
131+
s"[Delta] Warning: Failed to auto-discover server-side planning factory: ${e.getMessage}")
132+
// scalastyle:on println
133+
}
134+
}
135+
// ========== END SERVICE LOADER AUTO-REGISTRATION ==========
136+
82137
/**
83-
* Set a factory for production use or testing.
138+
* Set a factory, overriding any auto-registered factory.
139+
* Primarily useful for testing or providing custom implementations.
84140
*/
85141
private[serverSidePlanning] def setFactory(factory: ServerSidePlanningClientFactory): Unit = {
86142
registeredFactory = Some(factory)
87143
}
88144

89145
/**
90-
* Clear the registered factory.
146+
* Clear the registered factory. Primarily useful for testing to reset state between tests.
147+
* Note: This does NOT re-trigger ServiceLoader discovery. The factory must be manually
148+
* set again via setFactory() if needed.
91149
*/
92150
private[serverSidePlanning] def clearFactory(): Unit = {
93151
registeredFactory = None
94152
}
95153

96154
/**
97155
* Get the currently registered factory.
98-
* Throws IllegalStateException if no factory has been registered.
156+
* Throws IllegalStateException if no factory has been registered (either via ServiceLoader
157+
* auto-discovery or explicit setFactory() call).
99158
*/
100159
def getFactory(): ServerSidePlanningClientFactory = {
101160
registeredFactory.getOrElse {
102161
throw new IllegalStateException(
103162
"No ServerSidePlanningClientFactory has been registered. " +
104-
"Call ServerSidePlanningClientFactory.setFactory() to register an implementation.")
163+
"Ensure delta-iceberg JAR is on the classpath for auto-registration, " +
164+
"or call ServerSidePlanningClientFactory.setFactory() to register manually.")
105165
}
106166
}
107167

168+
/**
169+
* Check if a factory is currently registered (either via ServiceLoader or setFactory()).
170+
* Useful for testing or conditional logic.
171+
*/
172+
def isFactoryRegistered(): Boolean = registeredFactory.isDefined
173+
174+
/**
175+
* Get information about the currently registered factory for debugging/logging.
176+
* Returns None if no factory is registered.
177+
*/
178+
def getFactoryInfo(): Option[String] = registeredFactory.map(_.getClass.getName)
179+
108180
/**
109181
* Convenience method to create a client from metadata using the registered factory.
110182
*/

0 commit comments

Comments
 (0)