Skip to content

Commit 1f949fc

Browse files
committed
feat(gradle-plugin): enhance Spark SQL prep and extension runtime management
- Updated Gradle plugin to support Spark SQL preparation inside Gradle JVM with enhanced logging and module access configuration. - Added support for Iceberg Spark extensions and shaded runtime artifacts for improved dependency isolation. - Introduced extension resource directories for better resource management. - Enhanced extension runtime classpath configuration with added dependencies for AWS SDK, Iceberg AWS bundle, and servlet API. - Updated documentation to reflect new plugin configurations, logging options, and resource handling.
1 parent b497b70 commit 1f949fc

12 files changed

Lines changed: 281 additions & 23 deletions

File tree

doc/user-guide.adoc

Lines changed: 134 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,7 +1622,7 @@ If `hiveMetastore = true` fails for HMS-backed object-store tables, see link:hiv
16221622

16231623
== Gradle Plugin
16241624

1625-
Use the Gradle plugin when the test kit should be managed by Gradle instead of the application or test framework. This keeps container startup out of the Spring application context and out of JUnit annotations, while still starting the services before `bootRun`, `run`, `JavaExec`, or `Test` tasks launch their JVMs.
1625+
Use the Gradle plugin when the test kit should be managed by Gradle instead of the application or test framework. This keeps container startup out of the Spring application context and out of JUnit annotations, while still starting the services before `bootRun`, `run`, or other `JavaExec` tasks launch their JVMs. `Test` task wiring is opt-in.
16261626

16271627
[source,kotlin]
16281628
----
@@ -1689,7 +1689,11 @@ bigDataTest {
16891689

16901690
Extension provisioning dependencies are resolved into the plugin-owned `bigDataTestExtensionRuntime` configuration. They are not added to the application `implementation`, `runtimeClasspath`, `testImplementation`, or `testRuntimeClasspath` configurations.
16911691

1692-
By default the plugin adds the lightweight `extensions` artifact and then adds runtime libraries based on the configured extension TOML:
1692+
By default the plugin uses the shaded `extensions:<version>:runtime` artifact. This keeps Spark, Hadoop, Kafka, Avro, thin Iceberg Spark modules, and their transitive runtime dependencies managed by bigdata-test instead of letting the application dependency graph choose those versions. The extension runtime classloader prefers its own jars and delegates shared bigdata-test core types to the Gradle plugin parent classloader.
1693+
1694+
Application dependencies such as `implementation`, `runtimeOnly`, `testImplementation`, and `testRuntimeOnly` are not added to the bigdata-test extension runtime. Dependencies explicitly added to `bigDataTestExtensionRuntime` are different: they are intentionally part of the extension runtime and can affect extension execution. Use that configuration only for optional extension-only libraries that bigdata-test does not already provide.
1695+
1696+
If you need to debug or override individual runtime dependencies, disable the shaded artifact. In that mode, the plugin adds the lightweight `extensions` artifact and then adds runtime libraries based on the configured extension TOML:
16931697

16941698
* `[s3Jceks]` enables Hadoop client/runtime/AWS dependencies.
16951699
* `[kafkaAvro]` enables Confluent Schema Registry, Kafka Avro, and Avro dependencies.
@@ -1701,6 +1705,7 @@ Override dependency versions with the DSL:
17011705
----
17021706
bigDataTest {
17031707
extensionRuntime {
1708+
useShadedArtifact.set(false)
17041709
hadoopVersion.set("3.4.2")
17051710
sparkVersion.set("3.5.7")
17061711
confluentVersion.set("8.2.1")
@@ -1713,7 +1718,16 @@ bigDataTest {
17131718

17141719
When Spark and Confluent Kafka dependencies are both present, the plugin selects `at.yawk.lz4:lz4-java` for Gradle's shared `org.lz4:lz4-java` capability. Override `lz4Version` if the selected Confluent line uses a different compatible lz4 artifact.
17151720

1716-
For Hadoop S3A, the plugin excludes `software.amazon.awssdk:bundle` from `hadoop-aws` and adds the modular `software.amazon.awssdk:s3` dependency instead. This keeps the extension runtime focused on S3 support instead of shading every AWS service client.
1721+
For Hadoop S3A, the plugin excludes `software.amazon.awssdk:bundle` from `hadoop-aws` and adds the modular `software.amazon.awssdk:s3` and `software.amazon.awssdk:s3-transfer-manager` dependencies instead. S3A loads transfer-manager classes at runtime, so both modules are required while still avoiding the full AWS SDK bundle.
1722+
1723+
When `spark-sql-prep` is used by `bigDataTestStart` or `bigDataTestRun`, Spark starts inside the Gradle JVM. On Java 17, add Spark's required module access to the Gradle daemon JVM, for example in `gradle.properties`:
1724+
1725+
[source,properties]
1726+
----
1727+
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED
1728+
----
1729+
1730+
These flags cannot be applied after the Gradle daemon has already started. If a daemon was already running without them, stop it with `./gradlew --stop` or run the next command with `--no-daemon`.
17171731

17181732
When auto-detection is not enough, force dependency groups explicitly:
17191733

@@ -1729,7 +1743,7 @@ bigDataTest {
17291743
}
17301744
----
17311745

1732-
The `extensions` module also publishes a shaded runtime artifact with classifier `runtime`. It bundles the extension runtime dependencies for users who prefer one plugin runtime artifact instead of dynamic dependency sets:
1746+
The `extensions` module publishes the shaded runtime artifact with classifier `runtime`. This is the default plugin mode:
17331747

17341748
[source,kotlin]
17351749
----
@@ -1741,7 +1755,75 @@ bigDataTest {
17411755
}
17421756
----
17431757

1744-
With `useShadedArtifact = true`, the plugin resolves `org.openprojectx.bigdata.test.core:extensions:<version>:runtime@jar` into `bigDataTestExtensionRuntime` and skips adding separate Hadoop, Spark, Kafka, and Avro extension runtime dependencies.
1758+
With `useShadedArtifact = true`, the plugin resolves `org.openprojectx.bigdata.test.core:extensions:<version>:runtime@jar` into `bigDataTestExtensionRuntime` and skips adding separate Hadoop, Spark, Iceberg, Kafka, and Avro extension runtime dependencies.
1759+
1760+
The normal `extensions` artifact does not package Spark, Hadoop, Kafka, Log4j, or SLF4J implementations; those dependencies are supplied by the plugin-owned runtime configuration when needed. The shaded `extensions:<version>:runtime` artifact does package extension runtime dependencies, including logging libraries pulled in transitively by Spark such as `slf4j-api`, `log4j-api`, `log4j-core`, and `log4j-slf4j2-impl`. Jackson is relocated inside this shaded artifact to avoid collisions with Jackson versions used by the application or Gradle build logic.
1761+
1762+
=== Gradle Plugin Customization Surface
1763+
1764+
The Gradle plugin has three separate classpath/resource concepts:
1765+
1766+
[cols="1,2,2",options="header"]
1767+
|===
1768+
|Area |How to customize |Notes
1769+
1770+
|Application classpath
1771+
|Use normal Gradle configurations such as `implementation`, `runtimeOnly`, `testImplementation`, and `testRuntimeOnly`.
1772+
|These dependencies are used by the app and tests. They are not added to `bigDataTestRun` or extension provisioning.
1773+
1774+
|Extension runtime classpath
1775+
|Use the `bigDataTestExtensionRuntime` configuration, or set `extensionRuntime.useShadedArtifact.set(false)` and configure `hadoopVersion`, `sparkVersion`, `icebergVersion`, `confluentVersion`, `avroVersion`, `awsSdkVersion`, and `lz4Version`.
1776+
|This classpath is used only by bigdata-test extension execution. Dependencies added here can affect `spark-sql-prep`, JCEKS generation, Kafka Avro seeding, and other extensions.
1777+
1778+
|Extension config and SQL/resource lookup
1779+
|Use `extensionConfig.add(...)`. `classpath:` resources are searched from the extension runtime first and then from the project's main resource directories, including `src/main/resources` and compiled main resources.
1780+
|There is no dedicated DSL property for extra arbitrary resource directories today. Put files under main resources, reference an absolute or relative `file:` path, or add a small resource jar to `bigDataTestExtensionRuntime`.
1781+
1782+
|Gradle task JVM
1783+
|Configure Gradle itself with `org.gradle.jvmargs`, `GRADLE_OPTS`, or `JAVA_TOOL_OPTIONS`.
1784+
|`bigDataTestStart` and `bigDataTestRun` execute inside the Gradle JVM. Spark module-open flags and logging configuration for Spark SQL preparation must be visible when the Gradle daemon starts.
1785+
1786+
|Gradle task environment
1787+
|Set environment variables on the Gradle command, for example `TESTCONTAINERS_RYUK_DISABLED=true ./gradlew bigDataTestRun`.
1788+
|`bigDataTestRun` is not a `JavaExec` task, so it does not have a task-local `environment {}` API. It sees the Gradle process environment.
1789+
1790+
|Application task injected properties
1791+
|Set `injectRawEndpointProperties`, `injectNamespacedEndpointProperties`, and `injectEnvironmentVariables`.
1792+
|For auto-wired `JavaExec` and opt-in `Test` tasks, endpoint and extension output values are injected as JVM system properties and, when enabled, environment variables.
1793+
1794+
|Container environment, files, mounts, ports, network mode
1795+
|Use startup TOML `[containers.<service>.*]` tables or programmatic `withContainerCustomization(...)`.
1796+
|This customizes Docker containers, not the Gradle task process. Prefer copied files over host mounts in CI.
1797+
|===
1798+
1799+
Example extension-only dependency override:
1800+
1801+
[source,kotlin]
1802+
----
1803+
dependencies {
1804+
add("bigDataTestExtensionRuntime", "com.example:test-only-extension-helper:1.0.0")
1805+
}
1806+
----
1807+
1808+
Example task process environment:
1809+
1810+
[source,bash]
1811+
----
1812+
TESTCONTAINERS_RYUK_DISABLED=true GRADLE_USER_HOME=/data/.gradle ./gradlew bigDataTestRun
1813+
----
1814+
1815+
Example resource-backed Spark SQL preparation:
1816+
1817+
[source,toml]
1818+
----
1819+
[[extensions]]
1820+
type = "spark-sql-prep"
1821+
scripts = ["classpath:sql/create_tables.sql"]
1822+
----
1823+
1824+
Place the script at `src/main/resources/sql/create_tables.sql`, or use a `file:` URL when the SQL lives outside the project resources.
1825+
1826+
The shaded runtime jar intentionally favors isolation over small size. It uses the thin Iceberg Spark modules instead of the fat `iceberg-spark-runtime` jar, but still includes enough Hadoop, Spark, Iceberg, Kafka Avro, and AWS runtime code for config-driven provisioning without borrowing the user application's dependency graph. The largest remaining optional payload is `iceberg-aws-bundle`, which is kept for Iceberg S3 FileIO and AWS catalog support. If size or dependency inspection matters more than isolation, set `useShadedArtifact = false` and let the plugin resolve only the runtime families detected from `extensionConfig`, or add the exact artifacts you want to `bigDataTestExtensionRuntime`.
17451827

17461828
Gradle configuration is Gradle-native; the plugin does not read a Maven POM. When values should come from `gradle.properties` or `-P`, wire them through Gradle providers:
17471829

@@ -1763,11 +1845,11 @@ bigDataTest {
17631845

17641846
The plugin registers:
17651847

1766-
* `bigDataTestStart` - starts the configured kit for the current Gradle build and then returns. This task is intended as a dependency of app/test tasks.
1848+
* `bigDataTestStart` - starts the configured kit for the current Gradle build and then returns. This task is intended as a dependency of app tasks, or test tasks when explicitly opted in.
17671849
* `bigDataTestRun` - starts the configured kit and keeps the Gradle process running until interrupted. Use this for manual troubleshooting.
17681850
* `bigDataTestStop` - explicitly stops the kit in the current Gradle build.
17691851

1770-
By default, the plugin wires `bigDataTestStart` before `JavaExec` and `Test` tasks and injects endpoint and extension output properties as JVM system properties immediately before the task launches. It also injects environment variables derived from the same property map. The kit is closed automatically when the Gradle build service closes at the end of the build.
1852+
By default, the plugin wires `bigDataTestStart` before `JavaExec` tasks and injects endpoint and extension output properties as JVM system properties immediately before the task launches. It also injects environment variables derived from the same property map. `Test` tasks are not auto-wired by default; set `autoConfigureTestTasks.set(true)` when tests should start and receive the kit automatically. The kit is closed automatically when the Gradle build service closes at the end of the build.
17711853

17721854
`bigDataTestStart` is not an interactive keep-alive command. If you run it directly, it starts the kit, prints the injected property count, and exits when the build finishes. For a long-running local environment, run:
17731855

@@ -1783,6 +1865,51 @@ Testcontainers runtime environment variables, such as `TESTCONTAINERS_RYUK_DISAB
17831865
TESTCONTAINERS_RYUK_DISABLED=true GRADLE_USER_HOME=/data/.gradle ./gradlew --no-daemon bigDataTestRun
17841866
----
17851867

1868+
=== Gradle Plugin Logging
1869+
1870+
`bigDataTestRun` starts the kit and extension provisioning inside the Gradle JVM. Configure JVM logging through Gradle daemon JVM properties, not through the application `bootRun` or `test` task.
1871+
1872+
For the default plugin logging backend, use SLF4J Simple properties in `gradle.properties`:
1873+
1874+
[source,properties]
1875+
----
1876+
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 \
1877+
--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \
1878+
--add-opens=java.base/java.nio=ALL-UNNAMED \
1879+
--add-opens=java.base/java.net=ALL-UNNAMED \
1880+
-Dorg.slf4j.simpleLogger.defaultLogLevel=info \
1881+
-Dorg.slf4j.simpleLogger.log.org.openprojectx.bigdata.test=debug \
1882+
-Dorg.slf4j.simpleLogger.log.org.testcontainers=info
1883+
----
1884+
1885+
Spark SQL preparation is different when it runs through the Gradle plugin: Spark starts in-process inside the Gradle daemon, and Spark logs are routed through Gradle's logging bridge. Gradle controls the console rendering, so Log4j2 console pattern and ANSI color settings are not honored for those Spark extension logs. Use Gradle flags such as `--info` and extension/Spark log-level settings for verbosity. Full Log4j2 pattern/color control would require a separate Spark preparation JVM instead of the current in-process plugin execution model.
1886+
1887+
If a Gradle daemon is already running, restart it before expecting new `org.gradle.jvmargs` values to apply:
1888+
1889+
[source,bash]
1890+
----
1891+
./gradlew --stop
1892+
GRADLE_USER_HOME=/data/.gradle ./gradlew bigDataTestRun --info
1893+
----
1894+
1895+
Container logs are configured separately from Gradle JVM logs. Use startup TOML when you want service stdout/stderr in files:
1896+
1897+
[source,toml]
1898+
----
1899+
[containerLogs]
1900+
mode = "FILE"
1901+
directory = "build/container-logs"
1902+
append = false
1903+
----
1904+
1905+
or send container logs to the Gradle console:
1906+
1907+
[source,toml]
1908+
----
1909+
[containerLogs]
1910+
mode = "STDOUT"
1911+
----
1912+
17861913
Disable automatic task wiring when you want explicit task dependencies:
17871914

17881915
[source,kotlin]

example/spark/build.gradle.kts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,11 @@ val apacheSparkRuntimeClasspath = createSparkRuntimeClasspath("apacheSparkRuntim
9797
val clouderaSparkRuntimeClasspath = createSparkRuntimeClasspath("clouderaSparkRuntimeClasspath", "cloudera")
9898

9999
dependencies {
100-
add(apacheSparkRuntimeClasspath.name, "org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:$icebergVersion")
100+
add(apacheSparkRuntimeClasspath.name, "org.apache.iceberg:iceberg-spark-3.5_2.12:$icebergVersion")
101+
add(apacheSparkRuntimeClasspath.name, "org.apache.iceberg:iceberg-spark-extensions-3.5_2.12:$icebergVersion")
101102
add(apacheSparkRuntimeClasspath.name, "org.apache.logging.log4j:log4j-slf4j-impl:2.20.0")
102-
add(clouderaSparkRuntimeClasspath.name, "org.apache.iceberg:iceberg-spark-runtime-3.3_2.12")
103+
add(clouderaSparkRuntimeClasspath.name, "org.apache.iceberg:iceberg-spark-3.3_2.12")
104+
add(clouderaSparkRuntimeClasspath.name, "org.apache.iceberg:iceberg-spark-extensions-3.3_2.12")
103105
}
104106

105107
fun Test.useSparkRuntimeClasspath(runtimeClasspath: Configuration, dependencyLine: String) {

example/spring-gradle-plugin/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@ Spring JVM, runs the TOML extensions, and injects endpoint/extension output as J
3333
properties and environment variables. The application reads the JVM properties from
3434
`application-local.yaml`; non-Spring code can read equivalent env vars such as
3535
`BIGDATA_TEST_ENDPOINT_HDFS_HOST`.
36+
37+
Spark SQL preparation runs inside the Gradle JVM. In this in-process mode, Spark logs are routed
38+
through Gradle's logging bridge, so Log4j2 console pattern and color settings are not honored for
39+
Spark extension logs. Use Gradle logging flags such as `--info` and the extension/Spark log-level
40+
settings for verbosity. Full pattern/color control would require running Spark preparation in an
41+
isolated JVM instead of inside the Gradle daemon.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
1+
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED

example/spring-gradle-plugin/src/main/resources/spring-bigdata-extensions.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,21 @@ fileName = "s3.jceks"
88

99
[[s3Buckets]]
1010
bucket = "spring-example"
11+
12+
[[extensions]]
13+
type = "spark-sql-prep"
14+
id = "prepare-iceberg-s3"
15+
appName = "spring-gradle-plugin-iceberg-prep"
16+
enableHiveSupport = false
17+
statements = [
18+
"CREATE NAMESPACE IF NOT EXISTS spring_s3.demo",
19+
"CREATE TABLE IF NOT EXISTS spring_s3.demo.events (id BIGINT, name STRING) USING iceberg",
20+
"INSERT INTO spring_s3.demo.events VALUES (1, 'spring-gradle-plugin')",
21+
]
22+
23+
[extensions.configs]
24+
"spark.sql.extensions" = "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions"
25+
"spark.sql.catalog.spring_s3" = "org.apache.iceberg.spark.SparkCatalog"
26+
"spark.sql.catalog.spring_s3.type" = "hadoop"
27+
"spark.sql.catalog.spring_s3.warehouse" = "s3a://spring-example/iceberg"
28+
"spark.sql.shuffle.partitions" = "1"

extensions/build.gradle.kts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ description = "Config-driven bigdata-test extensions for Hadoop credential provi
1010
val shadedRuntime by configurations.creating {
1111
isCanBeConsumed = false
1212
isCanBeResolved = true
13+
resolutionStrategy.eachDependency {
14+
if (requested.group.startsWith("com.fasterxml.jackson")) {
15+
useVersion("2.15.2")
16+
because("Spark 3.5.x expects Jackson 2.15.x at runtime.")
17+
}
18+
}
1319
resolutionStrategy.capabilitiesResolution.withCapability("org.lz4:lz4-java") {
1420
select("at.yawk.lz4:lz4-java:${libs.versions.lz4.get()}")
1521
because("Kafka/Confluent and Spark publish different providers for the same lz4 capability.")
@@ -36,6 +42,7 @@ dependencies {
3642
compileOnly(libs.avro)
3743
compileOnly(libs.sparkSql)
3844
compileOnly(libs.sparkHive)
45+
compileOnly(libs.servletApi)
3946
implementation(libs.kotlinxSerialization)
4047

4148
shadedRuntime(libs.hadoopClientApi)
@@ -44,12 +51,21 @@ dependencies {
4451
exclude(group = "software.amazon.awssdk", module = "bundle")
4552
}
4653
shadedRuntime(libs.awsSdkS3)
54+
shadedRuntime(libs.awsSdkS3TransferManager)
4755
shadedRuntime(libs.kafkaAvroSerializer)
4856
shadedRuntime(libs.kafkaSchemaRegistryClient)
4957
shadedRuntime(libs.lz4Java)
5058
shadedRuntime(libs.avro)
51-
shadedRuntime(libs.sparkSql)
52-
shadedRuntime(libs.sparkHive)
59+
shadedRuntime(libs.sparkSql) {
60+
exclude(group = "org.rocksdb", module = "rocksdbjni")
61+
}
62+
shadedRuntime(libs.sparkHive) {
63+
exclude(group = "org.rocksdb", module = "rocksdbjni")
64+
}
65+
shadedRuntime(libs.icebergSpark)
66+
shadedRuntime(libs.icebergSparkExtensions)
67+
shadedRuntime(libs.icebergAwsBundle)
68+
shadedRuntime(libs.servletApi)
5369
shadedRuntime(libs.kotlinxSerialization)
5470

5571
testImplementation(libs.junitJupiterApi)
@@ -70,6 +86,7 @@ tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJ
7086
configurations = listOf(shadedRuntime)
7187
isZip64 = true
7288
mergeServiceFiles()
89+
relocate("com.fasterxml.jackson", "org.openprojectx.bigdata.test.shaded.fasterxml.jackson")
7390
}
7491

7592
configurations.matching { it.name.startsWith("test") }.configureEach {

extensions/src/main/kotlin/org/openprojectx/bigdata/test/extensions/core/BigDataExtensionResourceLoader.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@ import java.nio.file.Path
55

66
class BigDataExtensionResourceLoader(
77
private val classLoader: ClassLoader = Thread.currentThread().contextClassLoader,
8+
private val resourceDirectories: Iterable<Path> = emptyList(),
89
) {
910
fun readText(location: String): String = when {
1011
location.startsWith("classpath:") -> {
1112
val resource = location.removePrefix("classpath:").trimStart('/')
1213
classLoader.getResource(resource)?.readText()
13-
?: error("Classpath resource '$resource' was not found")
14+
?: resourceDirectories
15+
.asSequence()
16+
.map { it.resolve(resource) }
17+
.firstOrNull(Files::isRegularFile)
18+
?.let(Files::readString)
19+
?: error(
20+
"Classpath resource '$resource' was not found in the extension runtime classpath " +
21+
"or configured resource directories: ${resourceDirectories.joinToString()}",
22+
)
1423
}
1524
location.startsWith("file:") -> Files.readString(Path.of(location.removePrefix("file:")))
1625
else -> location

0 commit comments

Comments
 (0)