Skip to content

issue #7486 : A dedicated Spark pipeline execution engine#7539

Merged
mattcasters merged 44 commits into
apache:mainfrom
mattcasters:issue-7486
Jul 19, 2026
Merged

issue #7486 : A dedicated Spark pipeline execution engine#7539
mattcasters merged 44 commits into
apache:mainfrom
mattcasters:issue-7486

Conversation

@mattcasters

Copy link
Copy Markdown
Contributor

Summary

Adds a native Apache Spark 4.1.x pipeline engine for Hop batch pipelines (without Apache Beam), so the community can try Dataset-based execution alongside the existing Beam Spark runner.

Highlights

  • Engine & run configuration for Spark 4.1 (local[*] and cluster / spark-submit via MainSpark)
  • Native handlers: Memory Group By, Merge Join, Unique Rows, Sort Rows, Spark File Input/Output
  • Generic mapPartitions for other transforms (Filter, Switch/Case, Stream Lookup, Select Values, …)
  • Info streams (broadcast side data) for Stream Lookup and similar
  • Target streams for Filter Rows (true/false) and Switch/Case
  • Multi-previous merge when all input row layouts match (Beam Flatten equivalent)
  • Execution information location + sampling (driver metrics + executor samples)
  • User manual: Getting started with the native Spark engine
  • Integration tests: integration-tests/spark-native/ (0001–0006, 0099 complex)

Notes for reviewers / try-out

  • Engine plugin: plugins/engines/spark
  • Prefer Spark File Input/Output (not classic Text File I/O) on this engine
  • local[*] uses Spark libs under plugins/engines/spark/lib
  • Cluster: use fat jar with --spark-client-version=native-provided and org.apache.hop.spark.run.MainSpark
  • Streaming / windows remain on Beam engines
  • Hop Distribute vs Copy is not implemented (same approach as Beam: target streams route; no round-robin)

Test plan

  • ./mvnw -pl plugins/engines/spark test
  • Run integration-tests/spark-native workflows (main-0001main-0006, complex pipeline)
  • Create a Native Spark run configuration (local[*]) and run a small pipeline from Hop GUI
  • Optional: spark-submit with native-provided fat jar against a Spark 4.1 cluster
  • Spotless / RAT / docs build as applicable

Draft PR for early community feedback and try-out before full review/merge.

Related: #7486

…et streams, multi-input, docs)

Add target-stream fan-out for Filter Rows and Switch/Case, union of multiple
previous transforms with the same row layout, no-header CSV positional mapping,
metrics fix for leaf mapPartitions transforms, integration tests, and user-manual
getting-started documentation for the native Spark engine.
…sic I/O, ITs, docs)

Support classic file I/O on mapPartitions (accept-filenames, partition-scoped
Internal.Transform.*), active-graph collection for disabled hops, integration
tests 0007/0008, and a full native Spark getting started guide with screenshots.
@mattcasters

Copy link
Copy Markdown
Contributor Author

Update since PR opened

Follow-up work on the native Spark engine after the original PR (target streams / multi-input / early docs):

Engine / runtime

  • Classic I/O on mapPartitions: stage textfile / getfilenames / excel; partition-scoped ${Internal.Transform.ID} / CopyNr / BundleNr + Beam-parity beamContext for Text File Output uniqueness.
  • Accept-filenames (Get File Names → Text File Input): re-bind accept_transform_name to the mini-pipeline main injector (_INJECTOR_); preload all filename rows and drive with direct processRow() (avoids SingleThreadedPipelineExecutor stall after createReader when input hop size hits 0).
  • Active graph only: skip disabled/orphaned transforms so sinks without an enabled input path are not executed.
  • Spark File Output minor handler tweak; unit tests for accept-filenames binding, parallel file context, converter active-graph behaviour.
  • BaseFileInputMeta.setAcceptingTransformName for engine rebinding without protected getFileInput() access.
  • Small Text File Input dialog NPE guard on empty date locale.

Integration tests (integration-tests/spark-native)

  • 0007 Text File Input spark readback (create CSVs → GFN → TFI → Parquet → validate).
  • 0008 Excel catalog as Stream Lookup info + main from 3 Parquet parts → golden unit test.
  • 0099 complex golden/pipeline touch-ups.

Documentation

  • Rewrote Getting started with the native Spark pipeline engine: what it is, native vs Beam, version matrix, local[*] run config, fat jar (native-provided), detailed spark-submit (incl. 4th arg environment config for cluster vars like PROJECT_HOME=s3://…), Spark File I/O, ${Internal.Transform.ID}, metrics & execution information, limitations (Group By, Copy/Distribute, no native Kafka / Spark SQL / MLlib), and illustrated IT examples.
  • Screenshots under docs/.../assets/images/pipeline/spark/.
  • Linked from pipelines overview and native Spark run-config page.

Ready for another review pass when convenient.

…house Delta/Iceberg)

Add optional open table format support on the native Spark engine: Lake Table
Input/Output/Merge/Maintenance, Spark Catalog metadata, connector probe and
session plan, Maven -Plakehouse tests, user docs, and sample walkthroughs.
Connectors remain operator-installed (not in default assembly).
@mattcasters

Copy link
Copy Markdown
Contributor Author

Changelog: Open table formats (lakehouse) series on native Spark

This commit adds optional Delta Lake / Apache Iceberg support on the native Spark pipeline engine (not Beam). Summary of the planned PR stack landed together as cb9f5a2dcf.

Pins

Component Version
Spark (engine) 4.1.2 / Scala 2.13
Delta Lake io.delta:delta-spark_4.1_2.13:4.3.1
Apache Iceberg org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.11.0

Distribution: connectors are not in the default Hop assembly (docs-only + operator install). Install under plugins/engines/spark/lib/{delta,iceberg} + restart Hop, or use spark-submit --packages. Sibling plugin classloaders are not enough (engine TCCL).


PR 1 — Packaging spike + connector probe

  • Maven profile -Plakehouse: optional test deps + generate-test-resources → frozen jar list under target/lakehouse-connectors/
  • SparkLakeConnectorProbe with actionable missing-connector errors
  • Spike findings documented in plugins/engines/spark/README.md:
    • Delta PATH requires extension + DeltaCatalog (not extension alone)
    • Iceberg bare format("iceberg").load routes toward HiveCatalog → use hop_iceberg Hadoop catalog for PATH

PR 2 — Lake Table Input/Output PATH (Delta)

  • Transforms: Spark Lake Table Input / Spark Lake Table Output (Big Data; native Spark only)
  • Default Output save mode: ErrorIfExists (ACID-safe)
  • Handlers registered in HopPipelineMetaToSparkConverter; action-sink / empty-leaf write contract
  • Unit tests without connector where possible; Delta PATH IT under -Plakehouse

PR 3 — Iceberg PATH Input/Output

  • Symmetric PATH support for Iceberg via writeTo(hop_iceberg.\uri`)` + SQL read
  • Built-in hop_iceberg catalog coexists with Delta on spark_catalog
  • Iceberg PATH IT under -Plakehouse

PR 4 — Time travel on Input

  • Types: NONE / VERSION / TIMESTAMP
  • Delta: versionAsOf / timestampAsOf; Iceberg: VERSION AS OF / TIMESTAMP AS OF
  • IT: write-twice / read-as-of

PR 5 — Spark Catalog + LakeSessionPlan + TABLE mode

  • Metadata type Spark Catalog (Connections): Hadoop / REST / custom / advanced hive+glue
  • LakeSessionPlan + SparkCatalogApplier wire conf into hop-run session build (and active session where possible)
  • TABLE mode on Input/Output (Iceberg-first; Delta TABLE advanced)
  • Iceberg TABLE write locked to writeTo

PR 6 — MERGE

  • Spark Lake Table Merge: single upstream = source; target PATH or TABLE
  • Structured actions: matched UPDATE_ALL/DELETE/NONE, not-matched INSERT_ALL/NONE, optional not-matched-by-source DELETE
  • Advanced raw MERGE SQL override; log-only metrics
  • Delta PATH MERGE upsert IT

PR 7 — Maintenance

  • Spark Lake Table Maintenance (zero-input sink)
  • Operations: OPTIMIZE / VACUUM / EXPIRE_SNAPSHOTS / REWRITE_MANIFESTS / DELETE_WHERE
  • Retention required for vacuum/expire; acknowledge destructive for destructive ops
  • Delta OPTIMIZE smoke IT

PR 8 — Docs + samples

  • User guide: docs/.../pipeline/spark/lakehouse.adoc
  • Transform pages + metadata-types/spark-catalog.adoc; nav / indexes / getting-started cross-links
  • Operator samples: integration-tests/spark-native/lakehouse/README.md
  • Engine README updated with full lakehouse section

How to try

# Default tests (no connector download)
./mvnw -pl plugins/engines/spark -am test

# Lakehouse classpath + PATH/MERGE/maintenance ITs
./mvnw -pl plugins/engines/spark -am test -Plakehouse

Manual overlay: copy target/lakehouse-connectors/* into $HOP_HOME/plugins/engines/spark/lib/{delta,iceberg} and restart Hop. Cluster: native-provided fat jar + --packages (see lakehouse.adoc).

Out of scope (optional later / PR 9)

  • ASF-shipped connector overlay zip / fat-jar lakehouse tokens
  • MDI, Hudi
  • Default assembly embedding of Delta/Iceberg jars

… lakehouse connectors)

Embed Delta Lake 4.3.1 and Apache Iceberg 1.11.0 in the default engines-spark
assembly under lib/delta and lib/iceberg. native-provided fat jars keep these
connectors; docs and probe messages updated for shipped packaging.
@mattcasters

Copy link
Copy Markdown
Contributor Author

Changelog: PR 9 — ship Delta/Iceberg in default assembly

Follow-up to the lakehouse series (PR 1–8): connectors are now packaged with the native Spark engine by default.

What changed

  • Runtime Maven deps on hop-engines-spark:
    • io.delta:delta-spark_4.1_2.13:4.3.1 (+ kernel/storage/unitycatalog tree)
    • org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.11.0
    • Exclusions: Spark, Scala, Jackson core stack, slf4j (already on Hop / spark lib)
  • Plugin zip layout
    • plugins/engines/spark/lib/delta/*
    • plugins/engines/spark/lib/iceberg/*
    • Staged via dependency:copy-dependenciestarget/lakehouse-connectors → assembly fileSets (full freeze list)
  • -Plakehouse retained as a no-op alias for older scripts; lakehouse ITs run on the default test classpath
  • Fat jar (native-provided): lakehouse jars are not excluded (tests added in HopBeamGuiPluginNativeFatJarTest) — cluster submit no longer needs --packages for connectors when using a current fat jar
  • Probe / docs / samples / NOTICE updated for shipped packaging (manual overlay is fallback only)

How to verify

./mvnw -pl plugins/engines/spark -am package -DskipTests
unzip -l plugins/engines/spark/target/hop-engines-spark-*.zip | grep 'lib/delta\|lib/iceberg'

./mvnw -pl plugins/engines/spark test   # 103 tests, includes lakehouse ITs

Still optional / out of scope

  • MDI for Lake Table transforms
  • Hudi
  • Separate ASF overlay zip (not needed now that jars ship in the engine plugin)

… table MDI)

Enable metadata injection on Spark Lake Table Input/Output/Merge/Maintenance
so multi-table landings, CDC merges, and fleet maintenance can use template
pipelines. Add injection unit tests and document inject keys.
@mattcasters

Copy link
Copy Markdown
Contributor Author

Lake Table metadata injection (MDI)

Follow-up for complex multi-table / CDC-style work: all four Lake Table transforms are now metadata-injection capable.

What

  • @HopMetadataProperty(injectionKey=…) on Input, Output, Merge, Maintenance
  • Input field projection group FIELDS (NAME, TYPE, LENGTH, PRECISION, FORMAT_MASK via SparkField)
  • Unit tests: SparkLakeTable*MetaInjectionTest (4 tests, green)
  • Docs: each transform page + lakehouse guide + samples README

Shared inject keys

FORMAT, IDENTIFIER_MODE, TABLE_PATH, TABLE_IDENTIFIER, CATALOG_METADATA_NAME

Plus transform-specific keys (save mode, merge condition/actions, OPTIMIZE/VACUUM params, …) listed on the transform docs.

Pattern

Template lake pipeline → ETL Metadata Injection maps rows from a control/source pipeline → run under Native Spark.

…house/iceberg ITs)

Add integration-tests/lakehouse (Delta) and integration-tests/iceberg projects
with PATH Input/Output, overwrite + time travel (Delta VERSION 0), and MERGE
upsert workflows. Wire Docker compose and docs cross-links.
@mattcasters

Copy link
Copy Markdown
Contributor Author

Integration tests: lakehouse (Delta) + iceberg

New Hop IT projects for end-to-end Lake Table transforms on native Spark:

Project Format Scenarios
integration-tests/lakehouse Delta 0001 round-trip, 0002 overwrite + VERSION 0 time travel, 0003 MERGE
integration-tests/iceberg Iceberg 0001 round-trip, 0002 double overwrite + current, 0003 MERGE

Docker: integration-tests-lakehouse.yaml / integration-tests-iceberg.yaml (beam-base, Spark UI off).

Local verify (both green, exit 0):

export HOP_LOCATION=…/assemblies/client/target/hop
./integration-tests/scripts/run-tests.sh lakehouse
./integration-tests/scripts/run-tests.sh iceberg

…ssions

Use /tmp/hop-it-{lakehouse,iceberg} for tables and extracts, and replace
DELETE_FILES with a Shell cleanup that always exits 0. Avoids failures
deleting host-owned Spark .crc files on the mounted project volume.
… config fields

Introduce MULTI_LINE_TEXT with multiLineTextHeight (lines, default 1) on
@GuiWidgetElement, wire creation/layout/bind in GuiCompositeWidgets, and use
it for native Spark sparkConfigs (5 lines).
…config

Add a Load configuration template button with Local, capped local,
standalone, spark-submit, Databricks, YARN, and lakehouse presets.
Wire PipelineRunConfigurationEditor button listener and refresh widgets
after button actions so form fields update.
…UTTON widgets

Add Load catalog template presets (Iceberg Hadoop/REST, Hive, Glue, Nessie,
Unity, Delta) with # docs: links in conf extra. Wire SparkCatalogEditor
button flush/rebind, multi-line conf extra, and GuiCompositeWidgets
afterButtonPressed refresh so form fields update after modal template apply.
Document templates in spark-catalog and cross-link from lakehouse/native Spark.
Add workspace connection (PAT) for the Jobs REST API, a RestDatabricksJobsClient
with WireMock tests, and a Big Data workflow action to run-now or one-time
submit with wait/fire-and-forget and result variables. Document connection
and action; nav and metadata index links.
…nSpark JAR)

Add Deploy & run: upload fat jar, pipeline, and metadata JSON to DBFS,
create or update a JAR job (MainSpark) on an existing cluster, then
run-now. JobSpecFactory, HopSparkDeployHelper, client uploadToDbfs,
dialog fields, tests, and docs.
Extract shared DatabricksRunWaiter used by Job Run and new Job Wait.
Job Wait polls by run id (default ${DatabricksRunId}) with timeout,
poll interval, cancel-on-stop, and the same result variables. Docs and
nav cross-links for fire-and-forget + wait orchestration.
Document that Spark File/Lake paths use Hadoop URIs (s3a://, hdfs://),
not Hop VFS (s3://, named MinIO). Relabel UI fields, fix cluster examples,
and append error hints via SparkPathDialect when known Hop-only schemes
appear in Dataset I/O failures.
Add an optional from=to path scheme map on the Native Spark run
configuration so Dataset and Lake PATH I/O can rewrite schemes (e.g.
s3=s3a, minio=s3a) after variable resolution. Classic Hop VFS is
unchanged; FS credentials still come from Spark/Hadoop config.
…ster

Add a Native Spark project package (export via Tools menu and hop-conf
--export-spark-project) so Simple Mapping and Pipeline Executor can resolve
${PROJECT_HOME} children on workers. MainSpark accepts --HopProjectPackage;
the engine distributes the zip with SparkContext.addFile and mini-pipelines
materialize PROJECT_HOME from SparkFiles on each executor.
Add spark-mapping-demo sample project, Spark 4.1 compose cluster with
shared data volume, prepare-dist and spark-submit-demo scripts, and a
user-manual walk-through so Simple Mapping under PROJECT_HOME can be
verified on multi-node spark-submit with a project package.
…HOME

--project/-p only names a managed project and does not set PROJECT_HOME.
Export now accepts --export-spark-project-home and loads metadata from
that folder; prepare-dist.sh no longer requires registering the sample.
…aults

Open SparkFiles-local package zips via java.io (HopVfs misses bare
absolute paths). Default DIST_DIR to /tmp/spark-mapping-demo-dist.
Spark 4 master uses --host instead of --ip.
Stop stashing SparkFiles userFiles paths in HOP_SPARK_PROJECT_PACKAGE
(not valid on other JVMs). Resolve package from SparkFiles, then
$HOP_DATA/packages/<zip>, then the portable URI. Demo submit copies the
zip onto the shared volume and passes --files as backup.
@mattcasters

Copy link
Copy Markdown
Contributor Author

Follow-up: VFS vs Spark paths, project packages, and cluster mapping demo

Pushed additional work on this branch after the initial Native Spark engine landings. Summary of improvements:

Hop VFS vs Spark/Hadoop path dialects

  • Documented two path languages clearly (Hop VFS s3:// / named MinIO vs Spark/Hadoop s3a://, hdfs://, …).
  • Spark File / Lake path fields labeled as Spark path (Hadoop URI) with tooltips.
  • SparkPathDialect appends error hints when known Hop-only schemes appear in Dataset I/O.
  • Run-configuration path scheme map (from=to, e.g. s3=s3a) rewrites schemes for Spark File / Lake PATH only (not classic VFS).

Native Spark project package (nested pipelines / mappings)

  • Problem: Simple Mapping / Pipeline Executor only store a path (${PROJECT_HOME}/…); MainSpark shipped top-level .hpl + metadata only, so children failed on cluster workers.
  • Package layout: project/ tree + root metadata.json + marker (skips bulk datasets/ by default).
  • Export:
    • CLI: hop-conf --export-spark-project=… --export-spark-project-home=… (no project registration required; note -j enables a project, -p/--project only names one for manage ops).
    • GUI: Tools → Export project package for Native Spark…
  • Runtime: MainSpark --HopProjectPackage=… --HopPipelinePath=…; engine sets PROJECT_HOME after materialize under java.io.tmpdir.
  • Multi-host: resolve package from SparkFiles → $HOP_DATA/packages/<zip> (shared volume) → portable URI. Never stash driver-local …/userFiles-… paths in variables. Demo also uses spark-submit --files as backup.
  • Data vs definitions: Dataset paths use HOP_DATA (or s3a://…); package PROJECT_HOME is definitions only. Warnings when Spark File paths still reference PROJECT_HOME.

Docker walk-through (end-to-end proof)

  • Sample: plugins/engines/spark/src/samples/spark-mapping-demo (parent + Simple Mapping child + tiny CSV).
  • Compose: docker/integration-tests/integration-tests-spark-native-cluster.yaml — Spark 4.1.2 master + workers, shared data volume.
  • Scripts: prepare-dist.sh (default dist /tmp/spark-mapping-demo-dist), spark-submit-demo.sh (docker exec on master).
  • Docs: Cluster mapping walk-through + paths-and-filesystems updates.
  • Fix: Spark 4 master CLI uses --host (not Spark 3 --ip).

Verified

Local multi-node Docker submit of 01-enrich-with-mapping.hpl with nested mapping under package PROJECT_HOME completed successfully.

Happy path (from repo root):

./plugins/engines/spark/src/samples/spark-mapping-demo/scripts/prepare-dist.sh
docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \
  up -d --build --scale spark-worker=2
docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \
  exec spark /opt/hop-samples/spark-mapping-demo/scripts/spark-submit-demo.sh

…ark-demo

Add DISTRIBUTED/DRIVER_ONLY generic transform run mode with per-transform
override and canvas badge, nested Native Spark session reuse for Pipeline
Executor, Hop VFS-safe execution-info cache writes, and rename/expand the
cluster sample to spark-demo (workflows, nested pipelines, host hop-data
bind mount).
@mattcasters

Copy link
Copy Markdown
Contributor Author

Update: Spark run mode, nested pipelines, and spark-demo expansion

Pushed follow-up work that has been exercised on the Docker Spark cluster.

Engine

  • Generic transform run mode on Native Spark run config: DISTRIBUTED (default) vs DRIVER_ONLY
  • Per-transform override via context action Spark Run Mode (Inherit / Force distributed / Force Driver Only), stored on TransformMeta.attributes (spark / run_mode)
  • Bottom-left canvas badge for forced modes (DrawSparkRunModeOnTransformExtensionPoint)
  • Nested Pipeline Executor → Native Spark child: reuses parent SparkSession; nested engines do not stop/cancel the shared session
  • Warning when Pipeline Executor runs DISTRIBUTED with nested Spark intent

Execution info

  • CacheEntry.writeToDisk now uses Hop VFS so roots like file:///data/hop-data/executions (from ${HOP_DATA}/executions) work
  • Unit coverage for file:// root folder writes

spark-demo (renamed from spark-mapping-demo)

  • 01 mapping package demo (existing)
  • 02 Workflow Executor → country folders + instance markers
  • 03 nested Native Spark pipelines per country (Force Driver Only)
  • Execution info location + data profile sample metadata; host GUI vars in spark-demo-gui-config.json
  • Compose: host bind mount /tmp/spark-demo-dist/hop-data/data/hop-data so out/ and executions/ are visible without docker exec

Docs

  • Native Spark run config + cluster walk-through updates, including Force Driver Only screenshot

Happy path on the host after a run:

ls -la /tmp/spark-demo-dist/hop-data/out
ls -la /tmp/spark-demo-dist/hop-data/executions

@mattcasters
mattcasters marked this pull request as ready for review July 17, 2026 20:06
…ummary paths

Write temp Excel/ODS outputs under project output/ (world-writable), align
container UID defaults with the ASF Jenkins agent identity, and fix relative
path bugs when printing passed/failed test summaries from the repo root.
# Conflicts:
#	integration-tests/scripts/run-tests-docker.sh
…rent folders, hop_server rebuild

Clear empty field mappings on PipelineExecutor so child parameter defaults
apply (IT main-0003). Harden JsonOutput parent-folder creation and messages
for Beam 0005. Force docker compose --build for hop_server so remote export
ITs use a server image matching current assemblies.
…ithout GCP key

Wipe residual Spark output under project output/ (including other-UID files)
before IT runs so delete-output steps succeed. Skip spreadsheet Google Sheets
workflows when GCP_KEY_FILE is missing or not a service-account JSON, and
surface a clear credentials error from GoogleSheetsCredentials.
…unner

The Google Sheets skip only ran on the classic per-workflow path. Spreadsheet
ITs default to run-project-tests.hpl, so main-0009/0010 still executed with the
dummy GCP key. Force the classic runner for the spreadsheet project when
SKIP_GOOGLE_SHEETS=true so those workflows are skipped.
Do not delete tracked .gitkeep/.gitignore under project output/ when wiping
residual Spark/UID-mismatched files before integration tests.
LDAP stores setup pipelines in ldap/output/*.hpl. The pre-test output cleanup
was deleting everything except .gitkeep/.gitignore, which broke main-ldap-tests.
Only remove untracked residuals (git clean) and Spark part-*/.crc artifacts.
… fat jar, library-cache lesson

Stage UC Volume project packages to local disk for SparkFiles addFile so
executors rematerialize PROJECT_HOME (mappings and nested pipelines).
Deploy fat jars as hop-native-<sha12>.jar so Dedicated clusters pick up new
library URIs after rebuilds; document overwrite-vs-classpath caching and
cluster restart. MainSpark prints pkg-dist fingerprint for verification.
@mattcasters
mattcasters merged commit c84f65c into apache:main Jul 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant