Skip to content

[LIVY-1041] Add Spark 4 support via a new Maven profile - #542

Open
roczei wants to merge 3 commits into
apache:masterfrom
roczei:LIVY-1041
Open

[LIVY-1041] Add Spark 4 support via a new Maven profile#542
roczei wants to merge 3 commits into
apache:masterfrom
roczei:LIVY-1041

Conversation

@roczei

@roczei roczei commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This PR adds support for building and running Apache Livy against Apache Spark 4.x (pinned to 4.1.2) via two new Maven profiles:

-Pspark4 (Spark 4.1.2 + Hadoop 3.4.1 + Netty 4.2.7 + JDK 17)
-Pscala-2.13 (Scala 2.13.17)

Spark 4 introduced breaking changes that required widespread updates:
JDK 17 minimum, Scala 2.13 only, Netty 4.2.x, rewritten
Scala REPL, json4s 4.0.7 and new Scalatra 2.8.4 / ScalaTest 3.2 deps.

The PR is split into four commits so each self-contained prerequisite
can be reviewed on its own JIRA. Reviewers can look at them individually
in the order below; each is functional on its own on top of master.


Commit 1 — [LIVY-1066] Upgrade scalatest to 3.2.9 and scalatra to 2.8.4

Both upgrades are prerequisites for Spark 4 / Scala 2.13 support and
are split into a dedicated commit to keep test migrations separate
from core Spark 4 source changes for easier review.

scalatest 3.0.8 → 3.2.9

  • Migrate FunSuite/FunSpec/FunSpecLike/FlatSpec test classes to
    their 3.2 successors AnyFunSuite/AnyFunSpec/AnyFunSpecLike/AnyFlatSpec.
  • Move org.scalatest.Matchers to org.scalatest.matchers.should.Matchers.
  • Add org.scalatestplus:mockito-3-4_${scala.binary.version}:3.2.9.0,
    since scalatest 3.2 moved MockitoSugar.mock into a separate
    scalatestplus artifact.

scalatra 2.6.5 → 2.8.4

  • Bump metrics.version 3.1.0 → 4.2.19: scalatra 2.8.x's
    metrics-servlets pulls in Dropwizard metrics 4.x, and keeping
    metrics-core / metrics-healthchecks at 3.1.0 causes a
    NoClassDefFoundError for HealthCheckFilter at runtime on
    MiniCluster startup.

Commit 2 — [LIVY-1067] Bump CI Python to 3.11.11 and fix python-api PEP 440 version

Prepares the CI environment and the python-api package metadata for the
upcoming Spark 4 support commit. Both changes are also useful on the
existing Spark 3 build: the PEP 440 fix unblocks pip3 install livy-python-api on modern pip (>= 24), and the Python bump keeps the
CI image on a version supported by both Spark 3.5 and Spark 4.1.

CI Python bumped to 3.11.11
dev/docker/livy-dev-base/Dockerfile pyenv Python bumped from 3.9.21
to 3.11.11, and .github/workflows/integration-tests.yaml pins
pyenv global 3.11.11 explicitly. Rationale (per each Spark release's
python/setup.py): Spark 4.1 declares python_requires=">=3.10" with
classifiers listing 3.10/3.11/3.12/3.13/3.14, while Spark 3.5 declares
python_requires=">=3.8" with classifiers listing 3.8/3.9/3.10/3.11.
The intersection of the two supported ranges is 3.10 and 3.11; we pick
the higher one (3.11.11) so the same image serves both matrix profiles.
The old 3.9.21 was below the Spark 4.1 lower bound. Both the Dockerfile
and the workflow now document this choice inline so future bumps stay
in sync.

The CI Docker image (ghcr.io/${owner}/livy-ci:latest) had to be
rebuilt and pushed manually for the PR CI to pick up the new Python
before this PR is merged: .github/workflows/build-ci-image.yaml only
fires on push to master, so branch/PR runs would otherwise still pull
the stale image cached from the previous Dockerfile. Commands used
(from macOS Apple Silicon, cross-built for linux/amd64 to match the
GitHub Actions runners):

gh auth refresh --scopes write:packages,read:packages
gh auth token | docker login ghcr.io -u <owner> --password-stdin
docker buildx build --platform linux/amd64 \
  -t ghcr.io/<owner>/livy-ci:latest --push \
  dev/docker/livy-dev-base

After this PR merges to master, the workflow will republish the image
on any future Dockerfile change automatically, so this manual step is
only needed for the bootstrap run.

python-api/setup.py PEP 440 version fix
Version bumped from 1.0.0-SNAPSHOT (Maven-style, not PEP 440
compliant) to 1.0.0.dev0 (the canonical Python "pre-release under
active development" form). Modern pip (>= 24) refuses to parse the old
value with Invalid version: '1.0.0-SNAPSHOT', which surfaced as
WARNING: Error parsing dependencies of livy-python-api on every
pip3 install and blocked the validate Python-API requests
integration test from resolving its dependencies. The Maven POM
version (python-api/pom.xml) stays 1.0.0-SNAPSHOT — Maven and pip
have separate versioning conventions and only the pip-visible metadata
needs to change.


Commit 3 — [LIVY-1041] Add Spark 4 support via a new Maven profile

Build / POM changes

  • pom.xml: new spark4 and scala-2.13 profiles; maven-shade-plugin
    3.5.0 → 3.6.2 (Java 22 bytecode support for Jackson 2.18.2
    multi-release jars).
  • repl/pom.xml: added json4s-jackson-core to shade includes (json4s
    4.0.7 split JsonMethods into a separate artifact; missing it caused
    a NoClassDefFoundError in PythonInterpreter at runtime).
  • New Scala 2.13 module wrapper POMs: core/scala-2.13, repl/scala-2.13,
    scala-api/scala-2.13.

Spark 4 / Scala 2.13 source-level fixes

  • repl/scala-2.13: new SparkInterpreter.scala ported to the Spark 4
    SparkILoop (shell.ILoop, PrintWriter, createInterpreter(settings),
    operations via sparkILoop.intp, ReplCompletion). To make extra JARs
    supplied via spark.jars.packages visible to import ... we now
    feed those JARs to the Scala compiler through the -classpath
    argument at Settings construction time, instead of trying to inject
    them after the fact via IMain.addUrlsToClassPath. In Scala 2.13 the
    latter no longer registers URLs with the compiler's platform.classPath
    in a way import ... can resolve (global.extendCompilerClassPath
    runs, but import org.codehaus.plexus.util._ still fails with
    object plexus is not a member of package org.codehaus). A new
    collectUserJarsClasspath() helper walks the context classloader
    chain to Spark's org.apache.spark.util.MutableURLClassLoader, filters
    out livy-* and the wrong-version scala-reflect (same rules the old
    addUrls path used), and joins the resulting file paths with
    File.pathSeparator. If the chain does not contain a
    MutableURLClassLoader we log a warning and skip the extra -classpath
    entry rather than passing an empty argument. This is the same
    mechanism Spark 4's own spark-shell uses in
    repl/src/main/scala/org/apache/spark/repl/Main.scala.

  • AbstractSparkInterpreter: parseError skips leading caret lines
    (Scala 2.13 REPL format); isEffectivelyEmpty returns success for
    comment-only inputs (Scala 2.13 rejects them as errors).

  • Session.scala: SparkR job-group match accepts "4".

  • InteractiveSession: datanucleusJars case 4; parameterise
    scala-2.12 hard-coded path; Future.onSuccess/onFailure removed
    in Scala 2.13.

  • SparkEntries.java: reflection-based hiveClassesArePresent helper
    for both Spark 3 (SparkSession$) and Spark 4 (classic.SparkSession$).

  • SparkProcessBuilder: verbose(boolean) mutator for test-mode
    --verbose.

  • LivySparkUtils: Spark 4.0/4.1 → Scala 2.13 version map entries.

  • Thriftserver: JavaConversions (removed in 2.13) → JavaConverters;
    json4s 4.x parse API; ListBuffer.toSeq; Scala 2.13 error messages
    in LIVY-571 assertions.

  • Various Scala 2.13 collection widening fixes: toSeq,
    IndexedSeq.empty, asScala.toSeq across SparkYarnApp,
    ZooKeeperManager, InteractiveSessionServlet.

Thriftserver libthrift 0.9 / 0.16 compatibility
The Livy Thrift binary CLI service was written against libthrift 0.9.3
(shipped with Hive 3 and pinned by the Spark 3 profile). Spark 4 pulls
in libthrift 0.16.0 transitively through spark-hive, and 0.16 removed
four TThreadPoolServer.Args builder methods that Livy relied on:
requestTimeout, requestTimeoutUnit, beBackoffSlotLength and
beBackoffSlotLengthUnit. Compiling against 0.9 but running against
0.16 made the mini-cluster Thrift server abort during startup with a
NoSuchMethodError on TThreadPoolServer$Args.requestTimeout(int),
which in turn hung JdbcIT on the SASL handshake. ThriftBinaryCLIService
now invokes those four builders reflectively via a small
applyOptionalArg helper: they run unchanged on the Spark 3 classpath
and are silently skipped on Spark 4, where they no longer exist. The
functional loss on Spark 4 is limited to the login-timeout / backoff
knobs, which have no equivalent in the newer libthrift API.

Test fix: Scala 2.13 REPL output shape changes in InteractiveIT
The Scala 2.13 REPL prints results differently from Scala 2.12 in four
ways that InteractiveIT was too strict about; the Spark 4 matrix
(Scala 2.13 only) trips all four. Fixed in place:

  • Value results: val res0: Int = 2 (2.13) vs. res0: Int = 2 (2.12).
    Six verifyResult regexes (across basic interactive session,
    user jars are properly imported ... and recover interactive session) now accept an optional val prefix.
  • Compile errors: error: not found: value abcde (2.13) vs.
    <console>:12: error: not found: value abcde (2.12). The
    <console>:<line>: location marker is now optional in the
    verifyError regex.
  • Class definitions: class Item (2.13) vs. defined class Item
    (2.12). The case class Item(i: Int) check in user jars are properly imported ... now accepts an optional defined prefix.
  • Deprecation-warning header: Scala 2.13 prints a
    warning: 1 deprecation ... line above the value binding when the
    expression uses a deprecated API. SparkContext.parallelize is
    deprecated on Spark 4, so
    val rdd = sc.parallelize(Array.fill(10){...}) prints the warning
    before the val rdd: ... line. The verifyResult regex now uses
    (?s)(?:warning:.*\n)?(?:val )?rdd.* to accept the warning
    prefix and the DOTALL semantics needed to span the newline.
    Both forms match on -Pscala-2.12 and -Pscala-2.13.

Test fix: Spark 4 SQLContext / DataFrame runtime class in InteractiveIT
Spark 4 moved the runtime SQLContext and DataFrame implementations
into org.apache.spark.sql.classic; only the compile-time aliases
remain at org.apache.spark.sql.SQLContext / org.apache.spark.sql.DataFrame.
The Scala 2.13 REPL surfaces the runtime class name, so Spark 4 prints
sql: org.apache.spark.sql.classic.SQLContext = org.apache.spark.sql.classic.SQLContext@...
and val df: org.apache.spark.sql.classic.DataFrame = ..., where Spark
3 prints the alias form. Two InteractiveIT regexes now allow an
optional classic. package qualifier so the same expectations match
both -Pspark3 and -Pspark4:

  • sql: org.apache.spark.sql.(?:classic\.)?SQLContext = ...
  • (?:val )?df: org.apache.spark.sql.(?:classic\.)?DataFrame
    The old Pattern.quote("df: org.apache.spark.sql.DataFrame") literal
    was too strict for Spark 4 and caused the basic interactive session
    InteractiveIT case to fail.

CI matrix

  • Unit Tests: spark4/JDK-17 entry added.
  • Integration Tests: spark4/JDK-17 entry added.

How was this patch tested?

  • Unit tests: mvn verify -Pspark4 -Pscala-2.13 -Pthriftserver passes
    all 19 modules on JDK 17 (macOS aarch64). Verified modules include
    livy-rsc (40 tests), livy-repl_2.13 (87 tests), livy-server (205 tests),
    livy-client-http (17 tests), livy-scala-api_2.13 (17 tests), and the
    thriftserver integration suite (27 tests: HttpThriftServerSuite +
    BinaryThriftServerSuite).
  • The existing Spark 3 build (mvn verify -Pspark3 -Pscala-2.12) is
    unaffected and its test suite remains green.
  • Integration tests (mvn integration-test -Pspark3 -Pscala-2.12 -pl :livy-integration-test) pass once the metrics.version bump resolves
    the HealthCheckFilter NoClassDefFoundError on MiniCluster startup.
  • GitHub Actions CI runs both spark3 and spark4 matrix entries for unit
    and integration test workflows.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 4.7)

@roczei

roczei commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

The integration test failed because Python 3.11.11 is missing from the test environment:

pyenv: version `3.11.11' not installed
Error: Process completed with exit code 1.

This will be resolved once #540 is merged and the Docker image is updated. Related comment from @gyogal: #540 (comment)

roczei added 3 commits August 19, 2026 09:57
## What changes were proposed in this pull request?

Upgrade scalatest 3.0.8 -> 3.2.9 and scalatra 2.6.5 -> 2.8.4. Both
upgrades are prerequisites for Spark 4 / Scala 2.13 support (parent
JIRA LIVY-1041) and are split into a dedicated commit to keep test
migrations separate from core Spark 4 source changes for easier review.

Scalatest 3.0.8 -> 3.2.9:
- Migrate FunSuite/FunSpec/FunSpecLike/FlatSpec test classes to their
  3.2 successors AnyFunSuite/AnyFunSpec/AnyFunSpecLike/AnyFlatSpec.
- Move `org.scalatest.Matchers` to
  `org.scalatest.matchers.should.Matchers`.
- Add `org.scalatestplus:mockito-3-4_${scala.binary.version}:3.2.9.0`,
  since scalatest 3.2 moved `MockitoSugar.mock` into a separate
  scalatestplus artifact.

Scalatra 2.6.5 -> 2.8.4:
- Bump `metrics.version` 3.1.0 -> 4.2.19: scalatra 2.8.x's
  metrics-servlets pulls in Dropwizard metrics 4.x, and keeping
  metrics-core / metrics-healthchecks at 3.1.0 causes a
  NoClassDefFoundError for HealthCheckFilter at runtime.

## How was this patch tested?

- Unit tests: `mvn verify -Pspark3 -Pscala-2.12 -Pthriftserver` passes
  on JDK 8/17 for all modules with the migrated ScalaTest 3.2 test
  suites (matching what was verified as part of the parent LIVY-1041
  branch).
- The metrics 4.2.19 bump is tested by the integration test suite
  (`mvn integration-test -Pspark3 -Pscala-2.12 -pl
  :livy-integration-test`), which previously failed with
  `NoClassDefFoundError: com/codahale/metrics/servlets/HealthCheckFilter`
  on MiniCluster startup and now passes.

## Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 4.7)
## What changes were proposed in this pull request?

Prepares the CI environment and the python-api package metadata for the
upcoming Spark 4 support commit. Both changes are also useful on the
existing Spark 3 build: the PEP 440 fix unblocks `pip3 install
livy-python-api` on modern pip (>= 24), and the Python bump keeps the
CI image on a version supported by both Spark 3.5 and Spark 4.1.

**CI Python bumped to 3.11.11**
dev/docker/livy-dev-base/Dockerfile pyenv Python bumped from 3.9.21 to
3.11.11, and .github/workflows/integration-tests.yaml pins
`pyenv global 3.11.11` explicitly. Rationale (per each Spark release's
python/setup.py): Spark 4.1 declares `python_requires=">=3.10"` with
classifiers listing 3.10/3.11/3.12/3.13/3.14, while Spark 3.5 declares
`python_requires=">=3.8"` with classifiers listing 3.8/3.9/3.10/3.11.
The intersection of the two supported ranges is 3.10 and 3.11; we pick
the higher one (3.11.11) so the same image serves both matrix profiles.
The old 3.9.21 was below the Spark 4.1 lower bound. Both the Dockerfile
and the workflow now document this choice inline so future bumps stay
in sync.

The CI Docker image (`ghcr.io/${owner}/livy-ci:latest`) had to be
rebuilt and pushed manually for the PR CI to pick up the new Python
before this PR is merged: `.github/workflows/build-ci-image.yaml` only
fires on push to master, so branch/PR runs would otherwise still pull
the stale image cached from the previous Dockerfile. Commands used
(from macOS Apple Silicon, cross-built for linux/amd64 to match the
GitHub Actions runners):

    gh auth refresh --scopes write:packages,read:packages
    gh auth token | docker login ghcr.io -u <owner> --password-stdin
    docker buildx build --platform linux/amd64 \
      -t ghcr.io/<owner>/livy-ci:latest --push \
      dev/docker/livy-dev-base

After this PR merges to master, the workflow will republish the image
on any future Dockerfile change automatically, so this manual step is
only needed for the bootstrap run.

**python-api/setup.py PEP 440 version fix**
Version bumped from `1.0.0-SNAPSHOT` (Maven-style, not PEP 440
compliant) to `1.0.0.dev0` (the canonical Python "pre-release under
active development" form). Modern pip (>= 24) refuses to parse the old
value with `Invalid version: '1.0.0-SNAPSHOT'`, which surfaced as
`WARNING: Error parsing dependencies of livy-python-api` on every
`pip3 install` and blocked the `validate Python-API requests`
integration test from resolving its dependencies. The Maven POM
version (`python-api/pom.xml`) stays `1.0.0-SNAPSHOT` -- Maven and pip
have separate versioning conventions and only the pip-visible metadata
needs to change.

## How was this patch tested?

- `pip3 install ./python-api` no longer emits
  `WARNING: Error parsing dependencies of livy-python-api: Invalid
  version: '1.0.0-SNAPSHOT'`; the package installs cleanly on pip 24+.
- Rebuilt the CI Docker image locally with the pyenv 3.11.11 bump and
  confirmed `python3 --version` reports `Python 3.11.11` inside the
  container.
- GitHub Actions Integration Tests workflow runs `pyenv global 3.11.11`
  successfully on the -Pspark3 -Pscala-2.12 matrix entry (this commit
  does not yet add any Spark 4 entry).

## Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 4.7)
## What changes were proposed in this pull request?

This PR adds support for building and running Apache Livy against
Apache Spark 4.x (pinned to 4.1.2) via two new Maven profiles:
  -Pspark4  (Spark 4.1.2 + Hadoop 3.4.1 + Netty 4.2.7 + JDK 17)
  -Pscala-2.13  (Scala 2.13.17)

Spark 4 introduced breaking changes that required widespread updates:
JDK 17 minimum, Scala 2.13 only, Netty 4.2.x, a rewritten
Scala REPL, and json4s 4.0.7.

**Build / POM changes**
- pom.xml: new `spark4` and `scala-2.13` profiles; maven-shade-plugin
  3.5.0 -> 3.6.2 (bundles an ASM version that understands Java 22
  bytecode, needed to shade Jackson 2.18.2's `META-INF/versions/22`
  multi-release classes).
- repl/pom.xml: added `json4s-jackson-core` to the shade includes
  (json4s 4.0.7 split `JsonMethods` into a separate artifact; missing
  it caused a NoClassDefFoundError in PythonInterpreter at runtime).
  Also pulls `livy-test-lib` in as a test dependency so the repl tests
  can use the new shared `ScalaVersionAware` trait.
- New Scala 2.13 module wrapper POMs: `core/scala-2.13`,
  `repl/scala-2.13`, `scala-api/scala-2.13`.
- README.md: documented Spark 4 Python compatibility -- supported
  Python versions for Spark 4.1 are 3.10 – 3.14 (added to the
  `-Pspark4` Note block)

**Spark 4 / Scala 2.13 source-level fixes**
- `repl/scala-2.13/SparkInterpreter.scala`: new implementation ported
  to the Spark 4 SparkILoop (`shell.ILoop`, `PrintWriter`,
  `createInterpreter(settings)`, operations via `sparkILoop.intp`,
  `ReplCompletion`). To make extra JARs supplied via
  `spark.jars.packages` visible to `import ...` we feed those JARs to
  the Scala compiler through the `-classpath` argument at Settings
  construction time, instead of via a post-init
  `IMain.addUrlsToClassPath` call. In Scala 2.13 the latter does not
  reliably register URLs with the compiler's `platform.classPath`, so
  `import org.codehaus.plexus.util._` fails with `object plexus is not
  a member of package org.codehaus`. A new `collectUserJarsClasspath()`
  helper walks the context classloader chain to Spark's
  `MutableURLClassLoader`, filters out `livy-*` and the wrong-version
  `scala-reflect` (same rules as the old `addUrls` path), and joins the
  resulting file paths with `File.pathSeparator`. If the chain has no
  `MutableURLClassLoader` we log a warning and skip the extra
  `-classpath` entry rather than passing an empty argument.

- `AbstractSparkInterpreter.scala`:
  * `parseError` skips the leading caret line so `ename` lands on the
    "error: ..." message on both Scala 2.12 and 2.13 (2.13 prints the
    caret pointer before the message). Implemented with
    `lines.indexWhere(_.trim != "^") ... lines.patch(idx, Nil, 1)`.
  * New `isEffectivelyEmpty` helper strips block/line comments and
    returns success for comment-only inputs (Scala 2.12 accepted them
    silently; 2.13 rejects them as compile errors).
- `repl/Session.scala`: SparkR `setJobGroup` match now accepts `"4"`
  alongside `"2" | "3"`.
- `server/interactive/InteractiveSession.scala`: `datanucleusJars` and
  `mergeHiveSiteAndHiveDeps` now take an extra `scalaVersion`
  parameter, adds `case 3 | 4` to the major-version match and replaces
  the hard-coded `assembly/target/scala-2.12/jars` with
  `assembly/target/scala-$scalaVersion/jars`.
- `server/batch/BatchSession.scala`: pass `--verbose` to spark-submit
  under `LivyConf.TEST_MODE` so tests can grep the resolved arguments
  out of the child's log.
- `rsc/driver/SparkEntries.java`: new reflection-based
  `hiveClassesArePresent` helper that probes
  `org.apache.spark.sql.classic.SparkSession$` (Spark 4) first and
  falls back to `org.apache.spark.sql.SparkSession$` (Spark 3).
- `utils/SparkProcessBuilder.scala`: new `verbose(v: Boolean)` mutator
  and an internal `_verbose` flag that appends `--verbose` to
  `spark-submit` when set (used by `BatchSession`).
- `utils/LivySparkUtils.scala`: `(4, 0)` and `(4, 1)` -> `"2.13"`
  entries in `sparkScalaVersionMap`; `MAX_VERSION` bumped from
  `(3, 6)` to `(4, 2)`.
- `repl/SparkRInterpreter.scala`: log a warning on Spark 4 that
  SparkR was deprecated in SPARK-49347 and may be removed in a future
  Spark release.
- `scala-api/scalaapi/package.scala`: inline comment documenting that
  `Duration.isFinite` is a parameterless def in Scala 2.13 (took an
  empty parameter list in 2.12); dropping the parens compiles on both.
- Thriftserver Scala/json4s 4.x updates:
  * `session/Get{Columns,Functions,Schemas,Tables}Job.java`:
    `scala.collection.JavaConversions.seqAsJavaList` (removed in Scala
    2.13) replaced with
    `scala.collection.JavaConverters.seqAsJavaListConverter(...).asJava()`.
  * `types/DataTypeUtils.scala`: json4s 4.x `parse(input, useBigDecimal)`
    requires an `AsJsonInput[T]`; switch to the single-arg String
    overload `parse(sparkJson)`.
  * `LivyExecuteStatementOperation.scala`: explicit `res.toSeq` since
    Scala 2.13 no longer widens a mutable `Buffer` to `Seq` implicitly.
  * `ThriftServerSuites.scala`: `LIVY-571` assertion now also accepts
    the Spark 4 error string `[SCHEMA_NOT_FOUND] The schema
    `spark_catalog`.`invalid_database` cannot be found`.
- `InteractiveSessionServlet.scala`: `logs.asJava` -> `logs.toSeq.asJava`
  so Scala 2.13 picks the `Seq[A]` -> `java.util.List` `asJava`
  overload instead of erroring on ambiguity.

**Thriftserver libthrift pre-0.16 / 0.16 compatibility**
The Livy Thrift binary CLI service was written against libthrift 0.9.3
(the version pinned in the root pom.xml). Spark 3 keeps a compatible
pre-0.16 libthrift on the runtime classpath (e.g. 0.12.0 in Spark 3.3),
but Spark 4 pulls in libthrift 0.16.0 transitively through spark-hive,
and 0.16 removed four `TThreadPoolServer.Args` builder methods that
Livy relied on: `requestTimeout`, `requestTimeoutUnit`,
`beBackoffSlotLength` and `beBackoffSlotLengthUnit`. Compiling against
0.9.3 but running against 0.16.0 made the mini-cluster Thrift server
abort during startup with a `NoSuchMethodError` on
`TThreadPoolServer$Args.requestTimeout(int)`, which in turn hung
JdbcIT on the SASL handshake. `ThriftBinaryCLIService` now invokes
those four builders reflectively via a small `applyOptionalArg`
helper: they run unchanged on Spark 3 classpaths and are silently
skipped on Spark 4, where they no longer exist. The functional loss
on Spark 4 is limited to the login-timeout / backoff knobs, which
have no equivalent in the newer libthrift API.

**Test fix: Scala 2.13 REPL output shape changes in InteractiveIT**
The Scala 2.13 REPL prints results differently from Scala 2.12 in
four ways that `InteractiveIT` was too strict about; the Spark 4
matrix (Scala 2.13 only) trips all four. Fixed in place:
- Value results: `val res0: Int = 2` (2.13) vs. `res0: Int = 2`
  (2.12). Six `verifyResult` regexes (across `basic interactive
  session`, `user jars are properly imported ...` and `recover
  interactive session`) now accept an optional `val ` prefix.
- Compile errors: `error: not found: value abcde` (2.13) vs.
  `<console>:12: error: not found: value abcde` (2.12). The
  `<source>:<line>: ` location marker is now optional in the
  `verifyError` regex.
- Class definitions: `class Item` (2.13) vs. `defined class Item`
  (2.12). The `case class Item(i: Int)` check in `user jars are
  properly imported ...` now accepts an optional `defined ` prefix.
- Deprecation-warning header: Scala 2.13 prints a
  `warning: 1 deprecation ...` line above the value binding when the
  expression uses a deprecated API. `SparkContext.parallelize` is
  deprecated on Spark 4, so
  `val rdd = sc.parallelize(Array.fill(10){...})` prints the warning
  before the `val rdd: ...` line. The `verifyResult` regex now uses
  `(?s)(?:warning:.*\n)?(?:val )?rdd.*` to accept the warning prefix
  and the DOTALL semantics needed to span the newline.
Both forms match on `-Pscala-2.12` and `-Pscala-2.13`.

**Test fix: Spark 4 SQLContext / DataFrame runtime class in InteractiveIT**
Spark 4 moved the runtime `SQLContext` and `DataFrame` implementations
into `org.apache.spark.sql.classic`; only the compile-time aliases
remain at `org.apache.spark.sql.SQLContext` / `org.apache.spark.sql.DataFrame`.
The Scala 2.13 REPL surfaces the runtime class name, so Spark 4
prints `sql: org.apache.spark.sql.classic.SQLContext = ...` and
`val df: org.apache.spark.sql.classic.DataFrame = ...`, where Spark 3
prints the alias form. Two `InteractiveIT` regexes now allow an
optional `classic.` package qualifier so the same expectations match
both `-Pspark3` and `-Pspark4`:
- `sql: org.apache.spark.sql.(?:classic\.)?SQLContext = ...`
- `(?:val )?df: org.apache.spark.sql.(?:classic\.)?DataFrame`
The old `Pattern.quote("df: org.apache.spark.sql.DataFrame")` literal
was too strict for Spark 4 and caused the `basic interactive session`
case to fail.

**Cross-version test helpers: `ScalaVersionAware` trait**
New `test-lib/.../ScalaVersionAware` trait exposes the small
Scala-2.12 vs 2.13 REPL-output differences as reusable fragments:
- `optionalValPrefix` (`"val "` on 2.13, `""` on 2.12) for exact-match
  expected strings.
- `optionalValPrefixRegex` / `optionalDefinedPrefixRegex` /
  `optionalWarningPrefixRegex` regex fragments for the corresponding
  `verifyResult` regex assertions.
Detection uses the runtime `scala.util.Properties.versionNumberString`
so the same test class works regardless of which Scala the artifact
was compiled against. Mixed into `BaseInterpreterSpec`,
`BaseSessionSpec` and `InteractiveIT`. Downstream specs
(`ScalaInterpreterSpec`, `SharedSessionSpec`, `SparkSessionSpec`,
`InteractiveIT`) rewrote their expected-value strings against these
fragments; some hard-`equal` assertions were relaxed to
`include`/`fullyMatch regex` where the 2.13 REPL adds a deprecation
banner ahead of the result or renumbers `resN` slots differently.

**Server/unit tests: cross-Scala tolerance in `InteractiveSessionSpec`**
The `should get scala version` case now decomposes the JSON result by
hand and asserts `text/plain` equals either `res0: Int = 3\n` or
`val res0: Int = 3\n`, plus separate `status` / `execution_count`
assertions. Straight equality with a decomposed Map broke on the
Spark 4 driver where the 2.13 REPL adds the `val ` prefix.

**Unit tests: `LivySparkUtilsSuite`**
Added `4.0.0` and `4.1.2` to both the supported-version list and the
`defaultSparkScalaVersion` -> `"2.13"` expectations.

**scala-api tests**
- `ScalaClientTestUtils.scala`: `context.sc.parallelize(buffer, ...)`
  -> `context.sc.parallelize(buffer.toSeq, ...)` (2.13 no longer
  widens a mutable `ArrayBuffer` to `Seq` implicitly for the
  `parallelize` signature).
- `ScalaJobHandleTest.scala`: `Duration.Undefined` -> `Duration.Inf`
  (2.13's `Await.ready` rejects `Undefined` with "Cannot wait for
  Undefined duration of time"); assertion switched from
  `verify(mockJobHandle, times(1)).get()` to
  `verify(mockJobHandle, atLeastOnce()).isDone`, because 2.13's
  `Await.ready` short-circuits when `isCompleted` is already true and
  does not call the underlying `ready(atMost)` -- so `.get()` is not
  observed on 2.13. `isDone` is exercised by both versions.

**Repl tests: `SparkInterpreterSpec` move**
`SparkInterpreterSpec.scala` moved from `repl/scala-2.12/src/test/...`
into the shared `repl/src/test/scala/...` tree (it was byte-identical
between scala-2.12 and scala-2.13). A new `should skip leading caret
lines in Scala 2.13 error format.` test case was added for the caret-
first `parseError` branch, so the shared spec runs three cases under
both `-Pscala-2.12` and `-Pscala-2.13`.

**CI matrix**
- `.github/workflows/unit-tests.yaml` and `integration-tests.yaml`
  converted the flat `maven_profile` x `jdk_path` matrix to an
  explicit `include:` list and added a new spark4/JDK-17 entry
  (`-Pscala-2.13 -Pspark4` on `/usr/lib/jvm/java-17-openjdk-amd64`).

## How was this patch tested?

- Unit tests: `mvn verify -Pspark4 -Pscala-2.13 -Pthriftserver` passes
  all 19 modules on JDK 17 (macOS aarch64). Verified modules include
  livy-rsc (40 tests), livy-repl_2.13 (87 tests), livy-server
  (205 tests), livy-client-http (17 tests), livy-scala-api_2.13
  (17 tests), and the thriftserver integration suite (27 tests:
  HttpThriftServerSuite + BinaryThriftServerSuite). The
  `SparkInterpreterSpec` move added one new caret-first `parseError`
  test case; the shared spec now has 3 cases and runs identically
  under both scala-2.12 and scala-2.13.
- Integration tests (`mvn integration-test -Pspark3 -Pscala-2.12 -pl
  :livy-integration-test`) pass.
- GitHub Actions CI runs both spark3 and spark4 matrix entries for
  unit and integration test workflows.

## Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 4.7)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant