Skip to content

Commit ed2c33e

Browse files
committed
feat: support Spark 3 as well
1 parent 7f079f3 commit ed2c33e

5 files changed

Lines changed: 160 additions & 58 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ When configured, the operator injects everything required to make the https://op
99
* 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),
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

rust/operator-binary/src/config/jvm.rs

Lines changed: 83 additions & 44 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,7 +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.spec.open_lineage.is_some() {
48+
//
49+
// This is scoped to Spark 4.x: the flag is unnecessary — and on the JDK 17 Spark 3.5.x images the
50+
// operator also ships, potentially a startup error — so it must not be emitted there.
51+
if spark_application.openlineage_enabled() && spark_major_version(product_version)? >= 4 {
4352
jvm_args.push(OPENLINEAGE_ADD_OPENS.to_string());
4453
}
4554

@@ -63,16 +72,24 @@ pub fn construct_extra_java_options(
6372
None => jvm_args.clone(),
6473
};
6574

66-
(driver.join(" "), executor.join(" "))
75+
Ok((driver.join(" "), executor.join(" ")))
6776
}
6877

6978
#[cfg(test)]
7079
mod tests {
80+
use rstest::rstest;
81+
7182
use super::*;
7283

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+
7389
#[test]
7490
fn test_construct_jvm_arguments_defaults() {
75-
let input = r#"
91+
let spark_app = spark_app_from_yaml(
92+
r#"
7693
apiVersion: spark.stackable.tech/v1alpha1
7794
kind: SparkApplication
7895
metadata:
@@ -81,14 +98,11 @@ mod tests {
8198
mode: cluster
8299
mainApplicationFile: test.py
83100
sparkImage:
84-
productVersion: 1.2.3
85-
"#;
86-
87-
let deserializer = serde_yaml::Deserializer::from_str(input);
88-
let spark_app: SparkApplication =
89-
serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap();
101+
productVersion: 4.1.2
102+
"#,
103+
);
90104
let (driver_extra_java_options, executor_extra_java_options) =
91-
construct_extra_java_options(&spark_app, &None, &None);
105+
construct_extra_java_options(&spark_app, &None, &None, "4.1.2").unwrap();
92106

93107
assert_eq!(
94108
driver_extra_java_options,
@@ -102,7 +116,8 @@ mod tests {
102116

103117
#[test]
104118
fn test_construct_jvm_argument_overrides() {
105-
let input = r#"
119+
let spark_app = spark_app_from_yaml(
120+
r#"
106121
apiVersion: spark.stackable.tech/v1alpha1
107122
kind: SparkApplication
108123
metadata:
@@ -111,7 +126,7 @@ mod tests {
111126
mode: cluster
112127
mainApplicationFile: test.py
113128
sparkImage:
114-
productVersion: 1.2.3
129+
productVersion: 4.1.2
115130
driver:
116131
jvmArgumentOverrides:
117132
add:
@@ -122,13 +137,10 @@ mod tests {
122137
- -Dhttps.proxyHost=from-executor
123138
removeRegex:
124139
- -Djava.security.properties=.*
125-
"#;
126-
127-
let deserializer = serde_yaml::Deserializer::from_str(input);
128-
let spark_app: SparkApplication =
129-
serde_yaml::with::singleton_map_recursive::deserialize(deserializer).unwrap();
140+
"#,
141+
);
130142
let (driver_extra_java_options, executor_extra_java_options) =
131-
construct_extra_java_options(&spark_app, &None, &None);
143+
construct_extra_java_options(&spark_app, &None, &None, "4.1.2").unwrap();
132144

133145
assert_eq!(
134146
driver_extra_java_options,
@@ -140,32 +152,59 @@ mod tests {
140152
);
141153
}
142154

143-
#[test]
144-
fn test_construct_jvm_arguments_openlineage_add_opens() {
145-
let input = r#"
146-
apiVersion: spark.stackable.tech/v1alpha1
147-
kind: SparkApplication
148-
metadata:
149-
name: spark-example
150-
spec:
151-
mode: cluster
152-
mainApplicationFile: test.py
153-
sparkImage:
154-
productVersion: 1.2.3
155-
openLineage:
156-
connection:
157-
inline:
158-
host: marquez
159-
port: 5000
160-
"#;
161-
162-
let deserializer = serde_yaml::Deserializer::from_str(input);
163-
let spark_app: SparkApplication =
164-
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+
connection:
167+
inline:
168+
host: marquez
169+
port: 5000
170+
"#;
171+
172+
const OPENLINEAGE_ABSENT: &str = r#"
173+
apiVersion: spark.stackable.tech/v1alpha1
174+
kind: SparkApplication
175+
metadata:
176+
name: spark-example
177+
spec:
178+
mode: cluster
179+
mainApplicationFile: test.py
180+
sparkImage:
181+
productVersion: PLACEHOLDER
182+
"#;
183+
184+
/// `--add-opens` is emitted only on Spark 4.x with OpenLineage enabled — never on the Scala
185+
/// 2.12 / JDK 17 Spark 3.5.x images, and never when OpenLineage is absent.
186+
#[rstest]
187+
#[case::enabled_spark_3(OPENLINEAGE_ENABLED, "3.5.8", false)]
188+
#[case::enabled_spark_4(OPENLINEAGE_ENABLED, "4.1.2", true)]
189+
#[case::absent_spark_4(OPENLINEAGE_ABSENT, "4.1.2", false)]
190+
fn test_openlineage_add_opens_is_version_gated(
191+
#[case] yaml_template: &str,
192+
#[case] product_version: &str,
193+
#[case] expect_add_opens: bool,
194+
) {
195+
let spark_app = spark_app_from_yaml(&yaml_template.replace("PLACEHOLDER", product_version));
165196
let (driver_extra_java_options, executor_extra_java_options) =
166-
construct_extra_java_options(&spark_app, &None, &None);
197+
construct_extra_java_options(&spark_app, &None, &None, product_version).unwrap();
167198

168-
assert!(driver_extra_java_options.contains(OPENLINEAGE_ADD_OPENS));
169-
assert!(executor_extra_java_options.contains(OPENLINEAGE_ADD_OPENS));
199+
assert_eq!(
200+
driver_extra_java_options.contains(OPENLINEAGE_ADD_OPENS),
201+
expect_add_opens,
202+
"driver --add-opens presence mismatch for Spark {product_version}"
203+
);
204+
assert_eq!(
205+
executor_extra_java_options.contains(OPENLINEAGE_ADD_OPENS),
206+
expect_add_opens,
207+
"executor --add-opens presence mismatch for Spark {product_version}"
208+
);
170209
}
171210
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,13 @@ pub const OPENLINEAGE_LISTENER_CLASS: &str = "io.openlineage.spark.agent.OpenLin
113113
/// than on `extraClassPath` (system classloader) — is what lets OpenLineage's connector probes
114114
/// succeed; see the classpath discussion in the OpenLineage usage guide.
115115
///
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).
116+
/// This is a stable, version- and Scala-independent symlink the image maintains (mirroring the
117+
/// `jmx_prometheus_javaagent.jar` pattern): each Spark image bakes the correct
118+
/// `openlineage-spark_<scala>-<version>.jar` for its build (Scala 2.13 for Spark 4.x, Scala 2.12 for
119+
/// Spark 3.5.x) and symlinks it here. Referencing the symlink decouples the operator from the jar's
120+
/// version and Scala suffix, so bumping either only touches `docker-images/spark-k8s`.
118121
pub const OPENLINEAGE_JAR_LOCAL_URI: &str =
119-
"local:///stackable/spark/openlineage/openlineage-spark_2.13-1.51.0.jar";
122+
"local:///stackable/spark/openlineage/openlineage-spark.jar";
120123

121124
/// Java module-system flag OpenLineage requires on Spark 4.x: without it the driver throws a
122125
/// non-fatal `InaccessibleObjectException` and silently degrades extension-interface lineage.

0 commit comments

Comments
 (0)