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

dybyte pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 0470de53f0 [Fix][Connector-V2] Fix MessageContentPartitioner static 
state causing cross-job partition routing interference (#10885)
0470de53f0 is described below

commit 0470de53f02cfcbdc1e246711bf3fbb5f0b8f56c
Author: cosmosni <[email protected]>
AuthorDate: Fri May 22 21:03:44 2026 +0800

    [Fix][Connector-V2] Fix MessageContentPartitioner static state causing 
cross-job partition routing interference (#10885)
---
 .../seatunnel/kafka/sink/KafkaSinkWriter.java      |   8 +-
 .../kafka/sink/MessageContentPartitioner.java      |  16 +--
 .../kafka/sink/MessageContentPartitionerTest.java  | 124 +++++++++++++++++++++
 3 files changed, 136 insertions(+), 12 deletions(-)

diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkWriter.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkWriter.java
index 7096fde5dc..9fcbc91098 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkWriter.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/KafkaSinkWriter.java
@@ -39,7 +39,6 @@ import 
org.apache.seatunnel.connectors.seatunnel.kafka.serialize.SeaTunnelRowSer
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaCommitInfo;
 import org.apache.seatunnel.connectors.seatunnel.kafka.state.KafkaSinkState;
 
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.kafka.clients.producer.ProducerConfig;
 import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.common.serialization.ByteArraySerializer;
@@ -89,10 +88,6 @@ public class KafkaSinkWriter implements 
SinkWriter<SeaTunnelRow, KafkaCommitInfo
             List<KafkaSinkState> kafkaStates) {
         this.context = context;
         this.seaTunnelRowType = seaTunnelRowType;
-        if (pluginConfig.get(ASSIGN_PARTITIONS) != null
-                && 
!CollectionUtils.isEmpty(pluginConfig.get(ASSIGN_PARTITIONS))) {
-            
MessageContentPartitioner.setAssignPartitions(pluginConfig.get(ASSIGN_PARTITIONS));
-        }
 
         if (pluginConfig.get(TRANSACTION_PREFIX) != null) {
             this.transactionPrefix = pluginConfig.get(TRANSACTION_PREFIX);
@@ -169,6 +164,9 @@ public class KafkaSinkWriter implements 
SinkWriter<SeaTunnelRow, KafkaCommitInfo
             kafkaProperties.put(
                     ProducerConfig.PARTITIONER_CLASS_CONFIG,
                     
"org.apache.seatunnel.connectors.seatunnel.kafka.sink.MessageContentPartitioner");
+            kafkaProperties.put(
+                    MessageContentPartitioner.ASSIGN_PARTITIONS_CONFIG,
+                    pluginConfig.get(ASSIGN_PARTITIONS));
         }
 
         kafkaProperties.put(
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/MessageContentPartitioner.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/MessageContentPartitioner.java
index 635f6721c5..907e87c583 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/MessageContentPartitioner.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/MessageContentPartitioner.java
@@ -25,11 +25,10 @@ import java.util.List;
 import java.util.Map;
 
 public class MessageContentPartitioner implements Partitioner {
-    private static List<String> ASSIGNPARTITIONS;
 
-    public static void setAssignPartitions(List<String> assignPartitionList) {
-        ASSIGNPARTITIONS = assignPartitionList;
-    }
+    public static final String ASSIGN_PARTITIONS_CONFIG = "assign.partitions";
+
+    private List<String> assignPartitions;
 
     @Override
     public int partition(
@@ -42,10 +41,10 @@ public class MessageContentPartitioner implements 
Partitioner {
         List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
         int numPartitions = partitions.size();
 
-        int assignPartitionsSize = ASSIGNPARTITIONS.size();
+        int assignPartitionsSize = assignPartitions.size();
         String message = new String(valueBytes);
         for (int i = 0; i < assignPartitionsSize; i++) {
-            if (message.contains(ASSIGNPARTITIONS.get(i))) {
+            if (message.contains(assignPartitions.get(i))) {
                 return i;
             }
         }
@@ -57,6 +56,9 @@ public class MessageContentPartitioner implements Partitioner 
{
     @Override
     public void close() {}
 
+    @SuppressWarnings("unchecked")
     @Override
-    public void configure(Map<String, ?> map) {}
+    public void configure(Map<String, ?> configs) {
+        this.assignPartitions = (List<String>) 
configs.get(ASSIGN_PARTITIONS_CONFIG);
+    }
 }
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/MessageContentPartitionerTest.java
 
b/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/MessageContentPartitionerTest.java
new file mode 100644
index 0000000000..68d48f11da
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/sink/MessageContentPartitionerTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.kafka.sink;
+
+import org.apache.kafka.common.Cluster;
+import org.apache.kafka.common.Node;
+import org.apache.kafka.common.PartitionInfo;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class MessageContentPartitionerTest {
+
+    private static final String TOPIC = "test_topic";
+
+    private Cluster createCluster(int numPartitions) {
+        Node node = new Node(0, "localhost", 9092);
+        PartitionInfo[] partitions = new PartitionInfo[numPartitions];
+        for (int i = 0; i < numPartitions; i++) {
+            partitions[i] = new PartitionInfo(TOPIC, i, node, new Node[] 
{node}, new Node[] {node});
+        }
+        return new Cluster(
+                "test-cluster",
+                Collections.singletonList(node),
+                Arrays.asList(partitions),
+                Collections.emptySet(),
+                Collections.emptySet());
+    }
+
+    private MessageContentPartitioner createPartitioner(List<String> 
assignPartitions) {
+        MessageContentPartitioner partitioner = new 
MessageContentPartitioner();
+        Map<String, Object> configs = new HashMap<>();
+        configs.put(MessageContentPartitioner.ASSIGN_PARTITIONS_CONFIG, 
assignPartitions);
+        partitioner.configure(configs);
+        return partitioner;
+    }
+
+    @Test
+    void testCrossInstanceIsolation() {
+        // Two partitioners with different assign_partitions should not 
interfere
+        MessageContentPartitioner partitionerA =
+                createPartitioner(Arrays.asList("order", "payment"));
+        MessageContentPartitioner partitionerB =
+                createPartitioner(Arrays.asList("user", "product"));
+
+        Cluster cluster = createCluster(4);
+
+        // Partitioner A: "order" message should route to partition 0
+        int partitionA =
+                partitionerA.partition(
+                        TOPIC, null, null, "order-123", 
"order-123".getBytes(), cluster);
+        Assertions.assertEquals(0, partitionA);
+
+        // Partitioner B: "user" message should route to partition 0 (its own 
assign list)
+        int partitionB =
+                partitionerB.partition(
+                        TOPIC, null, null, "user-456", "user-456".getBytes(), 
cluster);
+        Assertions.assertEquals(0, partitionB);
+
+        // Partitioner A: "user" is NOT in A's list, should fall through to 
hash partition
+        int partitionAForUser =
+                partitionerA.partition(
+                        TOPIC, null, null, "user-456", "user-456".getBytes(), 
cluster);
+        // Must be in the remaining range [2, 3], not in assigned range [0, 1]
+        Assertions.assertTrue(
+                partitionAForUser >= 2 && partitionAForUser <= 3,
+                "Partitioner A should not route 'user' to assigned partitions 
0-1");
+    }
+
+    @Test
+    void testAssignedPartitionRouting() {
+        MessageContentPartitioner partitioner = 
createPartitioner(Arrays.asList("alpha", "beta"));
+        Cluster cluster = createCluster(4);
+
+        // "alpha" -> partition 0
+        Assertions.assertEquals(
+                0, partitioner.partition(TOPIC, null, null, "alpha", 
"alpha".getBytes(), cluster));
+
+        // "beta" -> partition 1
+        Assertions.assertEquals(
+                1, partitioner.partition(TOPIC, null, null, "beta", 
"beta".getBytes(), cluster));
+
+        // "contains alpha" -> partition 0 (contains match)
+        Assertions.assertEquals(
+                0,
+                partitioner.partition(
+                        TOPIC, null, null, "contains alpha", "contains 
alpha".getBytes(), cluster));
+    }
+
+    @Test
+    void testUnassignedMessageFallbackToHash() {
+        MessageContentPartitioner partitioner =
+                createPartitioner(Collections.singletonList("reserved"));
+        Cluster cluster = createCluster(4);
+
+        // "other" is not in assign list, should hash to partition [1, 3]
+        int partition =
+                partitioner.partition(TOPIC, null, null, "other", 
"other".getBytes(), cluster);
+        Assertions.assertTrue(
+                partition >= 1 && partition <= 3,
+                "Unassigned message should fall back to hash partition in 
range [1, 3]");
+    }
+}

Reply via email to