Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cf9c88b
Add reflection-based auto-registration for ServerSidePlanningClientFa…
murali-db Jan 22, 2026
d8df656
Simplify documentation comments
murali-db Jan 22, 2026
1ecfc3b
Clean up comments
murali-db Jan 22, 2026
2371d01
Throw specific errors for factory loading failures
murali-db Jan 22, 2026
7758d5b
Rename serviceLoaderAttempted to autoRegistrationAttempted
murali-db Jan 22, 2026
fcf1333
Simplify error handling to single catch block
murali-db Jan 22, 2026
2f1ddf2
Fix scalastyle line length violations
murali-db Jan 22, 2026
c6ccf01
Fix remaining scalastyle violations
murali-db Jan 22, 2026
b9fedac
Remove unused DeltaLogging import and trait extension
murali-db Jan 22, 2026
2f60c19
Add comprehensive tests for ServerSidePlanningClientFactory auto-regi…
murali-db Jan 23, 2026
c81fbc5
Refactor ServerSidePlanningClientFactorySuite to eliminate duplication
murali-db Jan 23, 2026
27e2588
Remove unnecessary FactoryAction sealed trait abstraction
murali-db Jan 23, 2026
1385732
Move auto-registration tests from spark to iceberg module
murali-db Jan 23, 2026
005a9db
Remove redundant auto-registration test cases
murali-db Jan 23, 2026
b984757
Extract class name constant and add synchronization to factory methods
murali-db Jan 23, 2026
fda4644
Remove unnecessary assertSameInstance helper function
murali-db Jan 23, 2026
9599b17
Remove redundant comments from isFactoryRegistered and getFactoryInfo
murali-db Jan 23, 2026
db2c72f
Remove redundant tests and rename getFactoryInfo to getRegisteredFact…
murali-db Jan 23, 2026
e26657c
Remove redundant test cases to keep only essential coverage
murali-db Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,8 @@ lazy val testDeltaIcebergJar = (project in file("testDeltaIcebergJar"))
val deltaIcebergSparkIncludePrefixes = Seq(
// We want everything from this package
"org/apache/spark/sql/delta/icebergShaded",
// Server-side planning support
"org/apache/spark/sql/delta/serverSidePlanning",

// We only want the files in this project from this package. e.g. we want to exclude
// org/apache/spark/sql/delta/commands/convert/ConvertTargetFile.class (from delta-spark project).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,48 @@ private[serverSidePlanning] trait ServerSidePlanningClientFactory {
}

/**
* Registry for client factories. Can be configured for testing or to provide
* production implementations (e.g., IcebergRESTCatalogPlanningClientFactory).
*
* By default, no factory is registered. Production code should register an appropriate
* factory implementation before attempting to create clients.
* Registry for client factories. Automatically discovers and registers implementations
* using reflection-based auto-discovery on first access to the factory. Manual registration
* using setFactory() is only needed for testing or to override the auto-discovered factory.
*/
private[serverSidePlanning] object ServerSidePlanningClientFactory {
@volatile private var registeredFactory: Option[ServerSidePlanningClientFactory] = None
@volatile private var autoRegistrationAttempted: Boolean = false

// Lazy initialization - only runs when getFactory() is called and no factory is set.
// Uses reflection to load the hardcoded IcebergRESTCatalogPlanningClientFactory class.
private def tryAutoRegisterFactory(): Unit = {
// Double-checked locking pattern to ensure initialization happens only once
if (!autoRegistrationAttempted) {
synchronized {
if (!autoRegistrationAttempted) {
autoRegistrationAttempted = true

try {
// Use reflection to load the Iceberg factory class
// scalastyle:off classforname
val clazz = Class.forName(
"org.apache.spark.sql.delta.serverSidePlanning." +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should this be a const in the object?

"IcebergRESTCatalogPlanningClientFactory")
// scalastyle:on classforname
val factory = clazz.getConstructor().newInstance()
.asInstanceOf[ServerSidePlanningClientFactory]
registeredFactory = Some(factory)
} catch {
case e: Exception =>
throw new IllegalStateException(
"Unable to load IcebergRESTCatalogPlanningClientFactory " +
"for server-side planning. Ensure the delta-iceberg JAR is on the " +
"classpath and compatible with this Delta version.",
e)
}
}
}
}
}

/**
* Set a factory for production use or testing.
* Set a factory, overriding any auto-registered factory.
*/
private[serverSidePlanning] def setFactory(factory: ServerSidePlanningClientFactory): Unit = {
registeredFactory = Some(factory)
Expand All @@ -91,20 +122,40 @@ private[serverSidePlanning] object ServerSidePlanningClientFactory {
*/
private[serverSidePlanning] def clearFactory(): Unit = {
registeredFactory = None
autoRegistrationAttempted = false
}

/**
* Get the currently registered factory.
* Throws IllegalStateException if no factory has been registered.
* Throws IllegalStateException if no factory has been registered (either via reflection-based
* auto-discovery or explicit setFactory() call).
*/
def getFactory(): ServerSidePlanningClientFactory = {
// Try auto-registration if not already attempted and no factory is manually set
if (registeredFactory.isEmpty) {
tryAutoRegisterFactory()
}

registeredFactory.getOrElse {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the dead access of the registeredFactory be in synchronized as well?

Alternatively registeredFactory is marked as volatile and the only synchronization needed is only in the auto registration so that multiple thread don't register concurrently.

throw new IllegalStateException(
"No ServerSidePlanningClientFactory has been registered. " +
"Call ServerSidePlanningClientFactory.setFactory() to register an implementation.")
"Ensure delta-iceberg JAR is on the classpath for auto-registration, " +
"or call ServerSidePlanningClientFactory.setFactory() to register manually.")
}
}

/**
* Check if a factory is currently registered (either via ServiceLoader or setFactory()).
* Useful for testing or conditional logic.
*/
def isFactoryRegistered(): Boolean = registeredFactory.isDefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these one line functions needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem to be used only for testing. Seems unnecessary additional public methods.


/**
* Get information about the currently registered factory for debugging/logging.
* Returns None if no factory is registered.
*/
def getFactoryInfo(): Option[String] = registeredFactory.map(_.getClass.getName)

/**
* Convenience method to create a client from metadata using the registered factory.
*/
Expand Down
2 changes: 2 additions & 0 deletions testDeltaIcebergJar/src/test/scala/JarSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class JarSuite extends AnyFunSuite {
"scala/",
// e.g. org/apache/spark/sql/delta/icebergShaded/IcebergTransactionUtils.class
"org/apache/spark/sql/delta/icebergShaded/",
// Server-side planning support
"org/apache/spark/sql/delta/serverSidePlanning/",
Copy link
Contributor

@tdas tdas Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need this changes now since you switched to reflection based approach?

Copy link
Contributor Author

@murali-db murali-db Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting some errors without this (java.lang.Exception: Prohibited jar classes found). here's what i think is happening: delta-iceberg has some sort of allowlist so it excludes classes by default (to prevent dupes vs delta-spark i guess). and i need to allow it in this suite so that it does not complain about a new class being present in the jar (due to including it in build.sbt) when it shouldn't .

// We explicitly include all the /delta/commands/convert classes we want, to ensure we don't
// accidentally pull in some from delta-spark package.
"org/apache/spark/sql/delta/commands/convert/IcebergFileManifest",
Expand Down
Loading