Skip to content

Commit 6c5a3cb

Browse files
committed
feat: support Spark 3.5.8
1 parent 87da728 commit 6c5a3cb

6 files changed

Lines changed: 216 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file.
88

99
- BREAKING: Add required CLI argument and env var to set the image repository used to construct final product image names: `IMAGE_REPOSITORY` (`--image-repository`), eg. `oci.example.org/my/namespace` ([#684]).
1010
- Support for Spark `4.1.2` ([#708])
11-
- Add `spec.openLineage` to enable OpenLineage lineage emission: injects the OpenLineage Spark listener, HTTP transport (endpoint resolved from a discovery ConfigMap), a stable job name, and the required `--add-opens` JVM flag ([#XXXX]).
11+
- Add `spec.openLineage` to enable OpenLineage lineage emission: injects the OpenLineage Spark listener, HTTP transport (endpoint resolved from a discovery ConfigMap) and a stable job name. The Scala build of the listener jar and the `--add-opens java.base/java.security` JVM flag are selected by the Spark version: the `--add-opens` flag is emitted only on Spark 4.x (it is unnecessary on the Spark 3.5.x images and must not be set there) ([#XXXX]).
1212

1313
### Fixed
1414

docs/modules/spark-k8s/pages/usage-guide/openlineage.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ The Spark operator can automatically emit lineage events for a `SparkApplication
66
When enabled, the operator injects everything required to make the https://openlineage.io/docs/integrations/spark/[OpenLineage Spark listener] work:
77

88
* the OpenLineage Spark listener (appended to `spark.extraListeners`, so any listener you configure yourself is preserved),
9-
* the OpenLineage jar baked into the Spark image, referenced via `spark.jars` so it shares the same classloader as connectors pulled in through xref:usage-guide/job-dependencies.adoc[`deps.packages`] (for example Apache Iceberg),
9+
* the OpenLineage jar baked into the Spark image, referenced via `spark.jars` so it shares the same classloader as connectors pulled in through xref:usage-guide/job-dependencies.adoc[`deps.packages`] (for example Apache Iceberg). The operator selects the jar built for the image's Scala version automatically (Scala 2.13 for Spark 4.x, Scala 2.12 for Spark 3.5.x),
1010
* an HTTP transport pointing at the backend endpoint,
1111
* a stable lineage namespace and job name (see <<job-name>>), and
12-
* the `--add-opens java.base/java.security=ALL-UNNAMED` JVM flag the listener requires on the driver and executors.
12+
* on Spark 4.x only, the `--add-opens java.base/java.security=ALL-UNNAMED` JVM flag the listener requires on the driver and executors. This flag is not added on the Spark 3.5.x images, where it is unnecessary (and, on their JVM, potentially harmful).
1313
1414
== Enabling OpenLineage
1515

Lines changed: 95 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use stackable_operator::crd::s3;
22

33
use crate::crd::{
4+
Error,
45
constants::{
56
JVM_SECURITY_PROPERTIES_FILE, OPENLINEAGE_ADD_OPENS, STACKABLE_TLS_STORE_PASSWORD,
67
STACKABLE_TRUST_STORE, VOLUME_MOUNT_PATH_LOG_CONFIG,
78
},
89
logdir::ResolvedLogDir,
10+
spark_major_version,
911
tlscerts::tls_secret_names,
1012
v1alpha1::SparkApplication,
1113
};
@@ -16,11 +18,15 @@ use crate::crd::{
1618
///
1719
/// Returns `(driver, executor)`: the operator-generated base arguments with the role's
1820
/// `jvmArgumentOverrides` applied on top.
21+
///
22+
/// `product_version` is the resolved Spark product version (e.g. `4.1.2`); it gates the
23+
/// version-specific OpenLineage `--add-opens` flag.
1924
pub fn construct_extra_java_options(
2025
spark_application: &SparkApplication,
2126
s3_conn: &Option<s3::v1alpha1::ConnectionSpec>,
2227
log_dir: &Option<ResolvedLogDir>,
23-
) -> (String, String) {
28+
product_version: &str,
29+
) -> Result<(String, String), Error> {
2430
// Note (@sbernauer): As of 2025-03-04, we did not set any heap related JVM arguments, so I
2531
// kept the implementation as is. We can always re-visit this as needed.
2632

@@ -39,12 +45,10 @@ pub fn construct_extra_java_options(
3945
// OpenLineage on Spark 4.x needs `java.base/java.security` opened to the unnamed module,
4046
// otherwise the driver throws a non-fatal `InaccessibleObjectException` and silently degrades
4147
// extension-interface lineage (MVP §7). Added to both driver and executor.
42-
if spark_application
43-
.spec
44-
.open_lineage
45-
.as_ref()
46-
.is_some_and(|open_lineage| open_lineage.enabled)
47-
{
48+
//
49+
// This is scoped to Spark 4.x: the flag is a no-op — and on some JVMs a startup error — on the
50+
// JDK 11/17 Spark 3.x images the operator also ships, so it must not be emitted there.
51+
if spark_application.openlineage_enabled() && spark_major_version(product_version)? >= 4 {
4852
jvm_args.push(OPENLINEAGE_ADD_OPENS.to_string());
4953
}
5054

@@ -68,16 +72,24 @@ pub fn construct_extra_java_options(
6872
None => jvm_args.clone(),
6973
};
7074

71-
(driver.join(" "), executor.join(" "))
75+
Ok((driver.join(" "), executor.join(" ")))
7276
}
7377

7478
#[cfg(test)]
7579
mod tests {
80+
use rstest::rstest;
81+
7682
use super::*;
7783

84+
fn spark_app_from_yaml(input: &str) -> SparkApplication {
85+
let deserializer = serde_yaml::Deserializer::from_str(input);
86+
serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap()
87+
}
88+
7889
#[test]
7990
fn test_construct_jvm_arguments_defaults() {
80-
let input = r#"
91+
let spark_app = spark_app_from_yaml(
92+
r#"
8193
apiVersion: spark.stackable.tech/v1alpha1
8294
kind: SparkApplication
8395
metadata:
@@ -86,14 +98,11 @@ mod tests {
8698
mode: cluster
8799
mainApplicationFile: test.py
88100
sparkImage:
89-
productVersion: 1.2.3
90-
"#;
91-
92-
let deserializer = serde_yaml::Deserializer::from_str(input);
93-
let spark_app: SparkApplication =
94-
serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap();
101+
productVersion: 4.1.2
102+
"#,
103+
);
95104
let (driver_extra_java_options, executor_extra_java_options) =
96-
construct_extra_java_options(&spark_app, &None, &None);
105+
construct_extra_java_options(&spark_app, &None, &None, "4.1.2").unwrap();
97106

98107
assert_eq!(
99108
driver_extra_java_options,
@@ -107,7 +116,8 @@ mod tests {
107116

108117
#[test]
109118
fn test_construct_jvm_argument_overrides() {
110-
let input = r#"
119+
let spark_app = spark_app_from_yaml(
120+
r#"
111121
apiVersion: spark.stackable.tech/v1alpha1
112122
kind: SparkApplication
113123
metadata:
@@ -116,7 +126,7 @@ mod tests {
116126
mode: cluster
117127
mainApplicationFile: test.py
118128
sparkImage:
119-
productVersion: 1.2.3
129+
productVersion: 4.1.2
120130
driver:
121131
jvmArgumentOverrides:
122132
add:
@@ -127,13 +137,10 @@ mod tests {
127137
- -Dhttps.proxyHost=from-executor
128138
removeRegex:
129139
- -Djava.security.properties=.*
130-
"#;
131-
132-
let deserializer = serde_yaml::Deserializer::from_str(input);
133-
let spark_app: SparkApplication =
134-
serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap();
140+
"#,
141+
);
135142
let (driver_extra_java_options, executor_extra_java_options) =
136-
construct_extra_java_options(&spark_app, &None, &None);
143+
construct_extra_java_options(&spark_app, &None, &None, "4.1.2").unwrap();
137144

138145
assert_eq!(
139146
driver_extra_java_options,
@@ -145,29 +152,71 @@ mod tests {
145152
);
146153
}
147154

148-
#[test]
149-
fn test_construct_jvm_arguments_openlineage_add_opens() {
150-
let input = r#"
151-
apiVersion: spark.stackable.tech/v1alpha1
152-
kind: SparkApplication
153-
metadata:
154-
name: spark-example
155-
spec:
156-
mode: cluster
157-
mainApplicationFile: test.py
158-
sparkImage:
159-
productVersion: 1.2.3
160-
openLineage:
161-
enabled: true
162-
"#;
163-
164-
let deserializer = serde_yaml::Deserializer::from_str(input);
165-
let spark_app: SparkApplication =
166-
serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap();
155+
const OPENLINEAGE_ENABLED: &str = r#"
156+
apiVersion: spark.stackable.tech/v1alpha1
157+
kind: SparkApplication
158+
metadata:
159+
name: spark-example
160+
spec:
161+
mode: cluster
162+
mainApplicationFile: test.py
163+
sparkImage:
164+
productVersion: PLACEHOLDER
165+
openLineage:
166+
enabled: true
167+
"#;
168+
169+
const OPENLINEAGE_DISABLED: &str = r#"
170+
apiVersion: spark.stackable.tech/v1alpha1
171+
kind: SparkApplication
172+
metadata:
173+
name: spark-example
174+
spec:
175+
mode: cluster
176+
mainApplicationFile: test.py
177+
sparkImage:
178+
productVersion: PLACEHOLDER
179+
openLineage:
180+
enabled: false
181+
"#;
182+
183+
const OPENLINEAGE_ABSENT: &str = r#"
184+
apiVersion: spark.stackable.tech/v1alpha1
185+
kind: SparkApplication
186+
metadata:
187+
name: spark-example
188+
spec:
189+
mode: cluster
190+
mainApplicationFile: test.py
191+
sparkImage:
192+
productVersion: PLACEHOLDER
193+
"#;
194+
195+
/// `--add-opens` is emitted only on Spark 4.x with OpenLineage enabled — never on the Scala
196+
/// 2.12 / JDK 17 Spark 3.5.x images, and never when OpenLineage is disabled or absent.
197+
#[rstest]
198+
#[case::enabled_spark_3(OPENLINEAGE_ENABLED, "3.5.8", false)]
199+
#[case::enabled_spark_4(OPENLINEAGE_ENABLED, "4.1.2", true)]
200+
#[case::disabled_spark_4(OPENLINEAGE_DISABLED, "4.1.2", false)]
201+
#[case::absent_spark_4(OPENLINEAGE_ABSENT, "4.1.2", false)]
202+
fn test_openlineage_add_opens_is_version_gated(
203+
#[case] yaml_template: &str,
204+
#[case] product_version: &str,
205+
#[case] expect_add_opens: bool,
206+
) {
207+
let spark_app = spark_app_from_yaml(&yaml_template.replace("PLACEHOLDER", product_version));
167208
let (driver_extra_java_options, executor_extra_java_options) =
168-
construct_extra_java_options(&spark_app, &None, &None);
209+
construct_extra_java_options(&spark_app, &None, &None, product_version).unwrap();
169210

170-
assert!(driver_extra_java_options.contains(OPENLINEAGE_ADD_OPENS));
171-
assert!(executor_extra_java_options.contains(OPENLINEAGE_ADD_OPENS));
211+
assert_eq!(
212+
driver_extra_java_options.contains(OPENLINEAGE_ADD_OPENS),
213+
expect_add_opens,
214+
"driver --add-opens presence mismatch for Spark {product_version}"
215+
);
216+
assert_eq!(
217+
executor_extra_java_options.contains(OPENLINEAGE_ADD_OPENS),
218+
expect_add_opens,
219+
"executor --add-opens presence mismatch for Spark {product_version}"
220+
);
172221
}
173222
}

rust/operator-binary/src/crd/constants.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,25 @@ pub const DEFAULT_SUBMIT_JOB_RETRY_ON_FAILURE_COUNT: u16 = 0;
108108
/// The OpenLineage Spark listener, appended to `spark.extraListeners` when OpenLineage is enabled.
109109
pub const OPENLINEAGE_LISTENER_CLASS: &str = "io.openlineage.spark.agent.OpenLineageSparkListener";
110110

111+
/// Version of the OpenLineage Spark listener jar baked into the Spark images.
112+
///
113+
/// IMPORTANT: MUST stay in sync with the `openlineage-spark-version` build-argument in
114+
/// `docker-images/spark-k8s/boil-config.toml` (the image is what places the jar on disk).
115+
pub const OPENLINEAGE_SPARK_JAR_VERSION: &str = "1.51.0";
116+
111117
/// `local://` URI of the OpenLineage jar baked into the Spark image, referenced via `spark.jars` so
112118
/// it shares the (child) classloader with `--packages` connectors. Delivering it this way — rather
113119
/// than on `extraClassPath` (system classloader) — is what lets OpenLineage's connector probes
114120
/// succeed; see the classpath discussion in the OpenLineage usage guide.
115121
///
116-
/// IMPORTANT: the version MUST stay in sync with the `openlineage-spark-version` build-argument in
117-
/// `docker-images/spark-k8s/boil-config.toml` (the image is what places the jar at this path).
118-
pub const OPENLINEAGE_JAR_LOCAL_URI: &str =
119-
"local:///stackable/spark/openlineage/openlineage-spark_2.13-1.51.0.jar";
122+
/// The jar's Scala suffix depends on the Spark image: Stackable ships Spark 4.x as Scala 2.13 and
123+
/// Spark 3.5.x as Scala 2.12, so the operator selects the matching jar by Spark major version.
124+
/// `scala_binary_version` is e.g. `"2.13"` or `"2.12"`.
125+
pub fn openlineage_jar_local_uri(scala_binary_version: &str) -> String {
126+
format!(
127+
"local:///stackable/spark/openlineage/openlineage-spark_{scala_binary_version}-{OPENLINEAGE_SPARK_JAR_VERSION}.jar"
128+
)
129+
}
120130

121131
/// The key the OpenLineage discovery ConfigMap must hold, carrying the backend base URL. Mirrors the
122132
/// `ADDRESS` contract of the existing `vectorAggregatorConfigMapName` discovery ConfigMap.

0 commit comments

Comments
 (0)