Skip to content

[836] xtable-spark-runtime: thin drop-in Spark bundle for 0.4.x (Spark 3.4 + 3.5)#843

Merged
vinishjail97 merged 13 commits into
apache:branch-0.4from
vinishjail97:836-spark-runtime-hudi0x
Jul 21, 2026
Merged

[836] xtable-spark-runtime: thin drop-in Spark bundle for 0.4.x (Spark 3.4 + 3.5)#843
vinishjail97 merged 13 commits into
apache:branch-0.4from
vinishjail97:836-spark-runtime-hudi0x

Conversation

@vinishjail97

@vinishjail97 vinishjail97 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Ports xtable-spark-runtime onto the Hudi 0.x line (branch-0.4, Hudi 0.14 / Spark 3.4) — a thin, relocated Spark bundle that runs an incremental XTable sync in-job; engines are provided by the cluster, never bundled. Hudi-0.x counterpart of #838; part of the 0.4.0 on-ramp.

  • Module: xtable-spark-runtime (~3.7 MB) with XTableSparkSync spark-submit entry point; resilient ConversionTargetFactory ServiceLoader discovery so a subset of engines works when others are absent.
  • Hudi 0.14 specifics: no hudi-hadoop-common (1.x split); engine classpath uses the hudi-spark bundle so Avro model classes link on Avro 1.12 (Iceberg 1.9.2), plus hudi-java-client.
  • Spark 3.4 and 3.5: Hudi/Iceberg conversion uses only Spark-free core classes and runs on either line. Delta is the only Spark-version-sensitive engine — it uses delta-core on Spark 3.4 and auto-switches to the Spark-free Delta Kernel on Spark 3.5+ (where the bundled delta-core does not run). A single --usedeltakernel flag forces Kernel on any version; otherwise it is auto-enabled by Spark version.
  • Testing: ITXTableSparkRuntimeBundle covers each direction across Hudi/Iceberg/Delta, spark-submit-ing the shaded jar and asserting the target is data-equivalent to the source. It's SPARK_HOME-gated, so the Spark Runtime Bundle Validation workflow runs it on a matrix of Spark 3.4.3 and 3.5.9 (green); the standard CI build skips it.
  • Docs: module README and a "Run XTable on Apache Spark" quickstart page in [docs] xtable-spark-runtime README + Spark quickstart docs #847 (targets main, to merge after 0.4.0 once the module lands there).

Ports the xtable-spark-runtime packaging slice onto the Hudi 0.x line
(main-hudi-0x, Hudi 0.14 / Spark 3.4). New thin, relocated bundle that
runs an incremental XTable sync inside a Spark job; engines are provided
by the cluster, never bundled.

Module:
- xtable-spark-runtime_${scala.binary.version}: xtable-core compile;
  Spark/Hadoop and the engines (Hudi/Iceberg/Delta) provided. Curated
  shade allowlist (xtable modules + guava/protobuf/commons-cli relocated);
  avro/parquet/jackson NOT relocated (exchanged with the engines). Thin
  ~3.7 MB bundle.
- XTableSparkSync: standalone spark-submit entry point (Apache Commons CLI).
- XTableSyncService / TableSyncSpec: build an INCREMENTAL ConversionConfig
  and run ConversionController.sync; target metadata path = source data
  path (required by Hudi; Iceberg data lives under <basePath>/data).

Hudi 0.14 specifics (vs the Hudi 1.x variant on main):
- No hudi-hadoop-common (that split is 1.x); hudi-common provides FSUtils.
- Engine classpath uses the hudi-spark bundle (its regenerated Avro model
  classes link on Avro 1.12, which Iceberg 1.9.2 requires) plus
  hudi-java-client for the Hudi target's Java write client, with the raw
  hudi-common excluded so the bundle's clean DecimalWrapper wins.

ConversionTargetFactory: make ServiceLoader discovery resilient so a
subset of engines works when others are absent (warn + skip on
LinkageError/ServiceConfigurationError; name-based Delta-Kernel check).

ITXTableSparkRuntimeBundle: spark-submits the shaded jar for one case per
direction across Hudi, Iceberg and Delta (source and target), engines on
a flat classpath, asserting data-equivalence over comparable scalar
columns. Requires a Spark 3.4 SPARK_HOME; skipped otherwise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread xtable-spark-runtime/pom.xml Outdated
Comment thread rfc/rfc-3/rfc-3.md
Comment thread rfc/rfc-3/rfc-3.md
Comment thread xtable-spark-runtime/pom.xml
Comment thread xtable-spark-runtime/pom.xml
Comment thread xtable-spark-runtime/pom.xml
Comment thread xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSyncService.java Outdated
vinishjail97 and others added 3 commits July 9, 2026 18:20
…sing, RFC)

Publish the shaded jar as the MAIN artifact with a dependency-reduced POM
(shadedArtifactAttached=false, createDependencyReducedPom=true), matching
iceberg-spark-runtime / hudi-spark-bundle. Resolving the coordinate via a
Maven dependency or --packages now yields the relocated bundle and pulls no
un-relocated transitive deps; --jars is equivalent. The thin main jar +
separate -bundle classifier (which re-introduced the cluster guava clash)
is gone. IT findBundleJar() now picks the shaded main jar.

Pass release/scripts/validate_shaded_license_coverage.sh (the existing
allowlist gate): the shade <includes> must equal the runtime dependency
tree, so jackson / scala-library / log4j-1.2-api - which every Spark
runtime supplies - are declared provided (dropped from the tree, kept off
the shaded jar) and excluded from the IT's flat engine classpath so Spark's
own copies win. avro/parquet stay on the flat classpath (the engine's newer
avro must win over Spark 3.4's).

Bundled-dependency licensing: add META-INF/LICENSE-bundled and
NOTICE-bundled (wired via IncludeResourceTransformer) attributing the only
bundled third-party - guava's closure (Apache-2.0) and commons-cli
(Apache-2.0), plus checker-qual (MIT). Remove the dead protobuf-java
allowlist entry and relocation: protobuf resolves as provided and was never
bundled.

RFC-3: scope v1 to the CLI (XTableSparkSync); mark the config-only
XTableSyncListener on-ramp (and XTableSparkConfig / PlanTargetResolver) as a
deferred follow-up. Fix the activation example to the shipped CLI and
document the required engine Avro version + flat-classpath placement.

XTableSyncService: normalize sourceFormat with Locale.ROOT.

Root pom: exclude the shade-generated dependency-reduced-pom.xml from
spotless (no license header; CI runs clean install).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate repeated rationale in the bundle pom (no functional change):
- Tell the hudi-common DecimalWrapper / Avro-1.8-1.9 story once (on the
  hudi-spark bundle dep); the hudi-java-client exclusion just points to it.
- State "engine Avro must win on a flat classpath" once (engine-classpath
  plugin); the avro dep and excludeGroupIds comments reference it.
- Explain jackson/scala-library are Spark-supplied once.
- Fix a stale "Spark 3.5" reference to Spark 3.4 (this is the Hudi 0.x line).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add main-hudi-0x to the push and pull_request branch filters of both
workflows (same change as apache#841) so CI runs on PRs targeting the Hudi 0.x
release line, including this one. A pull_request workflow's branch filter
takes effect from the PR's own branch, so this makes CI fire on apache#843.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vinishjail97
vinishjail97 marked this pull request as ready for review July 10, 2026 01:29
…k-submit stdout off-thread

- ConversionTargetFactory: hasNext() resolves provider classes lazily and can
  throw ServiceConfigurationError too, so move it inside the existing
  ServiceConfigurationError|LinkageError guard alongside next().
- ITXTableSparkRuntimeBundle: drain spark-submit stdout on a background thread
  so a hung process is caught by waitFor(10min) instead of blocking forever on
  readOutput() reading until EOF.
main-hudi-0x was deleted and replaced by branch-0.4 (the 0.4.0 release line),
so update the push/pull_request branch filters accordingly.
…istro

ITXTableSparkRuntimeBundle spark-submits the shaded bundle jar and
self-skips unless SPARK_HOME is set, so the main CI never runs it. Add a
path-filtered workflow that installs a matching Spark 3.4 distribution
from the Apache archive, sets SPARK_HOME, and runs the failsafe IT.
…filter)

The workflow is intended to be a required status check on branch-0.4. A
required check whose workflow is skipped by a path filter never reports,
leaving PRs blocked on a pending check, so run it unconditionally on the
covered branches. The Spark distro is cached, so the added cost is the
reactor build plus the ~20s IT.

@vinothchandar vinothchandar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @vinishjail97 ~ I think this will make life 10x better for users.

Comment thread .github/workflows/mvn-ci-build.yml
Comment thread .github/workflows/spark-runtime-validation.yml Outdated
Comment thread xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSparkSync.java Outdated
Comment thread xtable-spark-runtime/src/main/java/org/apache/xtable/spark/XTableSparkSync.java Outdated
Comment thread .github/workflows/spark-runtime-validation.yml
Comment thread xtable-spark-runtime/pom.xml
Comment thread xtable-spark-runtime/pom.xml
Comment thread rfc/rfc-3/rfc-3.md
Comment thread rfc/rfc-3/rfc-3.md
Comment thread rfc/rfc-3/rfc-3.md
…unix flags, docs)

- XTableSparkSync: validate --sourceformat/--targets up front and fail fast
  (before SparkSession creation) on empty or unsupported values. Split the
  allowed sets: sources are Hudi/Iceberg/Delta/Paimon/Parquet, targets are
  Hudi/Iceberg/Delta (Paimon/Parquet are read-only, no ConversionTarget).
- XTableSyncService.sourceProviderFor: wire Paimon and Parquet source providers
  (previously threw UnsupportedOperationException); engines remain cluster-provided.
- Rename CLI long-opts to unix-style lowercase (--basepath, --sourceformat,
  --tablename, --datapath, --partitionspec); update javadoc, IT and RFC example.
- basePathToName -> basePathToTableName: handle "/", trailing slashes and null
  by throwing with a "pass --tablename" hint instead of an empty table name.
- Add XTableSparkSyncTest covering table-name derivation and source/target
  format validation.
- ConversionTargetFactory: log the skipped provider's error class/message so
  operators can distinguish an intentionally-absent engine from a linkage error.
- spark-runtime-validation.yml: add a workflow_dispatch spark_version input and
  document why the Spark 3.4 line is pinned (Delta 2.4.0 is Spark-3.4-only).
- RFC-3: add a supported-formats/engine-versions section, "from application
  code" (Scala/Java + PySpark) activation examples, and list @vinothchandar as
  an approver.
@vinishjail97 vinishjail97 changed the title [836] xtable-spark-runtime: thin drop-in Spark bundle (Hudi 0.x) [836] xtable-spark-runtime: thin drop-in Spark bundle for 0.4.x branch Jul 20, 2026
Delta source/target now auto-switch from delta-core to the Spark-free
Delta Kernel implementation on Spark 3.5+ (where the bundled delta-core
does not run), controlled by a single --usedeltakernel toggle that is
auto-enabled by Spark version. Hudi/Iceberg sync was already Spark-free.

Also runs the spark-runtime bundle IT on both Spark 3.4.3 and 3.5.9 via
a CI matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vinishjail97 vinishjail97 changed the title [836] xtable-spark-runtime: thin drop-in Spark bundle for 0.4.x branch [836] xtable-spark-runtime: thin drop-in Spark bundle for 0.4.x (Spark 3.4 + 3.5) Jul 20, 2026
Both workflows used a job named "build", so both reported the same
status-check context and could not be required distinctly. Set unique
job names so branch protection (see apache#848) can
require each on branch-0.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@vinothchandar vinothchandar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the CR comments. Good to land once CI is green

XTableSparkSync accepts a --datasetconfig YAML (sourceFormat, targetFormats,
datasets[]) to sync multiple tables in one run, mutually exclusive with the
single-table --basepath/--sourceformat/--targets flags. The config is read
through the Spark Hadoop config, so it may live on a local or cloud
(s3/gcs/abfs) path, and reuses the same schema as the RunSync utility.

Parsed with SnakeYAML's SafeConstructor (plain maps/lists only, no arbitrary
type instantiation). SnakeYAML is bundled and relocated to
org.apache.xtable.shaded (like guava/commons-cli) because Spark ships its own
version (1.33 on 3.4, 2.0 on 3.5) that would otherwise clash; its multi-release
classes are filtered out so no un-relocated org.yaml classes remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vinishjail97

vinishjail97 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

In addition to the ITXTableSparkRuntimeBundle matrix, I ran the bundle end-to-end against real cloud storage on both Spark lines.

Setup:

  • Built the xtable-spark-runtime jar from this branch (4.3 MB shaded bundle) and ran XTableSparkSync via spark-submit. Engines were supplied on a flat classpath from target/engine-classpath.txt (Hudi 0.14, Iceberg 1.9.2, Delta), plus hadoop-aws for S3A access.
  • Source data was a 100 GB TPC-DS dataset on S3: 24 Hudi (COW) tables, synced in a single submit using the --datasetconfig multi-table mode.
  • Source format HUDI, targets ICEBERG and DELTA.
  • Ran the same jar on Spark 3.5.2 and Spark 3.4.3 (local mode). On 3.5.2 the Delta side ran through the Delta Kernel path (auto-enabled); on 3.4.3 it ran through Delta Standalone (delta-core).

Result (each run):

  • 24 of 24 tables synced, exit 0, no failures.
  • Verified the Iceberg (metadata/) and Delta (_delta_log/) metadata were written on S3 alongside the existing Hudi data files; no data was rewritten.
  • Read a sample of the tables back through Spark and confirmed the data matches across all three formats (Hudi, Iceberg, Delta).

Module docs (README plus a Spark quickstart, including a --datasetconfig TPC-DS example) are in #847.

@vinishjail97
vinishjail97 merged commit f2d9873 into apache:branch-0.4 Jul 21, 2026
4 checks passed
@vinishjail97
vinishjail97 deleted the 836-spark-runtime-hudi0x branch July 21, 2026 18:50
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.

3 participants