This is an automated email from the ASF dual-hosted git repository.

HyukjinKwon pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark-connect-rust.git


The following commit(s) were added to refs/heads/master by this push:
     new 66c2af8  [SPARK-59032][INFRA] Run behavioral e2e in a separate job so 
the parity gate gets a clean Connect server
66c2af8 is described below

commit 66c2af8245ca3401a1f95209ff5ac260135d3261
Author: Hyukjin Kwon <[email protected]>
AuthorDate: Fri Aug 28 14:36:09 2026 +0900

    [SPARK-59032][INFRA] Run behavioral e2e in a separate job so the parity 
gate gets a clean Connect server
    
    ### What changes were proposed in this pull request?
    
    **Bandaid for the arrow-UDF parity hang (SPARK-59032).** Move the Rust 
behavioral
    e2e tests out of the parity job into their own `connect-e2e` job with its 
own Connect
    server, so the parity gate starts against a server nothing else has touched.
    
    **Root cause (why this helps):** the parity gate hung on
    `test_parity_arrow_python_udf.py` only when the behavioral e2e cargo tests 
had already
    run against the *same* shared Connect server. `test_python_udf_segfault` 
deliberately
    crashes a Python worker (`ctypes.string_at(0)`, faulthandler enabled); on a 
server the
    e2e tests had already stressed, that left the server's Python-worker reuse 
wedged, so a
    later UDF query (`test_raise_stop_iteration`'s `range(5)`) never produced 
results - the
    server streamed only `ExecutionProgress` responses (0 Arrow batches, no 
`ResultComplete`),
    and the reattachable client dutifully re-attached forever. Diagnosed from a 
per-test
    client thread dump + server jstack + a response-type histogram of the 
stalled streams.
    
    Giving the parity run a clean server sidesteps the wedge. **No test is 
skipped.** This is
    an infra-level mitigation; a proper client/server worker-recovery fix is a 
follow-up.
    
    Also piggybacked here (unrelated cleanup, from #63):
    
    - Remove compiler-flagged unused Rust imports and redundant 
`StandardScalerModel`
      fields, plus unused imports/vars in tests (a `clippy --all-targets` 
sweep). No
      behavior change.
    
    ### Why are the changes needed?
    
    The parity gate timed out on every run because of the shared-server 
interaction above.
    This makes it green without skipping any test.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    CI: the parity gate runs against a fresh server; the behavioral e2e tests 
run in their
    own `connect-e2e` job. `cargo build --release` is clean and the 
`apache-spark-connect`
    unit tests pass.
    
    This pull request and its description were written by Isaac.
    
    Closes #58 from HyukjinKwon/fix-arrow-parity-hang.
    
    Authored-by: Hyukjin Kwon <[email protected]>
    Signed-off-by: Hyukjin Kwon <[email protected]>
---
 .github/workflows/build_python_connect.yml      | 118 +++++++++++++++++++++---
 crates/spark-connect/src/conf.rs                |   1 -
 crates/spark-connect/src/dataframe.rs           |   2 +-
 crates/spark-connect/src/merge.rs               |   1 -
 crates/spark-connect/src/ml.rs                  |   7 +-
 crates/spark-connect/src/row.rs                 |   2 +-
 crates/spark-connect/src/session.rs             |   1 -
 crates/spark-connect/tests/catalog_golden.rs    |  10 +-
 crates/spark-connect/tests/e2e_integration.rs   |   1 -
 crates/spark-connect/tests/ml_extra_golden.rs   |   2 +-
 crates/spark-connect/tests/ml_golden.rs         |   5 +-
 crates/spark-connect/tests/plans_golden.rs      |   2 +-
 crates/spark-connect/tests/readwriter_golden.rs |   7 +-
 crates/spark-connect/tests/test_conf_tvf.rs     |   2 -
 crates/spark-connect/tests/window_golden.rs     |   6 +-
 15 files changed, 120 insertions(+), 47 deletions(-)

diff --git a/.github/workflows/build_python_connect.yml 
b/.github/workflows/build_python_connect.yml
index 2896229..7117ba7 100644
--- a/.github/workflows/build_python_connect.yml
+++ b/.github/workflows/build_python_connect.yml
@@ -173,19 +173,10 @@ jobs:
             sleep 1
           done
 
-      - name: Rust behavioral e2e tests (against the live server)
-        # These assert results against a live server and otherwise self-gate on
-        # SPARK_REMOTE, so they no-op in the server-less rust.yml job. Run 
them here,
-        # where a Spark 4.2.0 Connect server is already up, so the drop-in 
behavior
-        # 
(functions/columns/dataframe/tvf/catalog/session/streaming/merge/ml/artifact)
-        # is actually verified in CI. Serial: they share the one server 
session.
-        env:
-          SPARK_REMOTE: sc://localhost:15002
-        run: |
-          cargo test -p apache-spark-connect \
-            --test e2e_functions --test e2e_columns --test e2e_dataframe \
-            --test e2e_coverage --test e2e_integration \
-            -- --test-threads=1
+      # NOTE (SPARK-59032): the behavioral e2e tests used to run here, sharing 
this
+      # server with the parity gate. They now run in their own job 
(connect-e2e,
+      # below) so the parity run starts against a server nothing else has 
touched.
+      # No test is skipped.
 
       - name: Parity - official connect suite through OUR Rust client
         id: parity
@@ -224,6 +215,107 @@ jobs:
             /tmp/connect-server.log
           if-no-files-found: warn
 
+  # ---------------------------------------------------------------------------
+  # Behavioral e2e tests (Rust drop-in against a live server), in their OWN job
+  # with a separate Connect server so they never share server state with the
+  # parity gate. The parity gate then starts against a server nothing else has
+  # touched, which avoids the SPARK-59032 arrow-UDF hang (a UDF that crashes a
+  # Python worker on the shared server wedged worker reuse for later tests).
+  # ---------------------------------------------------------------------------
+  connect-e2e:
+    name: Rust behavioral e2e
+    runs-on: ubuntu-latest
+    timeout-minutes: 45
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up Python 3.11
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.11"
+          cache: pip
+
+      - name: Set up JDK 17
+        uses: actions/setup-java@v4
+        with:
+          distribution: temurin
+          java-version: "17"
+
+      - name: Install Rust toolchain
+        run: |
+          rustup toolchain install stable --profile minimal
+          rustup default stable
+
+      - name: Cache cargo & target
+        uses: actions/cache@v4
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-e2e-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: ${{ runner.os }}-cargo-e2e-
+
+      - name: Install system dependencies
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y protobuf-compiler
+
+      - name: Install Python dependencies (server-side UDF workers)
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install pandas pyarrow numpy
+
+      - name: Cache Spark ${{ env.SPARK_VERSION }} distribution
+        uses: actions/cache@v4
+        with:
+          path: ~/spark-dist
+          key: spark-dist-${{ env.SPARK_VERSION }}-hadoop3
+
+      - name: Download Apache Spark ${{ env.SPARK_VERSION }} (Connect server)
+        id: spark-download
+        run: |
+          PKG="spark-${SPARK_VERSION}-bin-hadoop3"
+          if [ ! -d "${HOME}/spark-dist/${PKG}" ]; then
+            
URL="https://archive.apache.org/dist/spark/spark-${SPARK_VERSION}/${PKG}.tgz";
+            mkdir -p "${HOME}/spark-dist"
+            echo "Downloading ${URL}"
+            wget -q -O "${HOME}/spark-dist/${PKG}.tgz" "${URL}"
+            tar -xzf "${HOME}/spark-dist/${PKG}.tgz" -C "${HOME}/spark-dist"
+            rm -f "${HOME}/spark-dist/${PKG}.tgz"
+          else
+            echo "Using cached Spark distribution."
+          fi
+          echo "SPARK_HOME=${HOME}/spark-dist/${PKG}" >> "$GITHUB_OUTPUT"
+
+      - name: Start Spark Connect server
+        env:
+          PYSPARK_PYTHON: python
+          PYSPARK_DRIVER_PYTHON: python
+        run: |
+          SPARK_HOME="${{ steps.spark-download.outputs.SPARK_HOME }}"
+          "${SPARK_HOME}/sbin/start-connect-server.sh" \
+            --packages "org.apache.spark:spark-connect_2.13:${SPARK_VERSION}" \
+            --driver-memory 3g \
+            --conf spark.sql.session.localRelationSizeLimit=1073741824 \
+            > /tmp/connect-server.log 2>&1 &
+          for i in $(seq 1 90); do
+            if (echo > /dev/tcp/localhost/15002) 2>/dev/null; then
+              echo "Connect server is up (after ${i}s)."; break
+            fi
+            if [ "$i" -eq 90 ]; then echo "Server failed to start:"; cat 
/tmp/connect-server.log; exit 1; fi
+            sleep 1
+          done
+
+      - name: Rust behavioral e2e tests (against the live server)
+        env:
+          SPARK_REMOTE: sc://localhost:15002
+        run: |
+          cargo test -p apache-spark-connect \
+            --test e2e_functions --test e2e_columns --test e2e_dataframe \
+            --test e2e_coverage --test e2e_integration \
+            -- --test-threads=1
+
   # ---------------------------------------------------------------------------
   # E2E performance benchmark: our client vs reference, same 4.2.0 server.
   # ---------------------------------------------------------------------------
diff --git a/crates/spark-connect/src/conf.rs b/crates/spark-connect/src/conf.rs
index d6454d5..c72401f 100644
--- a/crates/spark-connect/src/conf.rs
+++ b/crates/spark-connect/src/conf.rs
@@ -124,7 +124,6 @@ impl Clone for RuntimeConf {
 
 #[cfg(test)]
 mod tests {
-    use super::*;
     use crate::session::SparkSession;
 
     fn session() -> SparkSession {
diff --git a/crates/spark-connect/src/dataframe.rs 
b/crates/spark-connect/src/dataframe.rs
index 718d045..3f43030 100644
--- a/crates/spark-connect/src/dataframe.rs
+++ b/crates/spark-connect/src/dataframe.rs
@@ -2431,7 +2431,7 @@ mod cache_tests {
         // The LocalRowIterator and collect() use the same underlying 
ExecutePlan.
         // The difference is purely in client-side consumption: streaming vs 
buffering.
         // We verify that to_local_iterator() creates the same iterator type.
-        let iter: LocalRowIterator;
+        let _iter: LocalRowIterator;
         // This test just verifies the type exists and is constructible.
         // A true integration test would create an actual stream.
     }
diff --git a/crates/spark-connect/src/merge.rs 
b/crates/spark-connect/src/merge.rs
index 3f03314..7cbe120 100644
--- a/crates/spark-connect/src/merge.rs
+++ b/crates/spark-connect/src/merge.rs
@@ -220,7 +220,6 @@ impl WhenNotMatchedBySource {
 
 #[cfg(test)]
 mod tests {
-    use super::*;
     use crate::session::SparkSession;
 
     fn session() -> SparkSession {
diff --git a/crates/spark-connect/src/ml.rs b/crates/spark-connect/src/ml.rs
index 892d94b..a0dd840 100644
--- a/crates/spark-connect/src/ml.rs
+++ b/crates/spark-connect/src/ml.rs
@@ -12,7 +12,6 @@ use uuid::Uuid;
 
 use crate::dataframe::DataFrame;
 use crate::plan::LogicalPlan;
-use crate::session::SparkSession;
 
 /// ML parameter handling: stores name -> literal value mappings.
 ///
@@ -418,8 +417,6 @@ impl Estimator for StandardScaler {
                 OperatorType::Model,
             ),
             params: self.params.clone(),
-            input_col: self.input_col.clone(),
-            output_col: self.output_col.clone(),
         };
         Ok(Box::new(model))
     }
@@ -430,8 +427,6 @@ impl Estimator for StandardScaler {
 pub struct StandardScalerModel {
     operator: MlOperator,
     params: Params,
-    input_col: String,
-    output_col: String,
 }
 
 impl Transformer for StandardScalerModel {
@@ -1408,6 +1403,8 @@ impl Model for CrossValidatorModel {
 #[cfg(test)]
 mod tests {
     use super::*;
+    // Used only by the tests below (the non-test build does not reference it).
+    use crate::session::SparkSession;
 
     #[test]
     fn test_params_creation() {
diff --git a/crates/spark-connect/src/row.rs b/crates/spark-connect/src/row.rs
index 362d6e1..7a16d21 100644
--- a/crates/spark-connect/src/row.rs
+++ b/crates/spark-connect/src/row.rs
@@ -291,7 +291,7 @@ mod tests {
 
         #[allow(clippy::approx_constant)]
         let d = Value::Double(3.14);
-        #[allow(clippy::approx_constant)]
+
         assert_eq!(d.as_f64(), Some(3.14));
 
         let s = Value::String("test".to_string());
diff --git a/crates/spark-connect/src/session.rs 
b/crates/spark-connect/src/session.rs
index b8c6a79..b40ff88 100644
--- a/crates/spark-connect/src/session.rs
+++ b/crates/spark-connect/src/session.rs
@@ -552,7 +552,6 @@ impl Default for SparkSessionBuilder {
 /// Convert Arrow RecordBatch data to IPC bytes.
 /// Used by createDataFrame to package local data.
 fn rows_to_arrow_ipc(rows: &[Row], schema: &DataType) -> Result<Vec<u8>> {
-    use arrow::array::*;
     use arrow::datatypes::Schema as ArrowSchema;
     use arrow::ipc::writer::StreamWriter;
     use arrow::record_batch::RecordBatch;
diff --git a/crates/spark-connect/tests/catalog_golden.rs 
b/crates/spark-connect/tests/catalog_golden.rs
index 41b2bb7..460767a 100644
--- a/crates/spark-connect/tests/catalog_golden.rs
+++ b/crates/spark-connect/tests/catalog_golden.rs
@@ -13,7 +13,6 @@ use std::io::{BufRead, BufReader};
 use base64::{engine::general_purpose::STANDARD, Engine};
 use prost::Message;
 
-use spark_connect::catalog::Catalog;
 use spark_connect_proto as proto;
 
 /// Recursively clear fields that vary run-to-run and are not client-authored:
@@ -36,13 +35,10 @@ fn normalize_expression(e: &mut proto::Expression) {
     if let Some(expr_type) = &mut e.expr_type {
         // Recursively normalize nested expressions as needed
         use proto::expression::ExprType;
-        match expr_type {
-            ExprType::Alias(alias) => {
-                if let Some(expr) = &mut alias.expr {
-                    normalize_expression(expr);
-                }
+        if let ExprType::Alias(alias) = expr_type {
+            if let Some(expr) = &mut alias.expr {
+                normalize_expression(expr);
             }
-            _ => {}
         }
     }
 }
diff --git a/crates/spark-connect/tests/e2e_integration.rs 
b/crates/spark-connect/tests/e2e_integration.rs
index 43910e8..f72bfa8 100644
--- a/crates/spark-connect/tests/e2e_integration.rs
+++ b/crates/spark-connect/tests/e2e_integration.rs
@@ -7,7 +7,6 @@
 //! Or simply skip with: `cargo test --test e2e_integration` (will skip all 
tests)
 
 use spark_connect::column::{col, lit};
-use spark_connect::functions;
 use spark_connect::row::Value;
 use spark_connect::session::SparkSession;
 use spark_connect::types::DataType;
diff --git a/crates/spark-connect/tests/ml_extra_golden.rs 
b/crates/spark-connect/tests/ml_extra_golden.rs
index 7bb7ff2..2456a7a 100644
--- a/crates/spark-connect/tests/ml_extra_golden.rs
+++ b/crates/spark-connect/tests/ml_extra_golden.rs
@@ -90,7 +90,7 @@ fn test_vector_assembler_operator_name() {
 
 #[test]
 fn test_vector_assembler_proto() {
-    let mut assembler = VectorAssembler::new()
+    let assembler = VectorAssembler::new()
         .set_input_cols(vec!["col1", "col2"])
         .set_output_col("vector");
 
diff --git a/crates/spark-connect/tests/ml_golden.rs 
b/crates/spark-connect/tests/ml_golden.rs
index 037bcd2..10f942a 100644
--- a/crates/spark-connect/tests/ml_golden.rs
+++ b/crates/spark-connect/tests/ml_golden.rs
@@ -24,10 +24,7 @@ fn normalize_expression(e: &mut proto::Expression) {
     use proto::expression::ExprType as T;
     e.common = None;
     if let Some(t) = e.expr_type.as_mut() {
-        match t {
-            T::Literal(_) => {}
-            _ => {}
-        }
+        if let T::Literal(_) = t {}
     }
 }
 
diff --git a/crates/spark-connect/tests/plans_golden.rs 
b/crates/spark-connect/tests/plans_golden.rs
index 66d16a7..14d4cb1 100644
--- a/crates/spark-connect/tests/plans_golden.rs
+++ b/crates/spark-connect/tests/plans_golden.rs
@@ -25,7 +25,7 @@ fn normalize_relation(r: &mut proto::Relation) {
     if let Some(rel_type) = &mut r.rel_type {
         use proto::relation::RelType;
         match rel_type {
-            RelType::Range(range) => {}
+            RelType::Range(_range) => {}
             RelType::Sql(_) => {}
             RelType::Project(proj) => {
                 if let Some(input) = &mut proj.input {
diff --git a/crates/spark-connect/tests/readwriter_golden.rs 
b/crates/spark-connect/tests/readwriter_golden.rs
index 95da505..de182b7 100644
--- a/crates/spark-connect/tests/readwriter_golden.rs
+++ b/crates/spark-connect/tests/readwriter_golden.rs
@@ -14,11 +14,8 @@ fn normalize_relation(r: &mut proto::Relation) {
     r.common = None;
     if let Some(_rel_type) = &mut r.rel_type {
         use proto::relation::RelType;
-        match _rel_type {
-            RelType::Read(_read) => {
-                // No recursive normalization needed for Read
-            }
-            _ => {}
+        if let RelType::Read(_read) = _rel_type {
+            // No recursive normalization needed for Read
         }
     }
 }
diff --git a/crates/spark-connect/tests/test_conf_tvf.rs 
b/crates/spark-connect/tests/test_conf_tvf.rs
index 3bf5454..da87f69 100644
--- a/crates/spark-connect/tests/test_conf_tvf.rs
+++ b/crates/spark-connect/tests/test_conf_tvf.rs
@@ -5,8 +5,6 @@
 #[cfg(test)]
 mod tests {
     use spark_connect::plan::LogicalPlan;
-    use spark_connect::table_arg::TableArg;
-    use spark_connect::tvf::TableValuedFunction;
 
     #[test]
     fn test_unresolved_table_valued_function_plan() {
diff --git a/crates/spark-connect/tests/window_golden.rs 
b/crates/spark-connect/tests/window_golden.rs
index 3c55331..5528d5e 100644
--- a/crates/spark-connect/tests/window_golden.rs
+++ b/crates/spark-connect/tests/window_golden.rs
@@ -10,10 +10,10 @@ use std::io::{BufRead, BufReader};
 
 use base64::{engine::general_purpose::STANDARD, Engine};
 use prost::Message;
-use spark_connect::column::{col, lit};
+use spark_connect::column::col;
 use spark_connect::expression::{Expression, LiteralExpression};
 use spark_connect::functions;
-use spark_connect::window::{FrameBound, FrameType, Window, WindowSpec};
+use spark_connect::window::{FrameBound, WindowSpec};
 use spark_connect_proto as proto;
 
 /// An integer literal Column (pyspark infers 32-bit `integer` for small ints).
@@ -261,7 +261,7 @@ fn test_window_golden() {
     let mut matched = 0;
 
     for (name, expected) in &goldens {
-        let built = build(name).expect(&format!("missing build case for {}", 
name));
+        let built = build(name).unwrap_or_else(|| panic!("missing build case 
for {}", name));
         let mut actual = built.to_proto();
         normalize(&mut actual);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to