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

lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 9efb58d82 [spark] Optimize Spark write newly dynamic bucket table 
(#3349)
9efb58d82 is described below

commit 9efb58d82161226d53e643ead819b910924c6643
Author: Jingsong Lee <[email protected]>
AuthorDate: Mon May 20 22:02:24 2024 +0800

    [spark] Optimize Spark write newly dynamic bucket table (#3349)
---
 .../org/apache/paimon/index/BucketAssigner.java    |   4 +
 .../apache/paimon/index/HashBucketAssigner.java    |   2 +-
 .../paimon/index/SimpleHashBucketAssigner.java     |  21 ++-
 .../paimon/index/SimpleHashBucketAssignerTest.java |  19 ++
 .../java/org/apache/paimon/spark/SparkRow.java     |   2 +-
 .../org/apache/paimon/spark/SparkTableWrite.java   |  78 ++++++++
 .../paimon/spark/commands/BucketProcessor.scala    |  19 +-
 .../paimon/spark/commands/PaimonSparkWriter.scala  | 204 ++++++++++++---------
 8 files changed, 235 insertions(+), 114 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/BucketAssigner.java 
b/paimon-core/src/main/java/org/apache/paimon/index/BucketAssigner.java
index 9690c66c9..20c3b3987 100644
--- a/paimon-core/src/main/java/org/apache/paimon/index/BucketAssigner.java
+++ b/paimon-core/src/main/java/org/apache/paimon/index/BucketAssigner.java
@@ -27,6 +27,10 @@ public interface BucketAssigner {
 
     void prepareCommit(long commitIdentifier);
 
+    static boolean isMyBucket(int bucket, int numAssigners, int assignId) {
+        return bucket % numAssigners == assignId % numAssigners;
+    }
+
     static int computeHashKey(int partitionHash, int keyHash, int numChannels, 
int numAssigners) {
         int start = Math.abs(partitionHash % numChannels);
         int id = Math.abs(keyHash % numAssigners);
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/HashBucketAssigner.java 
b/paimon-core/src/main/java/org/apache/paimon/index/HashBucketAssigner.java
index 114b8d3df..60bade817 100644
--- a/paimon-core/src/main/java/org/apache/paimon/index/HashBucketAssigner.java
+++ b/paimon-core/src/main/java/org/apache/paimon/index/HashBucketAssigner.java
@@ -159,7 +159,7 @@ public class HashBucketAssigner implements BucketAssigner {
     }
 
     private boolean isMyBucket(int bucket) {
-        return bucket % numAssigners == assignId % numAssigners;
+        return BucketAssigner.isMyBucket(bucket, numAssigners, assignId);
     }
 
     private PartitionIndex loadIndex(BinaryRow partition, int partitionHash) {
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/index/SimpleHashBucketAssigner.java
 
b/paimon-core/src/main/java/org/apache/paimon/index/SimpleHashBucketAssigner.java
index 7094684fc..5f7599370 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/index/SimpleHashBucketAssigner.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/index/SimpleHashBucketAssigner.java
@@ -18,11 +18,13 @@
 
 package org.apache.paimon.index;
 
+import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.utils.Int2ShortHashMap;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 /** When we need to overwrite the table, we should use this to avoid loading 
index. */
 public class SimpleHashBucketAssigner implements BucketAssigner {
@@ -42,8 +44,12 @@ public class SimpleHashBucketAssigner implements 
BucketAssigner {
 
     @Override
     public int assign(BinaryRow partition, int hash) {
-        SimplePartitionIndex index =
-                this.partitionIndex.computeIfAbsent(partition, p -> new 
SimplePartitionIndex());
+        SimplePartitionIndex index = partitionIndex.get(partition);
+        if (index == null) {
+            partition = partition.copy();
+            index = new SimplePartitionIndex();
+            this.partitionIndex.put(partition, index);
+        }
         return index.assign(hash);
     }
 
@@ -52,6 +58,11 @@ public class SimpleHashBucketAssigner implements 
BucketAssigner {
         // do nothing
     }
 
+    @VisibleForTesting
+    Set<BinaryRow> currentPartitions() {
+        return partitionIndex.keySet();
+    }
+
     /** Simple partition bucket hash assigner. */
     private class SimplePartitionIndex {
 
@@ -81,7 +92,7 @@ public class SimpleHashBucketAssigner implements 
BucketAssigner {
 
         private void loadNewBucket() {
             for (int i = 0; i < Short.MAX_VALUE; i++) {
-                if (i % numAssigners == assignId && 
!bucketInformation.containsKey(i)) {
+                if (isMyBucket(i) && !bucketInformation.containsKey(i)) {
                     currentBucket = i;
                     return;
                 }
@@ -90,4 +101,8 @@ public class SimpleHashBucketAssigner implements 
BucketAssigner {
                     "Can't find a suitable bucket to assign, all the bucket 
are assigned?");
         }
     }
+
+    private boolean isMyBucket(int bucket) {
+        return BucketAssigner.isMyBucket(bucket, numAssigners, assignId);
+    }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/index/SimpleHashBucketAssignerTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/index/SimpleHashBucketAssignerTest.java
index d795b8926..d1b26019f 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/index/SimpleHashBucketAssignerTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/index/SimpleHashBucketAssignerTest.java
@@ -23,6 +23,9 @@ import org.apache.paimon.data.BinaryRow;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import static org.apache.paimon.io.DataFileTestUtils.row;
+import static org.assertj.core.api.Assertions.assertThat;
+
 /** Tests for {@link SimpleHashBucketAssigner}. */
 public class SimpleHashBucketAssignerTest {
 
@@ -66,4 +69,20 @@ public class SimpleHashBucketAssignerTest {
             Assertions.assertThat(bucket).isEqualTo(0);
         }
     }
+
+    @Test
+    public void testPartitionCopy() {
+        SimpleHashBucketAssigner assigner = new SimpleHashBucketAssigner(1, 0, 
5);
+
+        BinaryRow partition = row(1);
+        assertThat(assigner.assign(partition, 0)).isEqualTo(0);
+        assertThat(assigner.assign(partition, 1)).isEqualTo(0);
+
+        partition.setInt(0, 2);
+        assertThat(assigner.assign(partition, 5)).isEqualTo(0);
+        assertThat(assigner.assign(partition, 6)).isEqualTo(0);
+
+        assertThat(assigner.currentPartitions()).contains(row(1));
+        assertThat(assigner.currentPartitions()).contains(row(2));
+    }
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkRow.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkRow.java
index 5ba1a346f..c650719b0 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkRow.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkRow.java
@@ -68,7 +68,7 @@ public class SparkRow implements InternalRow, Serializable {
 
     @Override
     public int getFieldCount() {
-        return row.size();
+        return type.getFieldCount();
     }
 
     @Override
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkTableWrite.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkTableWrite.java
new file mode 100644
index 000000000..3e8926080
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkTableWrite.java
@@ -0,0 +1,78 @@
+/*
+ * 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.paimon.spark;
+
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.spark.util.SparkRowUtils;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.CommitMessageSerializer;
+import org.apache.paimon.types.RowType;
+
+import org.apache.spark.sql.Row;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/** An util class for {@link BatchTableWrite}. */
+public class SparkTableWrite implements AutoCloseable {
+
+    private final BatchTableWrite write;
+    private final IOManager ioManager;
+
+    private final RowType rowType;
+    private final int rowKindColIdx;
+
+    public SparkTableWrite(BatchWriteBuilder writeBuilder, RowType rowType, 
int rowKindColIdx) {
+        this.write = writeBuilder.newWrite();
+        this.rowType = rowType;
+        this.rowKindColIdx = rowKindColIdx;
+        this.ioManager = SparkUtils.createIOManager();
+        write.withIOManager(ioManager);
+    }
+
+    public void write(Row row) throws Exception {
+        write.write(toPaimonRow(row));
+    }
+
+    public void write(Row row, int bucket) throws Exception {
+        write.write(toPaimonRow(row), bucket);
+    }
+
+    public Iterator<byte[]> finish() throws Exception {
+        CommitMessageSerializer serializer = new CommitMessageSerializer();
+        List<byte[]> commitMessages = new ArrayList<>();
+        for (CommitMessage message : write.prepareCommit()) {
+            commitMessages.add(serializer.serialize(message));
+        }
+        return commitMessages.iterator();
+    }
+
+    @Override
+    public void close() throws Exception {
+        write.close();
+        ioManager.close();
+    }
+
+    private SparkRow toPaimonRow(Row row) {
+        return new SparkRow(rowType, row, SparkRowUtils.getRowKind(row, 
rowKindColIdx));
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/BucketProcessor.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/BucketProcessor.scala
index 4af2b10b1..4a3393497 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/BucketProcessor.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/BucketProcessor.scala
@@ -76,7 +76,7 @@ case class CommonBucketProcessor(
       override def next(): Row = {
         val row = rowIterator.next
         val sparkInternalRow = encoderGroup.rowToInternal(row)
-        sparkInternalRow.setInt(bucketColIndex, getBucketId((new 
SparkRow(rowType, row))))
+        sparkInternalRow.setInt(bucketColIndex, getBucketId(new 
SparkRow(rowType, row)))
         encoderGroup.internalToRow(sparkInternalRow)
       }
     }
@@ -123,20 +123,3 @@ case class DynamicBucketProcessor(
     }
   }
 }
-
-case class UnawareBucketProcessor(bucketColIndex: Int, encoderGroup: 
EncoderSerDeGroup)
-  extends BucketProcessor {
-
-  def processPartition(rowIterator: Iterator[Row]): Iterator[Row] = {
-    new Iterator[Row] {
-      override def hasNext: Boolean = rowIterator.hasNext
-
-      override def next(): Row = {
-        val row = rowIterator.next
-        val sparkInternalRow = encoderGroup.rowToInternal(row)
-        sparkInternalRow.setInt(bucketColIndex, 0)
-        encoderGroup.internalToRow(sparkInternalRow)
-      }
-    }
-  }
-}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala
index ae7e67fc0..b9abbb02d 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/PaimonSparkWriter.scala
@@ -18,23 +18,22 @@
 
 package org.apache.paimon.spark.commands
 
-import org.apache.paimon.CoreOptions
 import org.apache.paimon.CoreOptions.WRITE_ONLY
-import org.apache.paimon.index.BucketAssigner
-import org.apache.paimon.spark.SparkRow
-import org.apache.paimon.spark.SparkUtils.createIOManager
+import org.apache.paimon.index.{BucketAssigner, SimpleHashBucketAssigner}
+import org.apache.paimon.spark.{SparkRow, SparkTableWrite}
 import org.apache.paimon.spark.schema.SparkSystemColumns
 import org.apache.paimon.spark.schema.SparkSystemColumns.{BUCKET_COL, 
ROW_KIND_COL}
 import org.apache.paimon.spark.util.SparkRowUtils
 import org.apache.paimon.table.{BucketMode, FileStoreTable}
 import org.apache.paimon.table.sink.{BatchWriteBuilder, CommitMessage, 
CommitMessageSerializer, RowPartitionKeyExtractor}
+import org.apache.paimon.utils.Preconditions
+import org.apache.paimon.utils.Preconditions.checkArgument
 
-import org.apache.spark.Partitioner
+import org.apache.spark.{Partitioner, TaskContext}
 import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}
 import org.apache.spark.sql.functions._
 
 import java.io.IOException
-import java.util.Collections
 import java.util.Collections.singletonMap
 
 import scala.collection.JavaConverters._
@@ -45,12 +44,7 @@ case class PaimonSparkWriter(table: FileStoreTable) {
 
   private lazy val rowType = table.rowType()
 
-  private lazy val bucketMode = table match {
-    case fileStoreTable: FileStoreTable =>
-      fileStoreTable.bucketMode
-    case _ =>
-      BucketMode.HASH_FIXED
-  }
+  private lazy val bucketMode = table.bucketMode
 
   private lazy val primaryKeyCols = tableSchema.trimmedPrimaryKeys().asScala
 
@@ -62,80 +56,78 @@ case class PaimonSparkWriter(table: FileStoreTable) {
     PaimonSparkWriter(table.copy(singletonMap(WRITE_ONLY.key(), "true")))
   }
 
-  def write(data: Dataset[_]): Seq[CommitMessage] = {
+  def write(data: Dataset[Row]): Seq[CommitMessage] = {
     val sparkSession = data.sparkSession
     import sparkSession.implicits._
 
-    val dataSchema = SparkSystemColumns.filterSparkSystemColumns(data.schema)
-    val rowkindColIdx = SparkRowUtils.getFieldIndex(data.schema, ROW_KIND_COL)
+    val rowKindColIdx = SparkRowUtils.getFieldIndex(data.schema, ROW_KIND_COL)
+    assert(
+      rowKindColIdx == -1 || rowKindColIdx == data.schema.length - 1,
+      "Row kind column should be the last field.")
 
     // append _bucket_ column as placeholder
     val withInitBucketCol = data.withColumn(BUCKET_COL, lit(-1))
     val bucketColIdx = withInitBucketCol.schema.size - 1
-
-    val originEncoderGroup = EncoderSerDeGroup(dataSchema)
     val encoderGroupWithBucketCol = EncoderSerDeGroup(withInitBucketCol.schema)
 
-    val withBucketCol =
-      assignBucketId(sparkSession, withInitBucketCol, bucketColIdx, 
encoderGroupWithBucketCol)
+    def newWrite(): SparkTableWrite = new SparkTableWrite(writeBuilder, 
rowType, rowKindColIdx)
 
-    val commitMessages = withBucketCol
-      .mapPartitions {
+    def writeWithoutBucket(): Dataset[Array[Byte]] = {
+      data.mapPartitions {
         iter =>
-          val ioManager = createIOManager
-          val write = writeBuilder.newWrite()
-          write.withIOManager(ioManager)
-          try {
-            iter.foreach {
-              row =>
-                val bucket = row.getInt(bucketColIdx)
-                val bucketColDropped =
-                  
originEncoderGroup.internalToRow(encoderGroupWithBucketCol.rowToInternal(row))
-                val sparkRow = new SparkRow(
-                  rowType,
-                  bucketColDropped,
-                  SparkRowUtils.getRowKind(row, rowkindColIdx))
-                write.write(sparkRow, bucket)
+          {
+            val write = newWrite()
+            try {
+              iter.foreach(row => write.write(row))
+              write.finish().asScala
+            } finally {
+              write.close()
             }
-            val serializer = new CommitMessageSerializer
-            write.prepareCommit().asScala.map(serializer.serialize).toIterator
-
-          } finally {
-            write.close()
-            ioManager.close()
           }
       }
-      .collect()
-      .map(deserializeCommitMessage(serializer, _))
-
-    commitMessages.toSeq
-  }
-
-  def commit(commitMessages: Seq[CommitMessage]): Unit = {
-    val tableCommit = writeBuilder.newCommit()
-    try {
-      tableCommit.commit(commitMessages.toList.asJava)
-    } catch {
-      case e: Throwable => throw new RuntimeException(e);
-    } finally {
-      tableCommit.close()
     }
-  }
 
-  /** assign a valid bucket id for each of record. */
-  private def assignBucketId(
-      sparkSession: SparkSession,
-      withInitBucketCol: DataFrame,
-      bucketColIdx: Int,
-      encoderGroupWithBucketCol: EncoderSerDeGroup): Dataset[Row] = {
+    def writeWithBucketProcessor(
+        dataFrame: DataFrame,
+        processor: BucketProcessor): Dataset[Array[Byte]] = {
+      val repartitioned = repartitionByPartitionsAndBucket(
+        
dataFrame.mapPartitions(processor.processPartition)(encoderGroupWithBucketCol.encoder))
+      repartitioned.mapPartitions {
+        iter =>
+          {
+            val write = newWrite()
+            try {
+              iter.foreach(row => write.write(row, row.getInt(bucketColIdx)))
+              write.finish().asScala
+            } finally {
+              write.close()
+            }
+          }
+      }
+    }
 
-    val encoderWithBucketCOl = encoderGroupWithBucketCol.encoder
+    def writeWithBucketAssigner(
+        dataFrame: DataFrame,
+        funcFactory: () => Row => Int): Dataset[Array[Byte]] = {
+      dataFrame.mapPartitions {
+        iter =>
+          {
+            val assigner = funcFactory.apply()
+            val write = newWrite()
+            try {
+              iter.foreach(row => write.write(row, assigner.apply(row)))
+              write.finish().asScala
+            } finally {
+              write.close()
+            }
+          }
+      }
+    }
 
-    bucketMode match {
+    val written: Dataset[Array[Byte]] = bucketMode match {
       case BucketMode.HASH_DYNAMIC =>
         assert(primaryKeyCols.nonEmpty, "Only primary-key table can support 
dynamic bucket.")
 
-        // Topology: input -> shuffle by special key & partition hash -> 
bucket-assigner -> shuffle by partition & bucket
         val numParallelism = 
Option(table.coreOptions.dynamicBucketAssignerParallelism)
           .map(_.toInt)
           .getOrElse {
@@ -147,42 +139,72 @@ case class PaimonSparkWriter(table: FileStoreTable) {
           .map(initialBuckets => Math.min(initialBuckets.toInt, 
numParallelism))
           .getOrElse(numParallelism)
 
-        val partitioned =
+        def partitionByKey(): DataFrame = {
           repartitionByKeyPartitionHash(
             sparkSession,
             withInitBucketCol,
             numParallelism,
             numAssigners)
-        val dynamicBucketProcessor =
-          DynamicBucketProcessor(
-            table,
-            bucketColIdx,
-            numParallelism,
-            numAssigners,
-            encoderGroupWithBucketCol)
-        repartitionByPartitionsAndBucket(
-          
partitioned.mapPartitions(dynamicBucketProcessor.processPartition)(encoderWithBucketCOl))
-
+        }
+
+        if (table.snapshotManager().latestSnapshot() == null) {
+          // bootstrap mode
+          // Topology: input -> shuffle by special key & partition hash -> 
bucket-assigner
+          writeWithBucketAssigner(
+            partitionByKey(),
+            () => {
+              val extractor = new RowPartitionKeyExtractor(table.schema)
+              val assigner =
+                new SimpleHashBucketAssigner(
+                  numAssigners,
+                  TaskContext.getPartitionId(),
+                  table.coreOptions.dynamicBucketTargetRowNum)
+              row => {
+                val sparkRow = new SparkRow(rowType, row)
+                assigner.assign(
+                  extractor.partition(sparkRow),
+                  extractor.trimmedPrimaryKey(sparkRow).hashCode)
+              }
+            }
+          )
+        } else {
+          // Topology: input -> shuffle by special key & partition hash -> 
bucket-assigner -> shuffle by partition & bucket
+          writeWithBucketProcessor(
+            partitionByKey(),
+            DynamicBucketProcessor(
+              table,
+              bucketColIdx,
+              numParallelism,
+              numAssigners,
+              encoderGroupWithBucketCol))
+        }
       case BucketMode.BUCKET_UNAWARE =>
-        assert(primaryKeyCols.isEmpty, "Only append table can support unaware 
bucket.")
-
-        // Topology: input -> bucket-assigner
-        val unawareBucketProcessor = UnawareBucketProcessor(bucketColIdx, 
encoderGroupWithBucketCol)
-        withInitBucketCol
-          
.mapPartitions(unawareBucketProcessor.processPartition)(encoderWithBucketCOl)
-          .toDF()
-
+        // Topology: input ->
+        writeWithoutBucket()
       case BucketMode.HASH_FIXED =>
         // Topology: input -> bucket-assigner -> shuffle by partition & bucket
-        val commonBucketProcessor =
-          CommonBucketProcessor(table, bucketColIdx, encoderGroupWithBucketCol)
-        repartitionByPartitionsAndBucket(
-          
withInitBucketCol.mapPartitions(commonBucketProcessor.processPartition)(
-            encoderWithBucketCOl))
-
+        writeWithBucketProcessor(
+          withInitBucketCol,
+          CommonBucketProcessor(table, bucketColIdx, 
encoderGroupWithBucketCol))
       case _ =>
         throw new UnsupportedOperationException(s"Spark doesn't support 
$bucketMode mode.")
     }
+
+    written
+      .collect()
+      .map(deserializeCommitMessage(serializer, _))
+      .toSeq
+  }
+
+  def commit(commitMessages: Seq[CommitMessage]): Unit = {
+    val tableCommit = writeBuilder.newCommit()
+    try {
+      tableCommit.commit(commitMessages.toList.asJava)
+    } catch {
+      case e: Throwable => throw new RuntimeException(e);
+    } finally {
+      tableCommit.close()
+    }
   }
 
   /** Compute bucket id in dynamic bucket mode. */
@@ -232,7 +254,7 @@ case class PaimonSparkWriter(table: FileStoreTable) {
     }
   }
 
-  case class ModPartitioner(partitions: Int) extends Partitioner {
+  private case class ModPartitioner(partitions: Int) extends Partitioner {
     override def numPartitions: Int = partitions
     override def getPartition(key: Any): Int = key.asInstanceOf[Int] % 
numPartitions
   }

Reply via email to