Skip to content

Oct 29408 qa - #918

Open
zacaydcloudera wants to merge 6 commits into
AbsaOSS:developfrom
OCTOPAILTD:OCT_29408_qa
Open

Oct 29408 qa#918
zacaydcloudera wants to merge 6 commits into
AbsaOSS:developfrom
OCTOPAILTD:OCT_29408_qa

Conversation

@zacaydcloudera

@zacaydcloudera zacaydcloudera commented Jun 30, 2026

Copy link
Copy Markdown

Summary

  • Resolves an issue where Delta Lake and Iceberg emit Spark V2 ReplaceData operations causing Spline to crash.
  • Safely extracts writeOptions using Try().getOrElse so missing fields don't throw NoSuchFieldException
  • Adds wildcard fallback to cleanly handle unknown V2 WriteCommands like ReplaceData instead of crashing.
  • Adds safe null checking for extracting location path

Test plan

  • Simulated Apache Iceberg UPDATE row-level operations locally, verified Spline generated lineage correctly instead of crashing.

Summary by CodeRabbit

  • New Features

    • Added a new Java example job demonstrating Spark lineage tracking with file-based lineage output.
  • Bug Fixes

    • Improved HDFS lineage persistence with atomic file writes, safer cleanup, run-scoped JSON output, and better handling of file:// paths.
    • Hardened lineage dispatch across HTTP/Kafka and primary-fallback/composite dispatch so non-fatal failures and listener setup issues don’t break Spark execution.
    • Made write-option and delta-table property parsing more tolerant of missing values.
  • Chores

    • Switched the build to the Scala 2.11 artifact line.

zacayd and others added 4 commits June 24, 2026 13:21
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…irectory

Co-authored-by: Cursor <cursoragent@cursor.com>
- Safely extract writeOptions using Try().getOrElse

- Add wildcard fallback to capture unknown V2 WriteCommands like ReplaceData

- Add safe null checking for extracting location path

Co-authored-by: Cursor <cursoragent@cursor.com>
@wajda

wajda commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have failed. 12 issues have been found so far.

Status Scan Engine Critical High Medium Low Total (12)
Open Source Security 6 0 6 0 12 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 962d844d-3bcf-4551-8f1f-577fcef78670

📥 Commits

Reviewing files that changed from the base of the PR and between d499ab7 and 690b504.

📒 Files selected for processing (1)
  • pom.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • pom.xml

Walkthrough

The PR updates Maven modules to Scala 2.11, hardens lineage dispatch and listener error handling, rewrites HDFS lineage persistence to use run-scoped atomic writes, and adds a Java example that runs Spark with the HDFS dispatcher.

Changes

Scala 2.11 migration and runtime hardening

Layer / File(s) Summary
Scala 2.11 coordinates
pom.xml, commons/pom.xml, core/pom.xml, examples/pom.xml, integration-tests/pom.xml, bundle-2.2/pom.xml, bundle-2.3/pom.xml, bundle-2.4/pom.xml
Root and module POMs update Scala version properties and artifact identifiers from 2.12 to 2.11.
Defensive lineage handling
core/src/main/scala/za/co/absa/spline/harvester/plugin/embedded/DataSourceV2Plugin.scala, core/src/main/scala/za/co/absa/spline/agent/SplineAgent.scala, core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala, core/src/main/scala/za/co/absa/spline/harvester/dispatcher/FallbackLineageDispatcher.scala, core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HttpLineageDispatcher.scala, core/src/main/scala/za/co/absa/spline/harvester/dispatcher/KafkaLineageDispatcher.scala, core/src/main/scala/za/co/absa/spline/harvester/listener/SplineQueryExecutionListener.scala
Plugin extraction, staged dispatch, dispatcher delegation, fallback handling, per-record HTTP/Kafka sends, and query-execution callbacks add safer defaults and NonFatal handling.
HDFS lineage persistence and example
core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala, examples/src/main/java/za/co/absa/spline/example/batch/JavaExampleJob_Files.java
HDFS lineage writing switches to run-scoped directories, cleanup, atomic file replacement, and file URI handling; the new Java example configures and exercises that dispatcher locally.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant JavaExampleJob_Files
  participant SparkLineageInitializer
  participant HDFSLineageDispatcher
  participant cleanupOldRunFolders
  participant persistRunFolderAtomically
  participant writeFileAtomically
  JavaExampleJob_Files->>SparkLineageInitializer: enableLineageTracking(...)
  JavaExampleJob_Files->>HDFSLineageDispatcher: send(plan/event)
  HDFSLineageDispatcher->>cleanupOldRunFolders: remove old run directories
  HDFSLineageDispatcher->>persistRunFolderAtomically: create runDir and lineage file
  persistRunFolderAtomically->>writeFileAtomically: write temp file then rename
Loading

Poem

🐇 I hopped from 2.12 to 2.11,
With lineage paths and helpers spun.
The HDFS writes now land just right,
And noisy failures stay out of sight.
A Java bunny runs the trail,
While Spark keeps calm and does not fail.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too vague and does not describe the actual changes in the pull request. Rename the PR to summarize the main change, such as the Scala 2.11 migration and Spark V2 write handling fixes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala`:
- Line 79: The HDFSLineageDispatcher is ignoring the configured output name
because `fileName` is hardcoded to `lineage_${executionPlanID}.json`. Update
`HDFSLineageDispatcher` so it uses the constructor/configured `fileName` value
when building the HDFS target path, keeping the existing `fileName` symbol and
replacing the inline literal with the configured contract.
- Around line 121-139: The current persistRunFolderAtomically implementation in
HDFSLineageDispatcher only makes the lineage file write atomic, not the whole
run-folder materialization, because it creates finalRunDir before writing the
file. Update persistRunFolderAtomically to follow the documented temp-directory
flow by writing into a hidden temp dir and renaming it to the final run
directory, or else adjust the method/comment to explicitly promise only
file-level atomicity. Ensure the behavior matches the guarantee implied by
writeFileAtomically and pathStringToFsWithPath.
- Around line 107-111: The cleanup in HDFSLineageDispatcher’s status iteration
is too broad because it deletes every directory whose name differs from the
currentRunID, which can remove unrelated run outputs. Update the logic around
the statuses.foreach block to only remove clearly stale temporary/TTL-expired
folders, or move this cleanup out of the write path entirely; keep the check in
HDFSLineageDispatcher scoped to safe, owned temp artifacts instead of sibling
run directories.
- Around line 157-162: The durability hints in writeFileAtomically are being
applied unconditionally, which can break writes on FileSystem implementations
that do not support them. Update the try block in
HDFSLineageDispatcher.writeFileAtomically to treat out.hflush() and out.hsync()
as best-effort by either checking the filesystem capability first or catching
UnsupportedOperationException around those calls, then continue with the
existing close and temp-file rename flow regardless.

In
`@core/src/main/scala/za/co/absa/spline/harvester/plugin/embedded/DataSourceV2Plugin.scala`:
- Around line 190-194: The fallback URI in DataSourceV2Plugin’s SourceIdentifier
construction currently hardcodes "unknown", which causes different V2 tables to
collide when path and properties.location are missing. Update the URI derivation
logic in the table scanning/identifier-building flow so it uses a table-specific
fallback or leaves the URI absent instead of defaulting to the literal string,
while keeping the provider lookup and SourceIdentifier creation intact.

In
`@examples/src/main/java/za/co/absa/spline/example/batch/JavaExampleJob_Files.java`:
- Around line 31-82: The main method in JavaExampleJob_Files creates a
SparkSession but does not ensure it is closed, leaving resources open on failure
or exit. Update the SparkSession session lifecycle in main so the job uses a
try/finally (or equivalent cleanup) around the Spark work and always calls
session.stop() in the cleanup path, keeping the existing
SparkLineageInitializer.enableLineageTracking and SparkSession.builder setup
intact.
- Line 42: The lineage directory in the Java example is hardcoded to a
Windows-only path, so update JavaExampleJob_Files to build the dispatcher URI
from a portable source such as java.io.tmpdir or a configurable argument instead
of using the fixed file:///C:/spline value. Keep the change localized to the
Spark config setup in the example job so the lineageDispatcher setting works
across Windows, Linux, and macOS.

In `@pom.xml`:
- Around line 147-148: The root build has been switched to Scala 2.11, but the
default cross-build properties still point to 2.12, so update the default Scala
version and binary version properties in pom.xml to match the existing
scala.version and scala.binary.version values; keep the change aligned with the
cross-build configuration used by scala-cross-build-maven-plugin so the default
coordinates and build defaults agree.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b62859b-f7ae-4695-a815-982144468b6a

📥 Commits

Reviewing files that changed from the base of the PR and between d094160 and ade070b.

📒 Files selected for processing (11)
  • bundle-2.2/pom.xml
  • bundle-2.3/pom.xml
  • bundle-2.4/pom.xml
  • commons/pom.xml
  • core/pom.xml
  • core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala
  • core/src/main/scala/za/co/absa/spline/harvester/plugin/embedded/DataSourceV2Plugin.scala
  • examples/pom.xml
  • examples/src/main/java/za/co/absa/spline/example/batch/JavaExampleJob_Files.java
  • integration-tests/pom.xml
  • pom.xml

// Best-effort cleanup of previous runs
cleanupOldRunFolders(appDir, runID)

val fileName = s"lineage_${executionPlanID}.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the configured filename contract effective.

HDFSLineageDispatcher still requires fileName, but this hardcoded lineage_${executionPlanID}.json path ignores the constructor/config value, so existing dispatcher configuration no longer controls output naming.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala`
at line 79, The HDFSLineageDispatcher is ignoring the configured output name
because `fileName` is hardcoded to `lineage_${executionPlanID}.json`. Update
`HDFSLineageDispatcher` so it uses the constructor/configured `fileName` value
when building the HDFS target path, keeping the existing `fileName` symbol and
replacing the inline literal with the configured contract.

Comment on lines +107 to +111
statuses.foreach { status =>
if (status.isDirectory && status.getPath.getName != currentRunID) {
logInfo(s"Deleting old runID folder: ${status.getPath}")
fs.delete(status.getPath, true) // recursive delete
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid deleting every non-current run folder.

This removes all sibling run IDs under the same plan directory, which can erase lineage history or another active application’s run output. Restrict cleanup to stale temp folders, TTL-expired folders, or remove this cleanup from the write path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala`
around lines 107 - 111, The cleanup in HDFSLineageDispatcher’s status iteration
is too broad because it deletes every directory whose name differs from the
currentRunID, which can remove unrelated run outputs. Update the logic around
the statuses.foreach block to only remove clearly stale temporary/TTL-expired
folders, or move this cleanup out of the write path entirely; keep the check in
HDFSLineageDispatcher scoped to safe, owned temp artifacts instead of sibling
run directories.

Comment on lines +121 to +139
* Atomically create a run directory with its lineage JSON file inside.
* Strategy:
* 1) Ensure parent exists (mkdirs is idempotent)
* 2) Create a hidden temp directory next to the target
* 3) Write JSON to a temp file, then rename temp file -> final name inside temp dir
* 4) Rename temp dir -> final run dir (atomic on HDFS)
*/
private def persistRunFolderAtomically(finalRunDirStr: String, fileName: String, content: String): Unit = blocking {
val (fs, finalRunDir) = pathStringToFsWithPath(finalRunDirStr)

val umask = FsPermission.getUMask(fs.getConf)
FsPermission.getFileDefault.applyUMask(umask)
// Ensure final run directory exists
if (!fs.exists(finalRunDir)) {
fs.mkdirs(finalRunDir)
try fs.setPermission(finalRunDir, permission) catch { case _: Throwable => () }
}

// Write the file atomically inside the run directory
val finalFileInRunDir = new Path(finalRunDir, fileName)
writeFileAtomically(fs, finalFileInRunDir, content.getBytes(StandardCharsets.UTF_8))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Match the atomic run-folder guarantee in the implementation.

The comment describes temp-directory materialization, but the code creates finalRunDir first and then writes a file inside it. Consumers can observe an empty/partial run directory after failures. Either implement temp-dir rename or downgrade the guarantee to file-level atomicity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala`
around lines 121 - 139, The current persistRunFolderAtomically implementation in
HDFSLineageDispatcher only makes the lineage file write atomic, not the whole
run-folder materialization, because it creates finalRunDir before writing the
file. Update persistRunFolderAtomically to follow the documented temp-directory
flow by writing into a hidden temp dir and renaming it to the final run
directory, or else adjust the method/comment to explicitly promise only
file-level atomicity. Ensure the behavior matches the guarantee implied by
writeFileAtomically and pathStringToFsWithPath.

Comment on lines +157 to +162
try {
out.write(bytes)
// Best-effort durability hints; on HDFS these are meaningful.
out.hflush()
out.hsync()
} finally {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the Hadoop/Spark versions used by this repo before confirming FSDataOutputStream sync behavior.
rg -n '<hadoop.version>|hadoop-common|hadoop-client|spark-core' pom.xml */pom.xml

Repository: AbsaOSS/spline-spark-agent

Length of output: 1612


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala"

echo "== file context =="
sed -n '120,190p' "$FILE"

echo
echo "== hflush/hsync usages =="
rg -n 'hflush|hsync|supports[A-Za-z]+|hasPathCapability|isInstanceOf|FileSystem' core/src/main/scala

echo
echo "== hadoop-related dependency context =="
sed -n '1,120p' pom.xml
echo
sed -n '1,110p' core/pom.xml
echo
for f in bundle-2.2/pom.xml bundle-2.3/pom.xml bundle-2.4/pom.xml bundle-3.0/pom.xml bundle-3.1/pom.xml bundle-3.3/pom.xml bundle-3.4/pom.xml bundle-3.5/pom.xml; do
  if [ -f "$f" ]; then
    echo
    echo "### $f"
    rg -n 'hadoop-client|hadoop-common|spark-core|hadoop.version|scala.binary.version' "$f"
  fi
done

Repository: AbsaOSS/spline-spark-agent

Length of output: 16242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check Hadoop API docs available in repository-local sources if any mention these methods.
rg -n 'def hflush|def hsync|hflush\(|hsync\(' .

Repository: AbsaOSS/spline-spark-agent

Length of output: 387


🌐 Web query:

Hadoop FSDataOutputStream hflush hsync unsupported filesystem implementation IOException documentation

💡 Result:

In Hadoop, the behavior of FSDataOutputStream.hflush and hsync on filesystems that do not support these operations is governed by the implementation of the Syncable interface [1][2]. Historically, and in some current implementations, FSDataOutputStream silently downgrades hflush and hsync calls to a standard OutputStream.flush if the underlying stream does not implement Syncable [3][2]. This behavior is discouraged because it can mislead applications into assuming data is visible or durable when it is not [2]. Modern guidance and best practices for filesystem implementers specify that if a filesystem does not support sync/flush operations, it should implement the Syncable interface but throw an UnsupportedOperationException instead of performing a silent downgrade [4][2][5]. Some modern connectors (such as certain S3A implementations) have moved toward throwing UnsupportedOperationException to ensure callers are explicitly aware that the operation is not supported [5]. To verify support for these operations programmatically before invoking them, developers should use the StreamCapabilities interface [4][6][2]. By calling hasCapability("hflush") or hasCapability("hsync"), an application can safely check if the stream supports these guarantees without risking an unexpected exception or a misleading silent failure [4][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala"

echo "== file context =="
sed -n '120,190p' "$FILE"

echo
echo "== hflush/hsync usages =="
rg -n 'hflush|hsync|supports[A-Za-z]+|hasPathCapability|isInstanceOf|FileSystem' core/src/main/scalasrc/main/scala 2>/dev/null || true
rg -n 'hflush|hsync|supports[A-Za-z]+|hasPathCapability|isInstanceOf|FileSystem' core/src/main/scala

echo
echo "== hadoop-related dependency context =="
sed -n '1,120p' pom.xml
echo
sed -n '1,110p' core/pom.xml
echo
for f in bundle-2.2/pom.xml bundle-2.3/pom.xml bundle-2.4/pom.xml bundle-3.0/pom.xml bundle-3.1/pom.xml bundle-3.3/pom.xml bundle-3.4/pom.xml bundle-3.5/pom.xml; do
  if [ -f "$f" ]; then
    echo
    echo "### $f"
    rg -n 'hadoop-client|hadoop-common|spark-core|hadoop.version|scala.binary.version' "$f"
  fi
done

Repository: AbsaOSS/spline-spark-agent

Length of output: 16242


Make hflush/hsync truly best-effort in writeFileAtomically
out.hflush() / out.hsync() are invoked on every FileSystem implementation here, but some supported connectors can reject those calls and abort the write. Guard them by capability (or catch UnsupportedOperationException) and still close + rename the temp file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala`
around lines 157 - 162, The durability hints in writeFileAtomically are being
applied unconditionally, which can break writes on FileSystem implementations
that do not support them. Update the try block in
HDFSLineageDispatcher.writeFileAtomically to treat out.hflush() and out.hsync()
as best-effort by either checking the filesystem capability first or catching
UnsupportedOperationException around those calls, then continue with the
existing close and temp-file rename flow regardless.

Comment on lines +190 to +194
} else {
"unknown"
}
val provider = Option(tableProps.get("provider"))
SourceIdentifier(provider, uri)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how SourceIdentifier URIs participate in source-node identity.
rg -n -C3 'SourceIdentifier|uris|format' core commons

Repository: AbsaOSS/spline-spark-agent

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the V2 plugin logic around the fallback URI.
ast-grep outline core/src/main/scala/za/co/absa/spline/harvester/plugin/embedded/DataSourceV2Plugin.scala --view expanded
sed -n '150,230p' core/src/main/scala/za/co/absa/spline/harvester/plugin/embedded/DataSourceV2Plugin.scala

# Inspect SourceIdentifier and its consumers to see how URIs affect identity/building.
sed -n '1,120p' core/src/main/scala/za/co/absa/spline/harvester/builder/SourceIdentifier.scala
sed -n '1,140p' core/src/main/scala/za/co/absa/spline/harvester/builder/read/PluggableReadCommandExtractor.scala
sed -n '1,140p' core/src/main/scala/za/co/absa/spline/harvester/builder/write/PluggableWriteCommandExtractor.scala
sed -n '1,120p' core/src/main/scala/za/co/absa/spline/harvester/builder/plan/read/ReadNodeBuilder.scala
sed -n '1,120p' core/src/main/scala/za/co/absa/spline/harvester/builder/plan/write/WriteNodeBuilder.scala

# Search for places where SourceIdentifier or its URIs are used as keys or deduped.
rg -n "SourceIdentifier\\(|sourceIdentifier\\.|\\.uris\\b|inputSources|outputSource|distinct\\(|groupBy\\(|toSet\\b|Map\\(" core/src/main/scala core/src/test/scala

Repository: AbsaOSS/spline-spark-agent

Length of output: 50382


Avoid the literal "unknown" fallback. Distinct V2 tables that share a provider and lack both path and properties.location will all emit SourceIdentifier(provider, "unknown"), making them indistinguishable in lineage payloads. Prefer a table-specific fallback or omit the URI when it isn’t available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/scala/za/co/absa/spline/harvester/plugin/embedded/DataSourceV2Plugin.scala`
around lines 190 - 194, The fallback URI in DataSourceV2Plugin’s
SourceIdentifier construction currently hardcodes "unknown", which causes
different V2 tables to collide when path and properties.location are missing.
Update the URI derivation logic in the table scanning/identifier-building flow
so it uses a table-specific fallback or leaves the URI absent instead of
defaulting to the literal string, while keeping the provider lookup and
SourceIdentifier creation intact.

Comment on lines +31 to +82
public static void main(String[] args) {



final SparkSession session = SparkSession.builder()
.appName("java example app")
.master("local[*]")
.config("spark.driver.host", "localhost")
.config("spark.spline.mode", "ENABLED") // ✅ Enable Spline
.config("spark.spline.lineageDispatcher", "hdfs") // ✅ Use HDFS dispatcher
.config("spark.spline.lineageDispatcher.hdfs.className", "za.co.absa.spline.harvester.dispatcher.HDFSLineageDispatcher") // 🔥 Fix: Define HDFS Dispatcher
.config("spark.spline.lineageDispatcher.hdfs.directory", "file:///C:/spline") // ✅ Store lineage files in local storage
.config("spark.spline.lineageDispatcher.hdfs.fileNamePrefix", "lineage_") // ✅ File naming pattern
.getOrCreate();



// Enable Spline Tracking
AgentConfig splineConfig = AgentConfig.builder().build();
SparkLineageInitializer.enableLineageTracking(session, splineConfig);
System.out.println("Spline Listeners: " + splineConfig.getConfigurationListeners().size());

System.out.println("Using HDFSLineageDispatcher from: " +
HDFSLineageDispatcher.class.getProtectionDomain().getCodeSource().getLocation());






System.out.println("Spline Mode: " + session.conf().get("spark.spline.mode", "NOT SET"));
System.out.println("Spline HDFS: " + session.conf().get("spark.spline.lineageDispatcher.hdfs.directory", "NOT SET"));


// Explicitly enable Spline lineage tracking
// This step is optional - see https://github.com/AbsaOSS/spline-spark-agent#programmatic-initialization



// Sample Spark Job
session.read()
.option("header", "true")
.option("inferSchema", "true")
.csv("data/input/batch/wikidata.csv")
.as("source")
.write()
.mode(SaveMode.Overwrite)
.csv("data/output/batch/java-sample3.csv");

// printLineageFiles("/tmp/spline");

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stop the Spark session in a finally block.

The example creates a local Spark session but never releases it, which can leave local resources and file handles open after failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/src/main/java/za/co/absa/spline/example/batch/JavaExampleJob_Files.java`
around lines 31 - 82, The main method in JavaExampleJob_Files creates a
SparkSession but does not ensure it is closed, leaving resources open on failure
or exit. Update the SparkSession session lifecycle in main so the job uses a
try/finally (or equivalent cleanup) around the Spark work and always calls
session.stop() in the cleanup path, keeping the existing
SparkLineageInitializer.enableLineageTracking and SparkSession.builder setup
intact.

.config("spark.spline.mode", "ENABLED") // ✅ Enable Spline
.config("spark.spline.lineageDispatcher", "hdfs") // ✅ Use HDFS dispatcher
.config("spark.spline.lineageDispatcher.hdfs.className", "za.co.absa.spline.harvester.dispatcher.HDFSLineageDispatcher") // 🔥 Fix: Define HDFS Dispatcher
.config("spark.spline.lineageDispatcher.hdfs.directory", "file:///C:/spline") // ✅ Store lineage files in local storage

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a portable local lineage directory.

file:///C:/spline is Windows-specific and maps poorly on Linux/macOS. Build the URI from java.io.tmpdir or accept it as an argument.

Proposed fix
+        final String lineageDir = new File(System.getProperty("java.io.tmpdir"), "spline").toURI().toString();
         final SparkSession session = SparkSession.builder()
@@
-            .config("spark.spline.lineageDispatcher.hdfs.directory", "file:///C:/spline")  // ✅ Store lineage files in local storage
+            .config("spark.spline.lineageDispatcher.hdfs.directory", lineageDir)  // ✅ Store lineage files in local storage
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.config("spark.spline.lineageDispatcher.hdfs.directory", "file:///C:/spline") // ✅ Store lineage files in local storage
final String lineageDir = new File(System.getProperty("java.io.tmpdir"), "spline").toURI().toString();
.config("spark.spline.lineageDispatcher.hdfs.directory", lineageDir) // ✅ Store lineage files in local storage
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/src/main/java/za/co/absa/spline/example/batch/JavaExampleJob_Files.java`
at line 42, The lineage directory in the Java example is hardcoded to a
Windows-only path, so update JavaExampleJob_Files to build the dispatcher URI
from a portable source such as java.io.tmpdir or a configurable argument instead
of using the fixed file:///C:/spline value. Keep the change localized to the
Spark config setup in the example job so the lineageDispatcher setting works
across Windows, Linux, and macOS.

Comment thread pom.xml
Comment on lines +147 to +148
<scala.version>2.11.12</scala.version>
<scala.binary.version>2.11</scala.binary.version>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== root cross-build properties =="
sed -n '138,178p' pom.xml

echo
echo "== scala-binary-dependent Spark/Delta coordinates in reviewed POMs =="
rg -n 'scala\.binary\.version|spark-(30|31|32|33|34|35)\.version|delta-(30|31|32|33|34|35)\.version' \
  pom.xml commons/pom.xml core/pom.xml examples/pom.xml integration-tests/pom.xml bundle-2.2/pom.xml bundle-2.3/pom.xml bundle-2.4/pom.xml

Repository: AbsaOSS/spline-spark-agent

Length of output: 12539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pom.xml around Scala/Spark profile matrix =="
sed -n '620,910p' pom.xml

echo
echo "== plugin and cross-build property references =="
rg -n 'defaultScalaBinaryVersion|defaultScalaVersion|scala-cross-build|scala\.binary\.version|scala\.version' pom.xml

Repository: AbsaOSS/spline-spark-agent

Length of output: 16675


🌐 Web query:

scala-cross-build-maven-plugin defaultScalaBinaryVersion defaultScalaVersion semantics

💡 Result:

The scala-cross-build-maven-plugin is a utility designed to assist with Scala cross-version builds in Maven projects [1][2]. Regarding the specific properties defaultScalaBinaryVersion and defaultScalaVersion, these are not core semantic configuration parameters of the plugin itself, but rather common naming conventions used by developers within their Maven POM files to manage cross-compilation profiles [3][4][5]. In the context of cross-building, the following semantics typically apply: 1. Property Semantics: - defaultScalaBinaryVersion: Usually defines the Scala binary version (e.g., 2.12, 2.13) that the project defaults to when no specific profile is activated [3][5]. - defaultScalaVersion: Often maps to the specific full Scala version (e.g., 2.12.18) corresponding to the default binary version [3][5]. 2. Plugin Usage: The scala-cross-build-maven-plugin itself focuses on goals such as changing the Scala version in your pom.xml (via change-version) and restoring it (via restore-version) [1][2]. It relies on Maven profiles to handle the actual switching of dependencies and plugin configurations [1][3]. 3. Best Practices: - Developers typically define these defaults in the properties section of their pom.xml so that IDEs and standard builds have a stable target [3]. - When building for other Scala versions, developers use Maven profiles to override these default properties [3][4]. - While some alternative plugins (like the scala-maven-plugin) have built-in properties like scala.version and scala.compat.version [6], the scala-cross-build-maven-plugin is generally used as a helper to modify the project structure dynamically to achieve similar results [1][2]. If you are implementing this, ensure your POM correctly uses Maven property interpolation to reference these variables in your dependency and plugin sections [3].

Citations:


Align the cross-build defaults with the 2.11 switch. default.scala.binary.version / default.scala.version still point at 2.12 while the root build is now 2.11, so scala-cross-build-maven-plugin and the checked-in coordinates disagree. Update the defaults to the 2.11 values too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pom.xml` around lines 147 - 148, The root build has been switched to Scala
2.11, but the default cross-build properties still point to 2.12, so update the
default Scala version and binary version properties in pom.xml to match the
existing scala.version and scala.binary.version values; keep the change aligned
with the cross-build configuration used by scala-cross-build-maven-plugin so the
default coordinates and build defaults agree.

Catch and log Spline listener, dispatch, and cleanup failures so unexpected lineage issues do not fail Spark job execution.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/main/scala/za/co/absa/spline/agent/SplineAgent.scala (1)

90-100: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Gate event dispatch on a successful plan send core/src/main/scala/za/co/absa/spline/agent/SplineAgent.scala:94-98

The separate catches still allow an ExecutionEvent to be dispatched after the plan send fails. For HTTP/Kafka backends, that can persist an event without its plan and leave a dangling lineage record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/main/scala/za/co/absa/spline/agent/SplineAgent.scala` around lines
90 - 100, The plan and event dispatch in SplineAgent’s harvest callback are
still independent, so an ExecutionEvent can be sent even when the corresponding
plan send fails. Update the logic around harvester.harvest(...).foreach and the
two withErrorHandling blocks so the event dispatch only runs after
lineageDispatcher.send(plan) succeeds, using the existing execution plan
dispatch and execution event dispatch symbols to keep the send order explicit
and gated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala`:
- Around line 46-61: `CompositeLineageDispatcher.delegate` only fails fast when
a child dispatcher throws, but `HttpLineageDispatcher`,
`KafkaLineageDispatcher`, and `FallbackLineageDispatcher` currently swallow
`NonFatal` errors and only log them. Update those dispatcher implementations so
their dispatch methods propagate failures back to `delegate` when `failOnErrors`
should apply, and keep the handling centralized around the
`delegate`/`failOnErrors` flow rather than hiding exceptions inside the child
dispatchers.

---

Outside diff comments:
In `@core/src/main/scala/za/co/absa/spline/agent/SplineAgent.scala`:
- Around line 90-100: The plan and event dispatch in SplineAgent’s harvest
callback are still independent, so an ExecutionEvent can be sent even when the
corresponding plan send fails. Update the logic around
harvester.harvest(...).foreach and the two withErrorHandling blocks so the event
dispatch only runs after lineageDispatcher.send(plan) succeeds, using the
existing execution plan dispatch and execution event dispatch symbols to keep
the send order explicit and gated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65d5eed9-387e-454a-92f4-b9b6f3e94eea

📥 Commits

Reviewing files that changed from the base of the PR and between ade070b and d499ab7.

📒 Files selected for processing (7)
  • core/src/main/scala/za/co/absa/spline/agent/SplineAgent.scala
  • core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala
  • core/src/main/scala/za/co/absa/spline/harvester/dispatcher/FallbackLineageDispatcher.scala
  • core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala
  • core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HttpLineageDispatcher.scala
  • core/src/main/scala/za/co/absa/spline/harvester/dispatcher/KafkaLineageDispatcher.scala
  • core/src/main/scala/za/co/absa/spline/harvester/listener/SplineQueryExecutionListener.scala
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HDFSLineageDispatcher.scala

Comment on lines 46 to 61
private def delegate(call: LineageDispatcher => Unit): Unit = {
delegatees.foreach(withErrorHandling(call))
}
val failures = delegatees.flatMap { disp =>
try {
call(disp)
None
} catch {
case NonFatal(e) =>
logWarning(s"Proceeding after an error occurred in an underlying dispatcher: ${disp.getClass.getName}", e)
Some(e)
}
}

private def withErrorHandling(call: LineageDispatcher => Unit): LineageDispatcher => Unit = { disp =>
try call(disp)
catch {
case NonFatal(e) =>
if (failOnErrors) throw e
else logWarning(s"Proceeding after an error occurred in an underlying dispatcher: ${disp.getClass.getName}", e)
if (failOnErrors) {
failures.headOption.foreach(e => throw e)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant files
git ls-files core/src/main/scala/za/co/absa/spline/harvester/dispatcher | sed -n '1,200p'

echo '--- CompositeLineageDispatcher.scala ---'
wc -l core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala
cat -n core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala | sed -n '1,220p'

echo '--- dispatcher implementations ---'
for f in \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HttpLineageDispatcher.scala \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/KafkaLineageDispatcher.scala \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/FallbackLineageDispatcher.scala \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/LineageDispatcher.scala
do
  if [ -f "$f" ]; then
    echo "### $f"
    wc -l "$f"
    cat -n "$f" | sed -n '1,240p'
  fi
done

Repository: AbsaOSS/spline-spark-agent

Length of output: 26323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all references to failOnErrors and dispatcher send implementations
rg -n "failOnErrors|FailOnErrorsKey|def send\\(|override def send|catch \\{[[:space:]]*case NonFatal|NonFatal\\(" core/src/main/scala/za/co/absa/spline/harvester/dispatcher core/src/main/scala/za/co/absa/spline/harvester | sed -n '1,240p'

Repository: AbsaOSS/spline-spark-agent

Length of output: 14139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect config/docs around failOnErrors semantics
rg -n "failOnErrors|FailOnErrorsKey|fail on errors|failOnErrors" core src docs . | sed -n '1,240p'

Repository: AbsaOSS/spline-spark-agent

Length of output: 4066


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Determine whether delegatees can propagate failures through return values instead of exceptions
ast-grep outline core/src/main/scala/za/co/absa/spline/harvester/dispatcher/LineageDispatcher.scala --view expanded || true
ast-grep outline core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala --view expanded || true

Repository: AbsaOSS/spline-spark-agent

Length of output: 367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Quick semantic probe: see if any underlying dispatchers return any value indicating failure
python3 - <<'PY'
from pathlib import Path
files = [
    Path("core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HttpLineageDispatcher.scala"),
    Path("core/src/main/scala/za/co/absa/spline/harvester/dispatcher/KafkaLineageDispatcher.scala"),
    Path("core/src/main/scala/za/co/absa/spline/harvester/dispatcher/FallbackLineageDispatcher.scala"),
]
for p in files:
    if p.exists():
        print(f"### {p}")
        text = p.read_text()
        for i, line in enumerate(text.splitlines(), 1):
            if "def send" in line or "override def send" in line or "NonFatal" in line or "Try(" in line or "Either" in line:
                print(f"{i}: {line}")
PY

Repository: AbsaOSS/spline-spark-agent

Length of output: 1581


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/HttpLineageDispatcher.scala \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/KafkaLineageDispatcher.scala \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/FallbackLineageDispatcher.scala \
  core/src/main/scala/za/co/absa/spline/harvester/dispatcher/LineageDispatcher.scala
do
  if [ -f "$f" ]; then
    echo "### $f"
    cat -n "$f" | sed -n '1,220p' | sed -n '/def send\|override def send\|delegate(call: LineageDispatcher => Unit)\|NonFatal\|failOnErrors/p'
    echo
  fi
done

Repository: AbsaOSS/spline-spark-agent

Length of output: 2369


failOnErrors never sees failures from the HTTP/Kafka/Fallback dispatchers

CompositeLineageDispatcher can only fail fast when a child dispatcher throws, but HttpLineageDispatcher, KafkaLineageDispatcher, and FallbackLineageDispatcher catch NonFatal internally and only log. That makes failOnErrors = true ineffective for those implementations. If this setting is supposed to surface dispatch errors, these dispatchers need to propagate failure instead of swallowing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/scala/za/co/absa/spline/harvester/dispatcher/CompositeLineageDispatcher.scala`
around lines 46 - 61, `CompositeLineageDispatcher.delegate` only fails fast when
a child dispatcher throws, but `HttpLineageDispatcher`,
`KafkaLineageDispatcher`, and `FallbackLineageDispatcher` currently swallow
`NonFatal` errors and only log them. Update those dispatcher implementations so
their dispatch methods propagate failures back to `delegate` when `failOnErrors`
should apply, and keep the handling centralized around the
`delegate`/`failOnErrors` flow rather than hiding exceptions inside the child
dispatchers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

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.

4 participants