This is an automated email from the ASF dual-hosted git repository.

taklwu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase-connectors.git


The following commit(s) were added to refs/heads/master by this push:
     new bca1b51  HBASE-30186 Port basic HBase operations to spark4 
HBaseContext (#158)
bca1b51 is described below

commit bca1b512b2eb43db36dfa31695bcafc006598da9
Author: Wellington Ramos Chevreuil <[email protected]>
AuthorDate: Wed Jul 29 22:02:25 2026 +0100

    HBASE-30186 Port basic HBase operations to spark4 HBaseContext (#158)
    
    Signed-off-by: Tak Lon (Stephen) Wu <[email protected]>
---
 .../hadoop/hbase/spark/HBaseConnectionCache.scala  | 270 +++++++++++++++++++++
 .../apache/hadoop/hbase/spark/HBaseContext.scala   | 223 ++++++++++++++++-
 .../hadoop/hbase/spark/HBaseRDDFunctions.scala     | 173 +++++++++++++
 .../hbase/spark/HBaseConnectionCacheSuite.scala    | 247 +++++++++++++++++++
 .../hadoop/hbase/spark/HBaseContextSuite.scala     | 263 +++++++++++++++++++-
 5 files changed, 1171 insertions(+), 5 deletions(-)

diff --git 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseConnectionCache.scala
 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseConnectionCache.scala
new file mode 100644
index 0000000..70b4c62
--- /dev/null
+++ 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseConnectionCache.scala
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.spark
+
+import java.io.IOException
+import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.hbase.HConstants
+import org.apache.hadoop.hbase.TableName
+import org.apache.hadoop.hbase.client.Admin
+import org.apache.hadoop.hbase.client.Connection
+import org.apache.hadoop.hbase.client.ConnectionFactory
+import org.apache.hadoop.hbase.client.RegionLocator
+import org.apache.hadoop.hbase.client.Table
+import org.apache.hadoop.hbase.ipc.RpcControllerFactory
+import org.apache.hadoop.hbase.security.User
+import org.apache.hadoop.hbase.security.UserProvider
+import org.apache.yetus.audience.InterfaceAudience
+import scala.collection.mutable
+
[email protected]
+private[spark] object HBaseConnectionCache extends Logging {
+
+  val connectionMap = new mutable.HashMap[HBaseConnectionKey, 
SmartConnection]()
+
+  val cacheStat = HBaseConnectionCacheStat(0, 0, 0)
+
+  // in milliseconds
+  private final val DEFAULT_TIME_OUT: Long = 10 * 60 * 1000
+  private var timeout = DEFAULT_TIME_OUT
+  private var closed: Boolean = false
+
+  var housekeepingThread = new Thread(new Runnable {
+    override def run(): Unit = {
+      while (true) {
+        try {
+          Thread.sleep(timeout)
+        } catch {
+          case e: InterruptedException =>
+          // setTimeout() and close() may interrupt the sleep and it's safe
+          // to ignore the exception
+        }
+        if (closed)
+          return
+        performHousekeeping(false)
+      }
+    }
+  })
+  housekeepingThread.setDaemon(true)
+  housekeepingThread.start()
+
+  def getStat: HBaseConnectionCacheStat = {
+    connectionMap.synchronized {
+      cacheStat.numActiveConnections = connectionMap.size
+      cacheStat.copy()
+    }
+  }
+
+  def close(): Unit = {
+    try {
+      connectionMap.synchronized {
+        if (closed)
+          return
+        closed = true
+        housekeepingThread.interrupt()
+        housekeepingThread = null
+        HBaseConnectionCache.performHousekeeping(true)
+      }
+    } catch {
+      case e: Exception => logWarning("Error in finalHouseKeeping", e)
+    }
+  }
+
+  def performHousekeeping(forceClean: Boolean): Unit = {
+    val tsNow: Long = System.currentTimeMillis()
+    connectionMap.synchronized {
+      connectionMap.foreach {
+        x =>
+          {
+            if (x._2.refCount < 0) {
+              logError(s"Bug to be fixed: negative refCount of connection 
${x._2}")
+            }
+
+            if (forceClean || ((x._2.refCount <= 0) && (tsNow - x._2.timestamp 
> timeout))) {
+              try {
+                x._2.connection.close()
+              } catch {
+                case e: IOException => logWarning(s"Fail to close connection 
${x._2}", e)
+              }
+              connectionMap.remove(x._1)
+            }
+          }
+      }
+    }
+  }
+
+  // For testing purpose only
+  def getConnection(key: HBaseConnectionKey, conn: => Connection): 
SmartConnection = {
+    connectionMap.synchronized {
+      if (closed)
+        return null
+      cacheStat.numTotalRequests += 1
+      val sc = connectionMap.getOrElseUpdate(
+        key, {
+          cacheStat.numActualConnectionsCreated += 1
+          new SmartConnection(conn)
+        })
+      sc.refCount += 1
+      sc
+    }
+  }
+
+  def getConnection(conf: Configuration): SmartConnection =
+    getConnection(new HBaseConnectionKey(conf), 
ConnectionFactory.createConnection(conf))
+
+  // For testing purpose only
+  def setTimeout(to: Long): Unit = {
+    connectionMap.synchronized {
+      if (closed)
+        return
+      timeout = to
+      housekeepingThread.interrupt()
+    }
+  }
+}
+
[email protected]
+private[hbase] case class SmartConnection(
+    connection: Connection,
+    var refCount: Int = 0,
+    var timestamp: Long = 0) {
+  def getTable(tableName: TableName): Table = connection.getTable(tableName)
+  def getRegionLocator(tableName: TableName): RegionLocator = 
connection.getRegionLocator(tableName)
+  def isClosed: Boolean = connection.isClosed
+  def getAdmin: Admin = connection.getAdmin
+  def close(): Unit = {
+    HBaseConnectionCache.connectionMap.synchronized {
+      refCount -= 1
+      if (refCount <= 0)
+        timestamp = System.currentTimeMillis()
+    }
+  }
+}
+
+/**
+ * Denotes a unique key to an HBase Connection instance.
+ * Please refer to 'org.apache.hadoop.hbase.client.HConnectionKey'.
+ *
+ * In essence, this class captures the properties in Configuration
+ * that may be used in the process of establishing a connection.
+ */
[email protected]
+class HBaseConnectionKey(c: Configuration) extends Logging {
+  val conf: Configuration = c
+  val CONNECTION_PROPERTIES: Array[String] = Array[String](
+    HConstants.ZOOKEEPER_QUORUM,
+    HConstants.ZOOKEEPER_ZNODE_PARENT,
+    HConstants.ZOOKEEPER_CLIENT_PORT,
+    HConstants.HBASE_CLIENT_PAUSE,
+    HConstants.HBASE_CLIENT_RETRIES_NUMBER,
+    HConstants.HBASE_RPC_TIMEOUT_KEY,
+    HConstants.HBASE_META_SCANNER_CACHING,
+    HConstants.HBASE_CLIENT_INSTANCE_ID,
+    HConstants.RPC_CODEC_CONF_KEY,
+    HConstants.USE_META_REPLICAS,
+    RpcControllerFactory.CUSTOM_CONTROLLER_CONF_KEY)
+
+  var username: String = _
+  var m_properties: mutable.HashMap[String, String] = 
mutable.HashMap.empty[String, String]
+  if (conf != null) {
+    for (property <- CONNECTION_PROPERTIES) {
+      val value: String = conf.get(property)
+      if (value != null) {
+        m_properties.+=((property, value))
+      }
+    }
+    try {
+      val provider: UserProvider = UserProvider.instantiate(conf)
+      val currentUser: User = provider.getCurrent
+      if (currentUser != null) {
+        username = currentUser.getName
+      }
+    } catch {
+      case e: IOException =>
+        logWarning("Error obtaining current user, skipping username in 
HBaseConnectionKey", e)
+    }
+  }
+
+  // make 'properties' immutable
+  val properties: Map[String, String] = m_properties.toMap
+
+  override def hashCode: Int = {
+    val prime: Int = 31
+    var result: Int = 1
+    if (username != null) {
+      result = username.hashCode
+    }
+    for (property <- CONNECTION_PROPERTIES) {
+      val value: Option[String] = properties.get(property)
+      if (value.isDefined) {
+        result = prime * result + value.hashCode
+      }
+    }
+    result
+  }
+
+  override def equals(obj: Any): Boolean = {
+    if (obj == null) return false
+    if (getClass ne obj.getClass) return false
+    val that: HBaseConnectionKey = obj.asInstanceOf[HBaseConnectionKey]
+    if (this.username != null && !(this.username == that.username)) {
+      return false
+    } else if (this.username == null && that.username != null) {
+      return false
+    }
+    if (this.properties == null) {
+      if (that.properties != null) {
+        return false
+      }
+    } else {
+      if (that.properties == null) {
+        return false
+      }
+      var flag: Boolean = true
+      for (property <- CONNECTION_PROPERTIES) {
+        val thisValue: Option[String] = this.properties.get(property)
+        val thatValue: Option[String] = that.properties.get(property)
+        flag = true
+        if (thisValue eq thatValue) {
+          flag = false // continue, so make flag be false
+        }
+        if (flag && (thisValue == null || !(thisValue == thatValue))) {
+          return false
+        }
+      }
+    }
+    true
+  }
+
+  override def toString: String = {
+    "HBaseConnectionKey{" + "properties=" + properties + ", username='" + 
username + '\'' + '}'
+  }
+}
+
+/**
+ * To log the state of 'HBaseConnectionCache'
+ *
+ * @param numTotalRequests number of total connection requests to the cache
+ * @param numActualConnectionsCreated number of actual HBase connections the 
cache ever created
+ * @param numActiveConnections number of current alive HBase connections the 
cache is holding
+ */
[email protected]
+case class HBaseConnectionCacheStat(
+    var numTotalRequests: Long,
+    var numActualConnectionsCreated: Long,
+    var numActiveConnections: Long)
diff --git 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala
 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala
index da66ed6..0a9be4b 100644
--- 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala
+++ 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseContext.scala
@@ -19,7 +19,7 @@ package org.apache.hadoop.hbase.spark
 
 import org.apache.hadoop.conf.Configuration
 import org.apache.hadoop.hbase.{CellUtil, TableName}
-import org.apache.hadoop.hbase.client.{Result, Scan}
+import org.apache.hadoop.hbase.client._
 import org.apache.hadoop.hbase.io.ImmutableBytesWritable
 import org.apache.hadoop.hbase.mapreduce.{IdentityTableMapper, 
TableInputFormat, TableMapReduceUtil}
 import org.apache.hadoop.hbase.util.Bytes
@@ -34,9 +34,10 @@ import org.apache.yetus.audience.InterfaceAudience
 import scala.reflect.ClassTag
 
 /**
- * Narrow Spark 4 port of `org.apache.hadoop.hbase.spark.HBaseContext`: enough 
for datasource scans
- * via `NewHBaseRDD` / TableInputFormat. Bulk load, streaming, and imperative 
helpers are intentionally
- * omitted for now and will be implemented later (see HBASE-30178).
+ * HBaseContext is a facade for HBase operations like bulk put, get, delete, 
and scan.
+ *
+ * HBaseContext will take the responsibilities of disseminating the 
configuration information
+ * to the workers and managing the life cycle of Connections.
  */
 @InterfaceAudience.Public
 class HBaseContext(@transient val sc: SparkContext, @transient val config: 
Configuration)
@@ -55,6 +56,119 @@ class HBaseContext(@transient val sc: SparkContext, 
@transient val config: Confi
 
   LatestHBaseContextCache.latest = this
 
+  /**
+   * A simple enrichment of the traditional Spark RDD foreachPartition.
+   * This function differs from the original in that it offers the
+   * developer access to a already connected Connection object
+   *
+   * Note: Do not close the Connection object.  All Connection
+   * management is handled outside this method
+   *
+   * @param rdd  Original RDD with data to iterate over
+   * @param f    Function to be given a iterator to iterate through
+   *             the RDD values and a Connection object to interact
+   *             with HBase
+   */
+  def foreachPartition[T](rdd: RDD[T], f: (Iterator[T], Connection) => Unit): 
Unit = {
+    rdd.foreachPartition(it => hbaseForeachPartition(broadcastedConf, it, f))
+  }
+
+  /**
+   * A simple enrichment of the traditional Spark RDD mapPartition.
+   * This function differs from the original in that it offers the
+   * developer access to a already connected Connection object
+   *
+   * Note: Do not close the Connection object.  All Connection
+   * management is handled outside this method
+   *
+   * @param rdd  Original RDD with data to iterate over
+   * @param mp   Function to be given a iterator to iterate through
+   *             the RDD values and a Connection object to interact
+   *             with HBase
+   * @return     Returns a new RDD generated by the user definition
+   *             function just like normal mapPartition
+   */
+  def mapPartitions[T, R: ClassTag](
+      rdd: RDD[T],
+      mp: (Iterator[T], Connection) => Iterator[R]): RDD[R] = {
+    rdd.mapPartitions[R](it => hbaseMapPartition[T, R](broadcastedConf, it, 
mp))
+  }
+
+  /**
+   * A simple abstraction over the HBaseContext.foreachPartition method.
+   *
+   * It allow addition support for a user to take RDD
+   * and generate puts and send them to HBase.
+   * The complexity of managing the Connection is
+   * removed from the developer
+   *
+   * @param rdd       Original RDD with data to iterate over
+   * @param tableName The name of the table to put into
+   * @param f         Function to convert a value in the RDD to a HBase Put
+   */
+  def bulkPut[T](rdd: RDD[T], tableName: TableName, f: (T) => Put): Unit = {
+    val tName = tableName.getName
+    rdd.foreachPartition(
+      it =>
+        hbaseForeachPartition[T](
+          broadcastedConf,
+          it,
+          (iterator, connection) => {
+            val m = connection.getBufferedMutator(TableName.valueOf(tName))
+            try {
+              iterator.foreach(t => m.mutate(f(t)))
+              m.flush()
+            } finally {
+              m.close()
+            }
+          }))
+  }
+
+  /**
+   * A simple abstraction over the HBaseContext.foreachPartition method.
+   *
+   * It allow addition support for a user to take a RDD and generate delete
+   * and send them to HBase.  The complexity of managing the Connection is
+   * removed from the developer
+   *
+   * @param rdd       Original RDD with data to iterate over
+   * @param tableName The name of the table to delete from
+   * @param f         Function to convert a value in the RDD to a
+   *                  HBase Deletes
+   * @param batchSize       The number of delete to batch before sending to 
HBase
+   */
+  def bulkDelete[T](rdd: RDD[T], tableName: TableName, f: (T) => Delete, 
batchSize: Integer): Unit =
+    {
+      bulkMutation(rdd, tableName, f, batchSize)
+    }
+
+  /**
+   * A simple abstraction over the HBaseContext.mapPartition method.
+   *
+   * It allow addition support for a user to take a RDD and generates a
+   * new RDD based on Gets and the results they bring back from HBase
+   *
+   * @param tableName        The name of the table to get from
+   * @param batchSize        How many gets to execute in a single batch
+   * @param rdd              Original RDD with data to iterate over
+   * @param makeGet          function to convert a value in the RDD to a
+   *                         HBase Get
+   * @param convertResult    This will convert the HBase Result object to
+   *                         what ever the user wants to put in the resulting
+   *                         RDD
+   * @return                 new RDD with results from Get
+   */
+  def bulkGet[T, U: ClassTag](
+      tableName: TableName,
+      batchSize: Integer,
+      rdd: RDD[T],
+      makeGet: (T) => Get,
+      convertResult: (Result) => U): RDD[U] = {
+
+    val getMapPartition = new GetMapPartition(tableName, batchSize, makeGet, 
convertResult)
+    rdd.mapPartitions[U](it => hbaseMapPartition[T, U](broadcastedConf, it, 
getMapPartition.run))
+  }
+
   /**
    * Produces an RDD of type U by scanning an HBase table and applying a 
transformation function
    * to each (key, result) pair.
@@ -149,6 +263,107 @@ class HBaseContext(@transient val sc: SparkContext, 
@transient val config: Confi
     }
   }
 
+  private def hbaseForeachPartition[T](
+      configBroadcast: Broadcast[SerializableWritable[Configuration]],
+      it: Iterator[T],
+      f: (Iterator[T], Connection) => Unit): Unit = {
+
+    val config = getConf(configBroadcast)
+    val smartConn = HBaseConnectionCache.getConnection(config)
+    if (smartConn == null) {
+      throw new IllegalStateException("HBaseConnectionCache is closed")
+    }
+    try {
+      f(it, smartConn.connection)
+    } finally {
+      smartConn.close()
+    }
+  }
+
+  private def hbaseMapPartition[K, U](
+      configBroadcast: Broadcast[SerializableWritable[Configuration]],
+      it: Iterator[K],
+      mp: (Iterator[K], Connection) => Iterator[U]): Iterator[U] = {
+
+    val config = getConf(configBroadcast)
+    val smartConn = HBaseConnectionCache.getConnection(config)
+    try {
+      mp(it, smartConn.connection)
+    } finally {
+      if (smartConn != null) smartConn.close()
+    }
+  }
+
+  private def bulkMutation[T](
+      rdd: RDD[T],
+      tableName: TableName,
+      f: (T) => Mutation,
+      batchSize: Integer): Unit = {
+
+    val tName = tableName.getName
+    rdd.foreachPartition(
+      it =>
+        hbaseForeachPartition[T](
+          broadcastedConf,
+          it,
+          (iterator, connection) => {
+            val table = connection.getTable(TableName.valueOf(tName))
+            try {
+              val mutationList = new java.util.ArrayList[Mutation]()
+              iterator.foreach { t =>
+                mutationList.add(f(t))
+                if (mutationList.size >= batchSize) {
+                  table.batch(mutationList, null)
+                  mutationList.clear()
+                }
+              }
+              if (mutationList.size() > 0) {
+                table.batch(mutationList, null)
+                mutationList.clear()
+              }
+            } finally {
+              table.close()
+            }
+          }))
+  }
+
+  private class GetMapPartition[T, U: ClassTag](
+      tableName: TableName,
+      batchSize: Integer,
+      makeGet: (T) => Get,
+      convertResult: (Result) => U)
+      extends Serializable {
+
+    val tName = tableName.getName
+
+    def run(iterator: Iterator[T], connection: Connection): Iterator[U] = {
+      val table = connection.getTable(TableName.valueOf(tName))
+      try {
+        val gets = new java.util.ArrayList[Get]()
+        val res = new scala.collection.mutable.ArrayBuffer[U]()
+
+        while (iterator.hasNext) {
+          gets.add(makeGet(iterator.next()))
+          if (gets.size() == batchSize) {
+            val results = table.get(gets)
+            res ++= results.iterator.map(convertResult)
+            gets.clear()
+          }
+        }
+
+        if (gets.size() > 0) {
+          val results = table.get(gets)
+          res ++= results.iterator.map(convertResult)
+          gets.clear()
+        }
+
+        res.iterator
+      } finally {
+        table.close()
+      }
+    }
+  }
+
   private[hbase] def getConf(
       configBroadcast: Broadcast[SerializableWritable[Configuration]]): 
Configuration = {
 
diff --git 
a/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseRDDFunctions.scala
 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseRDDFunctions.scala
new file mode 100644
index 0000000..c0093ea
--- /dev/null
+++ 
b/spark4/hbase-spark4/src/main/scala/org/apache/hadoop/hbase/spark/HBaseRDDFunctions.scala
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.spark
+
+import org.apache.hadoop.hbase.TableName
+import org.apache.hadoop.hbase.client._
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable
+import org.apache.spark.rdd.RDD
+import org.apache.yetus.audience.InterfaceAudience
+import scala.reflect.ClassTag
+
+/**
+ * HBaseRDDFunctions contains a set of implicit functions that can be
+ * applied to a Spark RDD so that we can easily interact with HBase
+ */
[email protected]
+object HBaseRDDFunctions {
+
+  /**
+   * These are implicit methods for a RDD that contains any type of
+   * data.
+   *
+   * @param rdd This is for rdd of any type
+   * @tparam T  This is any type
+   */
+  implicit class GenericHBaseRDDFunctions[T](val rdd: RDD[T]) {
+
+    /**
+     * Implicit method that gives easy access to HBaseContext's bulk
+     * put.  This will not return a new RDD.  Think of it like a foreach
+     *
+     * @param hc         The hbaseContext object to identify which
+     *                   HBase cluster connection to use
+     * @param tableName  The tableName that the put will be sent to
+     * @param f          The function that will turn the RDD values
+     *                   into HBase Put objects.
+     */
+    def hbaseBulkPut(hc: HBaseContext, tableName: TableName, f: (T) => Put): 
Unit = {
+      hc.bulkPut(rdd, tableName, f)
+    }
+
+    /**
+     * Implicit method that gives easy access to HBaseContext's bulk
+     * get.  This will return a new RDD.  Think about it as a RDD map
+     * function.  In that every RDD value will get a new value out of
+     * HBase.  That new value will populate the newly generated RDD.
+     *
+     * @param hc             The hbaseContext object to identify which
+     *                       HBase cluster connection to use
+     * @param tableName      The tableName that the get will be sent to
+     * @param batchSize      How many gets to execute in a single batch
+     * @param f              The function that will turn the RDD values
+     *                       in HBase Get objects
+     * @param convertResult  The function that will convert a HBase
+     *                       Result object into a value that will go
+     *                       into the resulting RDD
+     * @tparam R             The type of Object that will be coming
+     *                       out of the resulting RDD
+     * @return               A resulting RDD with type R objects
+     */
+    def hbaseBulkGet[R: ClassTag](
+        hc: HBaseContext,
+        tableName: TableName,
+        batchSize: Int,
+        f: (T) => Get,
+        convertResult: (Result) => R): RDD[R] = {
+      hc.bulkGet[T, R](tableName, batchSize, rdd, f, convertResult)
+    }
+
+    /**
+     * Implicit method that gives easy access to HBaseContext's bulk
+     * get.  This will return a new RDD.  Think about it as a RDD map
+     * function.  In that every RDD value will get a new value out of
+     * HBase.  That new value will populate the newly generated RDD.
+     *
+     * @param hc             The hbaseContext object to identify which
+     *                       HBase cluster connection to use
+     * @param tableName      The tableName that the get will be sent to
+     * @param batchSize      How many gets to execute in a single batch
+     * @param f              The function that will turn the RDD values
+     *                       in HBase Get objects
+     * @return               A resulting RDD with type R objects
+     */
+    def hbaseBulkGet(
+        hc: HBaseContext,
+        tableName: TableName,
+        batchSize: Int,
+        f: (T) => Get): RDD[(ImmutableBytesWritable, Result)] = {
+      hc
+        .bulkGet[T, (ImmutableBytesWritable, Result)](
+          tableName,
+          batchSize,
+          rdd,
+          f,
+          result =>
+            if (result != null && result.getRow != null) {
+              (new ImmutableBytesWritable(result.getRow), result)
+            } else {
+              null
+            })
+        .filter(_ != null)
+    }
+
+    /**
+     * Implicit method that gives easy access to HBaseContext's bulk
+     * Delete.  This will not return a new RDD.
+     *
+     * @param hc         The hbaseContext object to identify which HBase
+     *                   cluster connection to use
+     * @param tableName  The tableName that the deletes will be sent to
+     * @param f          The function that will convert the RDD value into
+     *                   a HBase Delete Object
+     * @param batchSize  The number of Deletes to be sent in a single batch
+     */
+    def hbaseBulkDelete(
+        hc: HBaseContext,
+        tableName: TableName,
+        f: (T) => Delete,
+        batchSize: Int): Unit = {
+      hc.bulkDelete(rdd, tableName, f, batchSize)
+    }
+
+    /**
+     * Implicit method that gives easy access to HBaseContext's
+     * foreachPartition method.  This will act very much like a normal RDD
+     * foreach method but for the fact that you will now have a HBase 
connection
+     * while iterating through the values.
+     *
+     * @param hc  The hbaseContext object to identify which HBase
+     *            cluster connection to use
+     * @param f   This function will get an iterator for a Partition of an
+     *            RDD along with a connection object to HBase
+     */
+    def hbaseForeachPartition(hc: HBaseContext, f: (Iterator[T], Connection) 
=> Unit): Unit = {
+      hc.foreachPartition(rdd, f)
+    }
+
+    /**
+     * Implicit method that gives easy access to HBaseContext's
+     * mapPartitions method.  This will act very much like a normal RDD
+     * map partitions method but for the fact that you will now have a
+     * HBase connection while iterating through the values
+     *
+     * @param hc  The hbaseContext object to identify which HBase
+     *            cluster connection to use
+     * @param f   This function will get an iterator for a Partition of an
+     *            RDD along with a connection object to HBase
+     * @tparam R  This is the type of objects that will go into the resulting
+     *            RDD
+     * @return    A resulting RDD of type R
+     */
+    def hbaseMapPartitions[R: ClassTag](
+        hc: HBaseContext,
+        f: (Iterator[T], Connection) => Iterator[R]): RDD[R] = {
+      hc.mapPartitions[T, R](rdd, f)
+    }
+  }
+}
diff --git 
a/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseConnectionCacheSuite.scala
 
b/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseConnectionCacheSuite.scala
new file mode 100644
index 0000000..f3ecffb
--- /dev/null
+++ 
b/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseConnectionCacheSuite.scala
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.spark
+
+import java.util.concurrent.ExecutorService
+import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.hbase.TableName
+import org.apache.hadoop.hbase.client.{
+  Admin,
+  BufferedMutator,
+  BufferedMutatorParams,
+  Connection,
+  RegionLocator,
+  Table,
+  TableBuilder
+}
+import org.scalatest.funsuite.AnyFunSuite
+import scala.util.Random
+
+case class HBaseConnectionKeyMocker(confId: Int) extends 
HBaseConnectionKey(null) {
+  override def hashCode: Int = {
+    confId
+  }
+
+  override def equals(obj: Any): Boolean = {
+    if (!obj.isInstanceOf[HBaseConnectionKeyMocker])
+      false
+    else
+      confId == obj.asInstanceOf[HBaseConnectionKeyMocker].confId
+  }
+}
+
+class ConnectionMocker extends Connection {
+  var isClosed: Boolean = false
+
+  def getRegionLocator(tableName: TableName): RegionLocator = null
+  def getConfiguration: Configuration = null
+  override def getTable(tableName: TableName): Table = null
+  override def getTable(tableName: TableName, pool: ExecutorService): Table = 
null
+  def getBufferedMutator(params: BufferedMutatorParams): BufferedMutator = null
+  def getBufferedMutator(tableName: TableName): BufferedMutator = null
+  def getAdmin: Admin = null
+  def getTableBuilder(tableName: TableName, pool: ExecutorService): 
TableBuilder = null
+
+  def close(): Unit = {
+    if (isClosed)
+      throw new IllegalStateException()
+    isClosed = true
+  }
+
+  def isAborted: Boolean = true
+  def abort(why: String, e: Throwable): Unit = {}
+
+  def clearRegionLocationCache(): Unit = {}
+}
+
+class HBaseConnectionCacheSuite extends AnyFunSuite with Logging {
+  /*
+   * These tests must be performed sequentially as they operate with an
+   * unique running thread and resource.
+   */
+  test("all test cases") {
+    testBasic()
+    testWithPressureWithoutClose()
+    testWithPressureWithClose()
+  }
+
+  def cleanEnv(): Unit = {
+    HBaseConnectionCache.connectionMap.synchronized {
+      HBaseConnectionCache.connectionMap.clear()
+      HBaseConnectionCache.cacheStat.numActiveConnections = 0
+      HBaseConnectionCache.cacheStat.numActualConnectionsCreated = 0
+      HBaseConnectionCache.cacheStat.numTotalRequests = 0
+    }
+  }
+
+  def testBasic(): Unit = {
+    cleanEnv()
+    HBaseConnectionCache.setTimeout(1 * 1000)
+
+    val connKeyMocker1 = new HBaseConnectionKeyMocker(1)
+    val connKeyMocker1a = new HBaseConnectionKeyMocker(1)
+    val connKeyMocker2 = new HBaseConnectionKeyMocker(2)
+
+    val c1 = HBaseConnectionCache
+      .getConnection(connKeyMocker1, new ConnectionMocker)
+
+    assert(HBaseConnectionCache.connectionMap.size === 1)
+    assert(HBaseConnectionCache.getStat.numTotalRequests === 1)
+    assert(HBaseConnectionCache.getStat.numActualConnectionsCreated === 1)
+    assert(HBaseConnectionCache.getStat.numActiveConnections === 1)
+
+    val c1a = HBaseConnectionCache
+      .getConnection(connKeyMocker1a, new ConnectionMocker)
+
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 1)
+      assert(HBaseConnectionCache.getStat.numTotalRequests === 2)
+      assert(HBaseConnectionCache.getStat.numActualConnectionsCreated === 1)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 1)
+    }
+
+    val c2 = HBaseConnectionCache
+      .getConnection(connKeyMocker2, new ConnectionMocker)
+
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 2)
+      assert(HBaseConnectionCache.getStat.numTotalRequests === 3)
+      assert(HBaseConnectionCache.getStat.numActualConnectionsCreated === 2)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 2)
+    }
+
+    c1.close()
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 2)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 2)
+    }
+
+    c1a.close()
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 2)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 2)
+    }
+
+    Thread.sleep(3 * 1000) // Leave housekeeping thread enough time
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 1)
+      assert(
+        HBaseConnectionCache.connectionMap.iterator
+          .next()
+          ._1
+          .asInstanceOf[HBaseConnectionKeyMocker]
+          .confId === 2)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 1)
+    }
+
+    c2.close()
+  }
+
+  def testWithPressureWithoutClose(): Unit = {
+    cleanEnv()
+
+    class TestThread extends Runnable {
+      override def run(): Unit = {
+        for (i <- 0 to 999) {
+          val c = HBaseConnectionCache.getConnection(
+            new HBaseConnectionKeyMocker(Random.nextInt(10)),
+            new ConnectionMocker)
+        }
+      }
+    }
+
+    HBaseConnectionCache.setTimeout(500)
+    val threads: Array[Thread] = new Array[Thread](100)
+for (i <- 0 to 99) {
+  threads.update(i, new Thread(new TestThread()))
+  threads(i).start()
+}
+try {
+  threads.foreach { x => x.join() }
+} catch {
+  case e: InterruptedException =>
+    Thread.currentThread().interrupt()
+    throw e
+}
+
+    Thread.sleep(1000)
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 10)
+      assert(HBaseConnectionCache.getStat.numTotalRequests === 100 * 1000)
+      assert(HBaseConnectionCache.getStat.numActualConnectionsCreated === 10)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 10)
+
+      var totalRc: Int = 0
+      HBaseConnectionCache.connectionMap.foreach { x => totalRc += 
x._2.refCount }
+      assert(totalRc === 100 * 1000)
+      HBaseConnectionCache.connectionMap.foreach {
+        x =>
+          {
+            x._2.refCount = 0
+            x._2.timestamp = System.currentTimeMillis() - 1000
+          }
+      }
+    }
+    Thread.sleep(1000)
+    assert(HBaseConnectionCache.connectionMap.size === 0)
+    assert(HBaseConnectionCache.getStat.numActualConnectionsCreated === 10)
+    assert(HBaseConnectionCache.getStat.numActiveConnections === 0)
+  }
+
+  def testWithPressureWithClose(): Unit = {
+    cleanEnv()
+
+    class TestThread extends Runnable {
+      override def run(): Unit = {
+        for (i <- 0 to 999) {
+          val c = HBaseConnectionCache.getConnection(
+            new HBaseConnectionKeyMocker(Random.nextInt(10)),
+            new ConnectionMocker)
+          Thread.`yield`()
+          c.close()
+        }
+      }
+    }
+
+    HBaseConnectionCache.setTimeout(3 * 1000)
+    val threads: Array[Thread] = new Array[Thread](100)
+    for (i <- threads.indices) {
+      threads.update(i, new Thread(new TestThread()))
+      threads(i).run()
+    }
+    try {
+      threads.foreach { x => x.join() }
+    } catch {
+      case e: InterruptedException => println(e.getMessage)
+    }
+
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 10)
+      assert(HBaseConnectionCache.getStat.numTotalRequests === 100 * 1000)
+      assert(HBaseConnectionCache.getStat.numActualConnectionsCreated === 10)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 10)
+    }
+
+    Thread.sleep(6 * 1000)
+    HBaseConnectionCache.connectionMap.synchronized {
+      assert(HBaseConnectionCache.connectionMap.size === 0)
+      assert(HBaseConnectionCache.getStat.numActualConnectionsCreated === 10)
+      assert(HBaseConnectionCache.getStat.numActiveConnections === 0)
+    }
+  }
+}
diff --git 
a/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala
 
b/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala
index d6a4b98..d374d39 100644
--- 
a/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala
+++ 
b/spark4/hbase-spark4/src/test/scala/org/apache/hadoop/hbase/spark/HBaseContextSuite.scala
@@ -18,13 +18,14 @@
 package org.apache.hadoop.hbase.spark
 
 import org.apache.hadoop.hbase.{CellUtil, HBaseTestingUtility, TableName}
-import org.apache.hadoop.hbase.client.{ConnectionFactory, Put, Scan}
+import org.apache.hadoop.hbase.client._
 import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter
 import org.apache.hadoop.hbase.io.ImmutableBytesWritable
 import org.apache.hadoop.hbase.util.Bytes
 import org.apache.spark.{SparkConf, SparkContext}
 import org.scalatest.BeforeAndAfterAll
 import org.scalatest.funsuite.AnyFunSuite
+import scala.collection.mutable.ListBuffer
 
 class HBaseContextSuite extends AnyFunSuite with BeforeAndAfterAll with 
Logging {
 
@@ -158,4 +159,264 @@ class HBaseContextSuite extends AnyFunSuite with 
BeforeAndAfterAll with Logging
     assert(rows(0).getString(0) == "value1")
     assert(rows(0).get(1) == null)
   }
+
+  test("bulkPut to test HBase client") {
+    val rdd = sc.parallelize(
+      Array[(Array[Byte], Array[(Array[Byte], Array[Byte], Array[Byte])])](
+        (
+          Bytes.toBytes("put1"),
+          Array((Bytes.toBytes(columnFamily), Bytes.toBytes("a"), 
Bytes.toBytes("foo1")))),
+        (
+          Bytes.toBytes("put2"),
+          Array((Bytes.toBytes(columnFamily), Bytes.toBytes("b"), 
Bytes.toBytes("foo2")))),
+        (
+          Bytes.toBytes("put3"),
+          Array((Bytes.toBytes(columnFamily), Bytes.toBytes("c"), 
Bytes.toBytes("foo3"))))))
+
+    hbaseContext.bulkPut[(Array[Byte], Array[(Array[Byte], Array[Byte], 
Array[Byte])])](
+      rdd,
+      TableName.valueOf(tableName),
+      (putRecord) => {
+        val put = new Put(putRecord._1)
+        putRecord._2.foreach((putValue) => put.addColumn(putValue._1, 
putValue._2, putValue._3))
+        put
+      })
+
+    val connection = 
ConnectionFactory.createConnection(TEST_UTIL.getConfiguration)
+    val table = connection.getTable(TableName.valueOf(tableName))
+    try {
+      val foo1 = Bytes.toString(
+        CellUtil.cloneValue(
+          table
+            .get(new Get(Bytes.toBytes("put1")))
+            .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("a"))))
+      assert(foo1 == "foo1")
+
+      val foo2 = Bytes.toString(
+        CellUtil.cloneValue(
+          table
+            .get(new Get(Bytes.toBytes("put2")))
+            .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("b"))))
+      assert(foo2 == "foo2")
+
+      val foo3 = Bytes.toString(
+        CellUtil.cloneValue(
+          table
+            .get(new Get(Bytes.toBytes("put3")))
+            .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("c"))))
+      assert(foo3 == "foo3")
+    } finally {
+      table.close()
+      connection.close()
+    }
+  }
+
+  test("bulkDelete to test HBase client") {
+    val connection = 
ConnectionFactory.createConnection(TEST_UTIL.getConfiguration)
+    val table = connection.getTable(TableName.valueOf(tableName))
+
+    try {
+      var put = new Put(Bytes.toBytes("delete1"))
+      put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes("a"), 
Bytes.toBytes("foo1"))
+      table.put(put)
+      put = new Put(Bytes.toBytes("delete2"))
+      put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes("a"), 
Bytes.toBytes("foo2"))
+      table.put(put)
+      put = new Put(Bytes.toBytes("delete3"))
+      put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes("a"), 
Bytes.toBytes("foo3"))
+      table.put(put)
+    } finally {
+      table.close()
+      connection.close()
+    }
+
+    val rdd = sc.parallelize(Array[Array[Byte]](Bytes.toBytes("delete1"), 
Bytes.toBytes("delete3")))
+
+    hbaseContext.bulkDelete[Array[Byte]](
+      rdd,
+      TableName.valueOf(tableName),
+      putRecord => new Delete(putRecord),
+      4)
+
+    val connection2 = 
ConnectionFactory.createConnection(TEST_UTIL.getConfiguration)
+    val table2 = connection2.getTable(TableName.valueOf(tableName))
+    try {
+      assert(
+        table2
+          .get(new Get(Bytes.toBytes("delete1")))
+          .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("a")) == null)
+      assert(
+        table2
+          .get(new Get(Bytes.toBytes("delete3")))
+          .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("a")) == null)
+      assert(
+        Bytes
+          .toString(
+            CellUtil.cloneValue(table2
+              .get(new Get(Bytes.toBytes("delete2")))
+              .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("a"))))
+          .equals("foo2"))
+    } finally {
+      table2.close()
+      connection2.close()
+    }
+  }
+
+  test("bulkGet to test HBase client") {
+    val connection = 
ConnectionFactory.createConnection(TEST_UTIL.getConfiguration)
+    val table = connection.getTable(TableName.valueOf(tableName))
+
+    try {
+      var put = new Put(Bytes.toBytes("get1"))
+      put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes("a"), 
Bytes.toBytes("foo1"))
+      table.put(put)
+      put = new Put(Bytes.toBytes("get2"))
+      put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes("a"), 
Bytes.toBytes("foo2"))
+      table.put(put)
+      put = new Put(Bytes.toBytes("get3"))
+      put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes("a"), 
Bytes.toBytes("foo3"))
+      table.put(put)
+    } finally {
+      table.close()
+      connection.close()
+    }
+
+    val rdd = sc.parallelize(
+      Array[Array[Byte]](
+        Bytes.toBytes("get1"),
+        Bytes.toBytes("get2"),
+        Bytes.toBytes("get3"),
+        Bytes.toBytes("get4")))
+
+    val getRdd = hbaseContext.bulkGet[Array[Byte], String](
+      TableName.valueOf(tableName),
+      2,
+      rdd,
+      record => {
+        new Get(record)
+      },
+      (result: Result) => {
+        if (result.listCells() != null) {
+          val it = result.listCells().iterator()
+          val B = new StringBuilder
+
+          B.append(Bytes.toString(result.getRow) + ":")
+
+          while (it.hasNext) {
+            val cell = it.next()
+            val q = Bytes.toString(CellUtil.cloneQualifier(cell))
+            if (q.equals("counter")) {
+              B.append("(" + q + "," + Bytes.toLong(CellUtil.cloneValue(cell)) 
+ ")")
+            } else {
+              B.append("(" + q + "," + 
Bytes.toString(CellUtil.cloneValue(cell)) + ")")
+            }
+          }
+          B.toString
+        } else {
+          ""
+        }
+      })
+    val getArray = getRdd.collect()
+
+    assert(getArray.length == 4)
+    assert(getArray.contains("get1:(a,foo1)"))
+    assert(getArray.contains("get2:(a,foo2)"))
+    assert(getArray.contains("get3:(a,foo3)"))
+  }
+
+  test("foreachPartition with connection") {
+    val tName = tableName
+    val cf = columnFamily
+    val rdd = sc.parallelize(
+      Array[(Array[Byte], Array[Byte])](
+        (Bytes.toBytes("fp1"), Bytes.toBytes("value_fp1")),
+        (Bytes.toBytes("fp2"), Bytes.toBytes("value_fp2")),
+        (Bytes.toBytes("fp3"), Bytes.toBytes("value_fp3"))))
+
+    hbaseContext.foreachPartition[(Array[Byte], Array[Byte])](
+      rdd,
+      (it, connection) => {
+        val m = connection.getBufferedMutator(TableName.valueOf(tName))
+        it.foreach {
+          case (rowKey, value) =>
+            val put = new Put(rowKey)
+            put.addColumn(Bytes.toBytes(cf), Bytes.toBytes("a"), value)
+            m.mutate(put)
+        }
+        m.flush()
+        m.close()
+      })
+
+    val connection = 
ConnectionFactory.createConnection(TEST_UTIL.getConfiguration)
+    val table = connection.getTable(TableName.valueOf(tableName))
+    try {
+      val v1 = Bytes.toString(
+        CellUtil.cloneValue(
+          table
+            .get(new Get(Bytes.toBytes("fp1")))
+            .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("a"))))
+      assert(v1 == "value_fp1")
+
+      val v2 = Bytes.toString(
+        CellUtil.cloneValue(
+          table
+            .get(new Get(Bytes.toBytes("fp2")))
+            .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("a"))))
+      assert(v2 == "value_fp2")
+
+      val v3 = Bytes.toString(
+        CellUtil.cloneValue(
+          table
+            .get(new Get(Bytes.toBytes("fp3")))
+            .getColumnLatestCell(Bytes.toBytes(columnFamily), 
Bytes.toBytes("a"))))
+      assert(v3 == "value_fp3")
+    } finally {
+      table.close()
+      connection.close()
+    }
+  }
+
+  test("mapPartitions with connection") {
+    val tName = tableName
+    val cf = columnFamily
+    val connection = 
ConnectionFactory.createConnection(TEST_UTIL.getConfiguration)
+    val table = connection.getTable(TableName.valueOf(tName))
+    try {
+      var put = new Put(Bytes.toBytes("mp1"))
+      put.addColumn(Bytes.toBytes(cf), Bytes.toBytes("a"), 
Bytes.toBytes("val_mp1"))
+      table.put(put)
+      put = new Put(Bytes.toBytes("mp2"))
+      put.addColumn(Bytes.toBytes(cf), Bytes.toBytes("a"), 
Bytes.toBytes("val_mp2"))
+      table.put(put)
+    } finally {
+      table.close()
+      connection.close()
+    }
+
+    val rdd = sc.parallelize(Array[Array[Byte]](Bytes.toBytes("mp1"), 
Bytes.toBytes("mp2")))
+
+    val resultRdd = hbaseContext.mapPartitions[Array[Byte], String](
+      rdd,
+      (it, conn) => {
+        val tbl = conn.getTable(TableName.valueOf(tName))
+        try {
+          val res = new ListBuffer[String]()
+          it.foreach { rowKey =>
+            val result = tbl.get(new Get(rowKey))
+            val cell = result.getColumnLatestCell(Bytes.toBytes(cf), 
Bytes.toBytes("a"))
+            if (cell != null) {
+              res += Bytes.toString(result.getRow) + "=" + 
Bytes.toString(CellUtil.cloneValue(cell))
+            }
+          }
+          res.iterator
+        } finally {
+          tbl.close()
+        }
+      })
+
+    val results = resultRdd.collect().sorted
+    assert(results.length == 2)
+    assert(results.contains("mp1=val_mp1"))
+    assert(results.contains("mp2=val_mp2"))
+  }
 }

Reply via email to