Skip to content

HBASE-30187 Port hbase-spark bulk load functionality to Spark 4 - #163

Merged
wchevreuil merged 2 commits into
apache:masterfrom
Sigma-Ma:HBASE-30187-port-hbase-spark-bulk-load-to-spark4
Sep 11, 2026
Merged

wchevreuil merged 2 commits into
apache:masterfrom
Sigma-Ma:HBASE-30187-port-hbase-spark-bulk-load-to-spark4

Conversation

@Sigma-Ma

Copy link
Copy Markdown
Contributor

JIRA: https://issues.apache.org/jira/browse/HBASE-30187

What changes were proposed in this pull request?

Port the Spark bulk-load APIs from the Spark 3 connector to the Spark 4 module. This adds bulkLoad, bulkLoadThinRows, the related RDD extension methods, and the helper classes used to partition rows and write HFiles. It also ports the existing bulk-load tests to Scala 2.13 and Spark 4.

The Spark 4 sources are formatted with the project Spotless configuration.

Why are the changes needed?

The Spark 4 connector does not currently provide the HFile bulk-load functionality available in the Spark 3 connector. This change provides the same wide-row and thin-row bulk-load support for Spark 4 users.

How was this patch tested?

mvn -Pspark4 -pl :hbase-spark4 -am spotless:check

mvn -Pspark4 -pl :hbase-spark4 -am \
  -Dsuites=org.apache.hadoop.hbase.spark.BulkLoadSuite package

mvn -Pspark4 -pl :hbase-spark4 -am clean package

@wchevreuil
wchevreuil requested a lite review from Copilot September 9, 2026 09:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Ports HBase Spark bulk-load APIs and supporting utilities from the Spark 3 connector into the Spark 4 module, including Scala 2.13/Spark 4 test coverage.

Changes:

  • Added Spark 4 bulk-load APIs (bulkLoad, bulkLoadThinRows) and RDD extension methods.
  • Added supporting helper classes for partitioning/sorting and HFile writing.
  • Ported bulk-load test suite(s) to Spark 4 / Scala 2.13 and reformatted Spark 4 sources.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala Minor formatting updates in existing tests.
spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseConnectionCacheSuite.scala Formatting updates (imports/indentation) in existing tests.
spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/BulkLoadSuite.scala Added Spark 4 bulk-load test suite covering wide/thin row bulk load behaviors.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseRDDFunctions.scala Added RDD extension methods for bulk-load APIs.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala Implemented bulk-load and thin-row bulk-load execution + HFile writing helpers.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/BulkLoadPartitioner.scala Added region-split-aware Spark partitioner used by bulk-load shuffles.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/KeyFamilyQualifier.scala Added sortable key type for wide-row bulk-load shuffles.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/ByteArrayWrapper.scala Added byte-array wrapper to support map keys / ordering.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/FamiliesQualifiersValues.scala Added container for thin-row in-memory family/qualifier/value sorting.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/FamilyHFileWriteOptions.scala Added per-family HFile writer options model.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +39 to +43
val comparator: Comparator[Array[Byte]] = new Comparator[Array[Byte]] {
override def compare(o1: Array[Byte], o2: Array[Byte]): Int = {
Bytes.compareTo(o1, o2)
}
}

@wchevreuil wchevreuil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Apologies for the delayed review, @Sigma-Ma . Overall, looks good. Can you please address the review comments? I have resolved copilot ones I thought were not relevant.

Comment on lines +154 to +160
def bulkDelete[T](
rdd: RDD[T],
tableName: TableName,
f: (T) => Delete,
batchSize: Integer): Unit = {
bulkMutation(rdd, tableName, f, batchSize)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we revert these non-logic changes?

Comment on lines +330 to +336
iterator.foreach {
t =>
mutationList.add(f(t))
if (mutationList.size >= batchSize) {
table.batch(mutationList, null)
mutationList.clear()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we revert these non-logic changes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. I reverted the unrelated formatting changes and restored the formatting-only test changes to match master.

@Sigma-Ma

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I addressed the remaining comments:

  • reverted the unrelated formatting changes;
  • reused and closed Connection, Admin, Table, and RegionLocator in BulkLoadSuite;
  • fixed the same connection leak pattern in the other bulk-load tests;
  • replaced KeyAlreadyExistsException with IllegalArgumentException and corrected the message;
  • reused the partition comparator and fixed the noted typos and style issues.

I left the staging-directory test and Partitioner equals/hashCode unchanged, as they are not needed for this port.

@wchevreuil

Copy link
Copy Markdown
Contributor

Thanks for the review. I addressed the remaining comments:

  • reverted the unrelated formatting changes;
  • reused and closed Connection, Admin, Table, and RegionLocator in BulkLoadSuite;
  • fixed the same connection leak pattern in the other bulk-load tests;
  • replaced KeyAlreadyExistsException with IllegalArgumentException and corrected the message;
  • reused the partition comparator and fixed the noted typos and style issues.

I left the staging-directory test and Partitioner equals/hashCode unchanged, as they are not needed for this port.

Can you push these changes? I don't see any commit in the PR branch.

@Sigma-Ma

Copy link
Copy Markdown
Contributor Author

Can you push these changes? I don't see any commit in the PR branch.

Apologies, I forgot to push the changes due to work commitments and only left the comment. The changes have now been pushed. Thanks for the reminder!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

One or more issues must be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

Previously missed (3) — in code that hasn't changed since the last review.

spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala:427

  • getRegionLocator returns a closeable locator, but this method only reads its start keys and never closes it. Since conn.close() only releases the cached connection reference, repeated bulk-load calls can retain locator resources; close the locator in a finally after obtaining the keys, and apply the same fix to bulkLoadThinRows.

This issue also appears on line 557 of the same file.
spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala:503

  • This final rollWriters call only runs after the partition iterator completes normally. If HFile creation/append, filesystem I/O, or the user flatMap throws, the open writers and underscore-prefixed partial files are left behind; Spark retries can accumulate them, and the method's existing-path check then prevents reusing the staging directory. Add failure cleanup that closes/removes unfinished files without renaming them, then rethrow the original error.
    spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala:789
  • A new RegionLocator is created for every executor partition/family here and is never closed. With many partitions or families this accumulates locator resources on the shared HBase connection; close it in a nested finally after getRegionLocation (or create one locator per partition and close it after the partition).

spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala:558

  • The thin-row implementation has the same unclosed RegionLocator problem here: every bulk-load call acquires one to read the region start keys, but only decrements the cached connection reference. Close this locator after getStartKeys so repeated calls do not accumulate client resources.
      val regionLocator = conn.getRegionLocator(tableName)
      val startKeys = regionLocator.getStartKeys

spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala:670

  • The thin-row callback has the same failure path: any exception, including the intentional duplicate-row validation error, skips this final close. That leaves open writers and partial underscore files in the staging directory, so retries can leak resources and cannot reuse the path; add exception cleanup that closes/removes unfinished files without renaming them.
            // This will get a writer for the column family
            // If there is no writer for a given column family then
            // it will get created here.
            // We have finished all the data so lets close up the writers
            rollWriters(fs, writerMap, regionSplitPartitioner, previousRow, compactionExclude)
            rollOverRequested = false
          })
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

@wchevreuil
wchevreuil merged commit 9c73e64 into apache:master Sep 11, 2026
1 check passed
@Sigma-Ma
Sigma-Ma deleted the HBASE-30187-port-hbase-spark-bulk-load-to-spark4 branch September 13, 2026 11:12
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