Repository navigation
[SPARK-59032][INFRA] Run behavioral e2e in a separate job so the parity gate gets a clean Connect server - #58
HyukjinKwon wants to merge 3 commits into
Conversation
viirya
left a comment
There was a problem hiding this comment.
Verified the core mechanism against the tonic 0.14.6 source rather than taking the description's word for it, and it holds up: http2_keep_alive_interval / keep_alive_timeout / keep_alive_while_idle do reach hyper (transport/channel/service/connection.rs:38-51), an expired client PING surfaces as hyper::Error::is_timeout(), and Status::from_hyper_error maps exactly that to UNAVAILABLE (status.rs:437-440), which can_retry accepts (retries.rs:74) — as does the reference DefaultPolicy, which matters here since the reattach loop driving the parity gate is the reference client's, not reattach.rs. Fixing a documented-but-ignored option is clearly right on its own merits. A few things before this merges.
keepalive_timeout_ms is silently coerced rather than validated. int_param returns i64 and accepts any parsable integer, so the two fields behave inconsistently: grpc_keepalive_time_ms=-1 quietly disables keepalive via the guard, but grpc_keepalive_timeout_ms=-1 becomes a zero-duration timeout through .max(0). Every PING then times out immediately, so the first keepalive interval kills a healthy connection with UNAVAILABLE, and under the 15-retry default that's a retry storm rather than a clean error — strictly worse than the hang being fixed. .max(0) is what disguises it, turning a bad input into a plausible-looking value instead of rejecting it. A zero timeout is never a meaningful request; either reject non-positive values in ChannelBuilder (consistent with how session_id and user_agent already validate) or clamp to a sane floor. Worth a unit test for the dangerous combination specifically — a valid interval with a -1 timeout — since keepalive_override only covers the happy path.
The same bug-class survives one line above the fix. The defect here is "a documented channel option is parsed but never applied," and channel.rs:21 declares GRPC_MAX_MESSAGE_LENGTH_DEFAULT = 128 MiB which is referenced nowhere in the crate — no max_decoding_message_size / max_encoding_message_size call exists anywhere. tonic defaults to a 4 MiB receive cap (codec/mod.rs:101), so any collect() returning a >4 MiB Arrow batch fails with a decode error the reference client handles fine. It's a dead constant documenting the intent, i.e. exactly the shape of the bug just fixed. Two builder calls on the same ep would close it; otherwise worth a follow-up ticket so it isn't rediscovered later as a mystery decode error.
The description covers two of the three changes. Removing the diagnostic workflow is accurate for .github/workflows, but 31ac6ee and 509b9e6 add a new permanent diagnostic layer the description never mentions — the faulthandler watchdog in rust_transport_plugin.py and _dump_server_jvm() in run_official_tests.py. Please fold that into the description; as it stands an approval based on the description wouldn't know it's in the diff. The machinery itself is reasonable to keep (a CI-only hang with no local repro genuinely needs post-mortem capture), with three things I'd tighten:
_dump_server_jvmtakesfound[0]blindly, butpgrep -f org.apache.spark.deploy.SparkSubmitcan match several PIDs, so it may jstack the wrong process.found[-1], or dumping every match, is more robust for a diagnostic whose whole value is landing on the right JVM.run_oursis called from aThreadPoolExecutor, so with--jobs> 1 a timeout in file A dumps a JVM shared with B and C while attributing it to A alone. The per-filedump_pathis correctly keyed bytest_file.name; the JVM dump has no such keying, so a comment noting it's only unambiguous at--jobs 1would help._hang_dump_fhis a module-scopeopen(..., "w")that's never closed, so an unwritableRUST_HANG_DUMPraises during plugin import — failing the whole file with rc≠0 and looking like a client regression. Atry/exceptthere applies the same principle you already got right in_dump_server_jvm("must never mask the timeout").
Two different root causes are now in play. Replacing the whole-file skip with a retry is the right call, and identifying the SLOW_FILE_TIMEOUTS=1200 attempt as wrong and reverting it is the honest read. But if keepalive genuinely fixes the root cause, the 60s/20s defaults surface a wedged stream in ~80s, well inside the 360s cap, so the retry should never fire. Meanwhile SPARK-59032 still says "suspected race opening the first reattachable-execute stream" while the new client comment asserts the keepalive explanation. Keepalive explains a wedged transport; it doesn't explain 509b9e6's own observation that the server produced nothing at all while the reference client got an error in <1s. Worth stating explicitly that keepalive makes the symptom recoverable without necessarily being the root cause, so the ticket doesn't get closed on a fix that only masks it. Separately, note that adding a hang to FLAKY_FILES costs a full 360s cap per attempt — a recurrence burns 720s+ of gate time. I agree with the trade (coverage over speed), just flagging that this file will make the gate slow before it makes it red.
Smaller things: the lazy-connect rationale now appears twice in connect(), at client.rs:82-87 and again at client.rs:121-125, saying the same thing in different words — the refactor moved the call without removing the original comment, so one should go. The True → "env" change in the work() tuple is churn: the value is only consumed truthily (if skipped_file:) and the field is named skipped_file, so a magic string implies a variant set that doesn't exist while the new inline comment already does the whole job. format!("Invalid endpoint: {}", e) keeps the positional form while the new code above it uses inline {scheme} captures. And the keepalive comment is 10 lines of incident narrative over 8 lines of code — the why (parsed but never applied; expired PING → UNAVAILABLE → retriable) is worth keeping, but the CI-incident detail is already in the commit message and the JIRA.
|
Thanks @viirya — thorough review, all fair. Addressed in the latest commit (866acf0):
|
|
Update: the actual root cause is now found and fixed, and the PR has pivoted accordingly (title/description updated). It was not the keepalive path and not test-specific. Via CI RPC-tracing + a server jstack: the reattachable-execute driver fires a Fix: bound |
169d023 to
ac58ae1
Compare
Compiler-flagged dead code with no behavior change: - ml.rs: drop the unused `crate::session::SparkSession` import, and the `input_col`/`output_col` fields of `StandardScalerModel` - the fitted model never reads them (it transforms via its operator/plan; the estimator keeps its own copies), so they were redundant storage. - session.rs: drop the unused `use arrow::array::*;` glob in `rows_to_arrow_ipc`. Left in place (deliberately, not accidental unused imports): `Observation`'s `dataframe`/`set_dataframe`/`set_metrics` and `ProfileResult.metadata` are scaffolding for not-yet-wired features (metric collection / profile metadata display); removing them would erase intended structure and cascade through several call sites, so they are better handled when those features are completed. Co-authored-by: Isaac <no-reply@databricks.com>
A deeper `cargo clippy --workspace --all-targets` pass (beyond the default build)
found more genuinely-unused items, all in test code:
- unused `use super::*` in the conf.rs / merge.rs test modules,
- unused imports in tests/{test_conf_tvf,e2e_integration,catalog_golden,
ml_golden,ml_extra_golden,readwriter_golden,window_golden}.rs,
- unused test locals `iter` / `range` (prefixed `_`),
- a redundant `#[allow(clippy::approx_constant)]` in row.rs.
Not touched (intentional, not accidental-unused): the WASM-UDF example functions
(add_one/shout/sum/... - flagged "never used" only because the host build doesn't
call them), the Observation/ProfileResult scaffolding, and a pre-existing
clippy::approx_constant lint in some test literals (a correctness lint, not an
unused import). All test targets compile and the 205 lib unit tests pass.
Co-authored-by: Isaac <no-reply@databricks.com>
…ty gate gets a clean Connect server The arrow-UDF parity gate hung when the behavioral e2e cargo tests ran first on the same shared Connect server: a UDF that crashes a Python worker (test_python_udf_segfault, faulthandler enabled) left the server's Python-worker reuse wedged, so a later UDF query stalled - the server streamed only ExecutionProgress with no ResultComplete and the reattachable client waited forever. Moving the e2e tests into their own job (connect-e2e) with its own server means the parity run starts against a server nothing else has touched, which sidesteps the wedge. No test is skipped. Co-authored-by: Isaac <no-reply@databricks.com>
cdc43be to
f730c18
Compare
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-e2ejob with its own Connectserver, 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.pyonly when the behavioral e2e cargo tests had alreadyrun against the same shared Connect server.
test_python_udf_segfaultdeliberatelycrashes a Python worker (
ctypes.string_at(0), faulthandler enabled); on a server thee2e tests had already stressed, that left the server's Python-worker reuse wedged, so a
later UDF query (
test_raise_stop_iteration'srange(5)) never produced results - theserver streamed only
ExecutionProgressresponses (0 Arrow batches, noResultComplete),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):
StandardScalerModelfields, plus unused imports/vars in tests (a
clippy --all-targetssweep). Nobehavior 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-e2ejob.cargo build --releaseis clean and theapache-spark-connectunit tests pass.
This pull request and its description were written by Isaac.