Skip to content

Commit bedec4f

Browse files
minskyaclaude
andcommitted
add dataflint-spark4-databricks shaded artifact for DBR 17.3+
Databricks Runtime 17.3 ships javax.servlet instead of jakarta.servlet, crashing the standard Spark 4 plugin at startup with NoClassDefFoundError on jakarta/servlet/Servlet (issue #47). Add a parallel SBT module pluginspark4databricks that source-shares with pluginspark4 but applies ShadeRule.rename("jakarta.servlet.**" -> "javax.servlet.@1") at assembly time, producing io.dataflint:dataflint-spark4-databricks_2.13. A Spark4DatabricksPageFactory subclass inverts the Databricks UI gate so the new jar enables the UI only on DBR (and silently degrades to listeners-only if accidentally installed on stock Spark 4); the original Spark4PageFactory is unchanged. Drop the Maven-Central verify step in cd.yml — it only checked spark_2.12 and didn't work for snapshots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7958e14 commit bedec4f

6 files changed

Lines changed: 101 additions & 31 deletions

File tree

.github/workflows/cd.yml

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -95,35 +95,6 @@ jobs:
9595
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
9696
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
9797

98-
- name: Verify artifact published
99-
run: |
100-
VERSION=$(grep 'lazy val versionNum' build.sbt | grep -oE '"[0-9.]+"' | tr -d '"')
101-
if [[ "$IS_RELEASE" == "true" ]]; then
102-
URL="https://repo1.maven.org/maven2/io/dataflint/spark_2.12/${VERSION}/spark_2.12-${VERSION}.pom"
103-
AUTH_ARGS=""
104-
EXPECTED_VERSION="${VERSION}"
105-
else
106-
URL="https://central.sonatype.com/repository/maven-snapshots/io/dataflint/spark_2.12/maven-metadata.xml"
107-
AUTH_ARGS="-u ${SONATYPE_USERNAME}:${SONATYPE_PASSWORD}"
108-
EXPECTED_VERSION="${VERSION}-SNAPSHOT"
109-
fi
110-
echo "Verifying $EXPECTED_VERSION at: $URL"
111-
sleep 15
112-
for i in $(seq 1 10); do
113-
if curl -sf $AUTH_ARGS "$URL" | grep -q "${EXPECTED_VERSION}"; then
114-
echo "Artifact published successfully"
115-
exit 0
116-
fi
117-
echo "Attempt $i/10 — waiting 30s..."
118-
sleep 30
119-
done
120-
echo "Artifact not found after 5 minutes"
121-
exit 1
122-
working-directory: ./spark-plugin
123-
env:
124-
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
125-
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
126-
12798
- name: Create Release
12899
if: github.event.inputs.release_type == 'official' || startsWith(github.ref, 'refs/tags/v')
129100
run: |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ spark-submit
125125
* There is also support for scala 2.13, if your spark cluster is using scala 2.13 change package name to io.dataflint:spark_**2.13**:0.9.6
126126
* For more installation options, including for **python** and **k8s spark-operator**, see [Install on Spark docs](https://dataflint.gitbook.io/dataflint-for-spark/getting-started/install-on-spark)
127127
* For installing DataFlint OSS in **spark history server** for observability on completed runs see [install on spark history server docs](https://dataflint.gitbook.io/dataflint-for-spark/getting-started/install-on-spark-history-server)
128-
* For installing DataFlint OSS on **DataBricks** see [install on databricks docs](https://dataflint.gitbook.io/dataflint-for-spark/getting-started/install-on-databricks)
128+
* For installing DataFlint OSS on **DataBricks** see [install on databricks docs](https://dataflint.gitbook.io/dataflint-for-spark/getting-started/install-on-databricks). Databricks Runtime 17.3+ ships `javax.servlet` instead of `jakarta.servlet`, so use the dedicated shaded artifact `io.dataflint:dataflint-spark4-databricks_2.13` (same plugin class — only the jar coordinate differs).
129129

130130
## How it Works
131131

spark-plugin/build.sbt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ lazy val dataflint = project
1212
plugin,
1313
pluginspark3,
1414
pluginspark4,
15+
pluginspark4databricks,
1516
example_3_1_3,
1617
example_3_2_4,
1718
example_3_3_3,
@@ -162,11 +163,66 @@ lazy val pluginspark4 = (project in file("pluginspark4"))
162163

163164
// Include source from plugin directory for self-contained build
164165
Compile / unmanagedSourceDirectories += (plugin / Compile / sourceDirectory).value / "scala",
165-
166+
166167
// Include resources from plugin directory for static UI files
167168
Compile / unmanagedResourceDirectories += (plugin / Compile / resourceDirectory).value
168169
)
169170

171+
lazy val pluginspark4databricks = (project in file("pluginspark4databricks"))
172+
.enablePlugins(AssemblyPlugin)
173+
.settings(
174+
name := "dataflint-spark4-databricks",
175+
organization := "io.dataflint",
176+
scalaVersion := scala213,
177+
crossScalaVersions := List(scala213), // Only Scala 2.13 for Spark 4.x
178+
version := (if (git.gitCurrentTags.value.exists(_.startsWith("v"))) {
179+
versionNum
180+
} else {
181+
versionNum + "-SNAPSHOT"
182+
}),
183+
libraryDependencies += "org.apache.spark" %% "spark-core" % "4.0.1" % "provided",
184+
libraryDependencies += "org.apache.spark" %% "spark-sql" % "4.0.1" % "provided",
185+
libraryDependencies += "com.amazonaws" % "aws-java-sdk-s3" % "1.12.470" % "provided",
186+
libraryDependencies += "org.apache.iceberg" %% "iceberg-spark-runtime-3.5" % "1.5.0" % "provided",
187+
libraryDependencies += "io.delta" %% "delta-spark" % "3.2.0" % "provided",
188+
189+
// Source-share with pluginspark4 + plugin so we don't duplicate code.
190+
Compile / unmanagedSourceDirectories += (pluginspark4 / Compile / sourceDirectory).value / "scala",
191+
Compile / unmanagedSourceDirectories += (plugin / Compile / sourceDirectory).value / "scala",
192+
Compile / unmanagedResourceDirectories += (plugin / Compile / resourceDirectory).value,
193+
194+
// Drop the upstream DataflintSparkUILoader so our local copy (which uses
195+
// Spark4DatabricksPageFactory) is the one compiled.
196+
Compile / unmanagedSources / excludeFilter := {
197+
val upstreamLoader = (pluginspark4 / Compile / sourceDirectory).value /
198+
"scala" / "org" / "apache" / "spark" / "dataflint" / "DataflintSparkUILoader.scala"
199+
new sbt.io.SimpleFileFilter(_.getCanonicalPath == upstreamLoader.getCanonicalPath)
200+
},
201+
202+
assembly / assemblyJarName := s"${name.value}_${scalaBinaryVersion.value}-${version.value}.jar",
203+
assembly / assemblyOption := (assembly / assemblyOption).value.withIncludeScala(false),
204+
// Rewrite jakarta.servlet → javax.servlet in our bytecode so the artifact
205+
// loads on Databricks Runtime 17.3, which ships javax instead of jakarta.
206+
assembly / assemblyShadeRules := Seq(
207+
ShadeRule.rename("jakarta.servlet.**" -> "javax.servlet.@1").inAll
208+
),
209+
assembly / assemblyMergeStrategy := {
210+
case PathList("META-INF", "services", xs @ _*) => MergeStrategy.concat
211+
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
212+
case "application.conf" => MergeStrategy.concat
213+
case "reference.conf" => MergeStrategy.concat
214+
case _ => MergeStrategy.first
215+
},
216+
217+
Compile / packageBin := assembly.value,
218+
publishTo := {
219+
if (isSnapshot.value)
220+
Some("snapshots" at "https://central.sonatype.com/repository/maven-snapshots/")
221+
else
222+
sonatypePublishToBundle.value
223+
}
224+
)
225+
170226
lazy val example_3_1_3 = (project in file("example_3_1_3"))
171227
.settings(
172228
name := "DataflintSparkExample313",

spark-plugin/clean-and-setup.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ sbt pluginspark3/assembly
3131
echo "Building Spark 4 fat JAR..."
3232
sbt ++2.13.16 pluginspark4/assembly
3333

34+
# Build Spark 4 Databricks fat JAR (jakarta.servlet → javax.servlet shaded)
35+
echo "Building Spark 4 Databricks fat JAR..."
36+
sbt ++2.13.16 pluginspark4databricks/assembly
37+
3438
echo "✅ Setup complete!"
3539
echo ""
3640
echo "📋 Next steps:"
@@ -40,4 +44,5 @@ echo ""
4044
echo "📦 Fat JARs created:"
4145
echo "- Spark 3.x: pluginspark3/target/scala-2.12/dataflint-spark3_2.12-0.9.7-SNAPSHOT.jar"
4246
echo "- Spark 4.x: pluginspark4/target/scala-2.13/dataflint-spark4_2.13-0.9.7-SNAPSHOT.jar"
47+
echo "- Spark 4 (Databricks): pluginspark4databricks/target/scala-2.13/dataflint-spark4-databricks_2.13-0.9.7-SNAPSHOT.jar"
4348

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.apache.spark.dataflint
2+
3+
import org.apache.spark.SparkContext
4+
import org.apache.spark.dataflint.api.Spark4DatabricksPageFactory
5+
import org.apache.spark.ui.SparkUI
6+
7+
/**
8+
* Databricks variant of the Spark 4 loader. Identical to the pluginspark4
9+
* loader except it instantiates [[Spark4DatabricksPageFactory]], which
10+
* inverts the Databricks UI gate so the shaded jar serves UI only on DBR.
11+
* Same FQN as the upstream loader so the shared SparkDataflintPlugin
12+
* entrypoint resolves it without any per-flavor wiring.
13+
*/
14+
object DataflintSparkUILoader {
15+
private val pageFactory = new Spark4DatabricksPageFactory()
16+
17+
def install(context: SparkContext): String =
18+
new org.apache.spark.dataflint.DataflintSparkUICommonInstaller().install(context, pageFactory)
19+
20+
def loadUI(ui: SparkUI): String =
21+
new org.apache.spark.dataflint.DataflintSparkUICommonInstaller().loadUI(ui, pageFactory)
22+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.apache.spark.dataflint.api
2+
3+
import org.apache.spark.ui.SparkUI
4+
5+
/**
6+
* Databricks variant of [[Spark4PageFactory]]. The parent class skips the
7+
* DataFlint UI on any Databricks runtime to avoid the jakarta.servlet
8+
* NoClassDefFoundError on DBR 17.3 (see issue #47). This subclass inverts
9+
* the check: enable UI ONLY on Databricks (where the javax-shaded bytecode
10+
* matches the runtime). If this jar is installed on stock Spark 4 by
11+
* mistake, the UI is silently skipped instead of crashing.
12+
*/
13+
class Spark4DatabricksPageFactory extends Spark4PageFactory {
14+
override def isUISupported(ui: SparkUI): Boolean =
15+
ui.conf.getOption("spark.databricks.clusterUsageTags.cloudProvider").isDefined
16+
}

0 commit comments

Comments
 (0)