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

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


The following commit(s) were added to refs/heads/master by this push:
     new c8453bb52e7 [FLINK-40796][state-processor-api] Reject key types that 
cannot be hashed reliably
c8453bb52e7 is described below

commit c8453bb52e7f46e4453f93ca7ee0f7a34a075215
Author: SEPURI-SAI-KRISHNA <[email protected]>
AuthorDate: Thu Sep 24 22:51:37 2026 +0530

    [FLINK-40796][state-processor-api] Reject key types that cannot be hashed 
reliably
    
    Generated-by: Claude Code (Opus 5)
---
 .../state/api/StateBootstrapTransformation.java    |  4 +
 .../api/StateBootstrapTransformationTest.java      | 60 +++++++++++++
 .../streaming/api/datastream/KeyedStream.java      | 98 +---------------------
 .../flink/streaming/util/keys/KeySelectorUtil.java | 96 +++++++++++++++++++++
 4 files changed, 162 insertions(+), 96 deletions(-)

diff --git 
a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/StateBootstrapTransformation.java
 
b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/StateBootstrapTransformation.java
index e5f9b853ff6..1302e7e86e5 100644
--- 
a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/StateBootstrapTransformation.java
+++ 
b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/StateBootstrapTransformation.java
@@ -43,6 +43,7 @@ import 
org.apache.flink.streaming.api.operators.StreamOperator;
 import org.apache.flink.streaming.api.operators.StreamOperatorFactory;
 import org.apache.flink.streaming.api.transformations.PartitionTransformation;
 import 
org.apache.flink.streaming.runtime.partitioner.KeyGroupStreamPartitioner;
+import org.apache.flink.streaming.util.keys.KeySelectorUtil;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -185,6 +186,9 @@ public class StateBootstrapTransformation<T> {
         // the nested bootstrapped operator needs.
         DataStream<T> input = stream;
         if (keySelector != null) {
+            // keyBy() used to reject key types that cannot be hashed 
reliably; keep that check
+            // here now that the partitioning is built directly.
+            KeySelectorUtil.validateKeyType(keyType);
             input =
                     new DataStream<>(
                             stream.getExecutionEnvironment(),
diff --git 
a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/StateBootstrapTransformationTest.java
 
b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/StateBootstrapTransformationTest.java
index 3935a493b85..ea635db264d 100644
--- 
a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/StateBootstrapTransformationTest.java
+++ 
b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/StateBootstrapTransformationTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.flink.state.api;
 
+import org.apache.flink.api.common.InvalidProgramException;
 import org.apache.flink.api.java.functions.KeySelector;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.core.fs.Path;
@@ -36,6 +37,8 @@ import org.apache.flink.test.util.AbstractTestBase;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Tests for bootstrap transformations. */
 class StateBootstrapTransformationTest extends AbstractTestBase {
@@ -164,6 +167,48 @@ class StateBootstrapTransformationTest extends 
AbstractTestBase {
                 .isEqualTo(CustomKeySelector.class);
     }
 
+    @Test
+    void testUnhashableKeyTypeIsRejected() {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        DataStream<byte[]> input = env.fromData(new byte[] {1});
+
+        StateBootstrapTransformation<byte[]> transformation =
+                OperatorTransformation.bootstrapWith(input)
+                        .keyBy(new ArrayKeySelector())
+                        .transform(new 
ExampleArrayKeyedStateBootstrapFunction());
+
+        assertThatThrownBy(
+                        () ->
+                                transformation.writeOperatorSubtaskStates(
+                                        OperatorIdentifier.forUid("uid"),
+                                        new HashMapStateBackend(),
+                                        new Path(),
+                                        transformation.getMaxParallelism(4)))
+                .as("An array key cannot be hashed reliably and must be 
rejected")
+                .isInstanceOf(InvalidProgramException.class)
+                .hasMessageContaining("cannot be used as key");
+    }
+
+    @Test
+    void testHashableKeyTypeIsAccepted() {
+        StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
+        DataStream<String> input = env.fromData("");
+
+        StateBootstrapTransformation<String> transformation =
+                OperatorTransformation.bootstrapWith(input)
+                        .keyBy(new CustomKeySelector())
+                        .transform(new ExampleKeyedStateBootstrapFunction());
+
+        assertThatNoException()
+                .isThrownBy(
+                        () ->
+                                transformation.writeOperatorSubtaskStates(
+                                        OperatorIdentifier.forUid("uid"),
+                                        new HashMapStateBackend(),
+                                        new Path(),
+                                        transformation.getMaxParallelism(4)));
+    }
+
     private static class CustomKeySelector implements KeySelector<String, 
String> {
 
         @Override
@@ -197,4 +242,19 @@ class StateBootstrapTransformationTest extends 
AbstractTestBase {
         @Override
         public void processElement(String value, Context ctx) throws Exception 
{}
     }
+
+    private static class ArrayKeySelector implements KeySelector<byte[], 
byte[]> {
+
+        @Override
+        public byte[] getKey(byte[] value) throws Exception {
+            return value;
+        }
+    }
+
+    private static class ExampleArrayKeyedStateBootstrapFunction
+            extends KeyedStateBootstrapFunction<byte[], byte[]> {
+
+        @Override
+        public void processElement(byte[] value, Context ctx) throws Exception 
{}
+    }
 }
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/streaming/api/datastream/KeyedStream.java
 
b/flink-runtime/src/main/java/org/apache/flink/streaming/api/datastream/KeyedStream.java
index e99f390276e..b49a74c6c28 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/streaming/api/datastream/KeyedStream.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/streaming/api/datastream/KeyedStream.java
@@ -21,19 +21,12 @@ import org.apache.flink.annotation.Experimental;
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.annotation.Public;
 import org.apache.flink.annotation.PublicEvolving;
-import org.apache.flink.api.common.InvalidProgramException;
 import org.apache.flink.api.common.functions.FlatMapFunction;
 import org.apache.flink.api.common.functions.ReduceFunction;
 import org.apache.flink.api.common.state.ReducingStateDescriptor;
 import org.apache.flink.api.common.state.ValueStateDescriptor;
-import org.apache.flink.api.common.typeinfo.BasicArrayTypeInfo;
-import org.apache.flink.api.common.typeinfo.PrimitiveArrayTypeInfo;
 import org.apache.flink.api.common.typeinfo.TypeInformation;
 import org.apache.flink.api.java.functions.KeySelector;
-import org.apache.flink.api.java.typeutils.EnumTypeInfo;
-import org.apache.flink.api.java.typeutils.ObjectArrayTypeInfo;
-import org.apache.flink.api.java.typeutils.PojoTypeInfo;
-import org.apache.flink.api.java.typeutils.TupleTypeInfoBase;
 import org.apache.flink.api.java.typeutils.TypeExtractor;
 import 
org.apache.flink.runtime.asyncprocessing.operators.AsyncKeyedProcessOperator;
 import org.apache.flink.runtime.asyncprocessing.operators.AsyncStreamFlatMap;
@@ -64,16 +57,12 @@ import 
org.apache.flink.streaming.api.windowing.windows.GlobalWindow;
 import org.apache.flink.streaming.api.windowing.windows.Window;
 import 
org.apache.flink.streaming.runtime.partitioner.KeyGroupStreamPartitioner;
 import org.apache.flink.streaming.runtime.partitioner.StreamPartitioner;
+import org.apache.flink.streaming.util.keys.KeySelectorUtil;
 import org.apache.flink.util.OutputTag;
 import org.apache.flink.util.Preconditions;
 import org.apache.flink.util.Utils;
 
-import org.apache.commons.lang3.StringUtils;
-
 import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Stack;
 import java.util.UUID;
 
 import static org.apache.flink.util.Preconditions.checkNotNull;
@@ -160,90 +149,7 @@ public class KeyedStream<T, KEY> extends DataStream<T> {
 
         super(stream.getExecutionEnvironment(), partitionTransformation);
         this.keySelector = clean(keySelector);
-        this.keyType = validateKeyType(keyType);
-    }
-
-    /**
-     * Validates that a given type of element (as encoded by the provided 
{@link TypeInformation})
-     * can be used as a key in the {@code DataStream.keyBy()} operation. This 
is done by searching
-     * depth-first the key type and checking if each of the composite types 
satisfies the required
-     * conditions (see {@link #validateKeyTypeIsHashable(TypeInformation)}).
-     *
-     * @param keyType The {@link TypeInformation} of the key.
-     */
-    @SuppressWarnings("rawtypes")
-    private TypeInformation<KEY> validateKeyType(TypeInformation<KEY> keyType) 
{
-        Stack<TypeInformation<?>> stack = new Stack<>();
-        stack.push(keyType);
-
-        List<TypeInformation<?>> unsupportedTypes = new ArrayList<>();
-
-        while (!stack.isEmpty()) {
-            TypeInformation<?> typeInfo = stack.pop();
-
-            if (!validateKeyTypeIsHashable(typeInfo)) {
-                unsupportedTypes.add(typeInfo);
-            }
-
-            if (typeInfo instanceof TupleTypeInfoBase) {
-                for (int i = 0; i < typeInfo.getArity(); i++) {
-                    stack.push(((TupleTypeInfoBase) typeInfo).getTypeAt(i));
-                }
-            }
-        }
-
-        if (!unsupportedTypes.isEmpty()) {
-            throw new InvalidProgramException(
-                    "Type "
-                            + keyType
-                            + " cannot be used as key. Contained "
-                            + "UNSUPPORTED key types: "
-                            + StringUtils.join(unsupportedTypes, ", ")
-                            + ". Look "
-                            + "at the keyBy() documentation for the conditions 
a type has to satisfy in order to be "
-                            + "eligible for a key.");
-        }
-
-        return keyType;
-    }
-
-    /**
-     * Validates that a given type of element (as encoded by the provided 
{@link TypeInformation})
-     * can be used as a key in the {@code DataStream.keyBy()} operation.
-     *
-     * @param type The {@link TypeInformation} of the type to check.
-     * @return {@code false} if:
-     *     <ol>
-     *       <li>it is a POJO type but does not override the {@link 
#hashCode()} method and relies
-     *           on the {@link Object#hashCode()} implementation.
-     *       <li>it is an array of any type (see {@link 
PrimitiveArrayTypeInfo}, {@link
-     *           BasicArrayTypeInfo}, {@link ObjectArrayTypeInfo}).
-     *       <li>it is enum type
-     *     </ol>
-     *     , {@code true} otherwise.
-     */
-    private boolean validateKeyTypeIsHashable(TypeInformation<?> type) {
-        try {
-            return (type instanceof PojoTypeInfo)
-                    ? !type.getTypeClass()
-                            .getMethod("hashCode")
-                            .getDeclaringClass()
-                            .equals(Object.class)
-                    : !(isArrayType(type) || isEnumType(type));
-        } catch (NoSuchMethodException ignored) {
-            // this should never happen as we are just searching for the 
hashCode() method.
-        }
-        return false;
-    }
-
-    private static boolean isArrayType(TypeInformation<?> type) {
-        return type instanceof PrimitiveArrayTypeInfo
-                || type instanceof BasicArrayTypeInfo
-                || type instanceof ObjectArrayTypeInfo;
-    }
-
-    private static boolean isEnumType(TypeInformation<?> type) {
-        return type instanceof EnumTypeInfo;
+        this.keyType = KeySelectorUtil.validateKeyType(keyType);
     }
 
     // ------------------------------------------------------------------------
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/streaming/util/keys/KeySelectorUtil.java
 
b/flink-runtime/src/main/java/org/apache/flink/streaming/util/keys/KeySelectorUtil.java
index 2fda4ade164..9aeb23e41f3 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/streaming/util/keys/KeySelectorUtil.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/streaming/util/keys/KeySelectorUtil.java
@@ -19,6 +19,7 @@ package org.apache.flink.streaming.util.keys;
 
 import org.apache.flink.annotation.Internal;
 import org.apache.flink.api.common.ExecutionConfig;
+import org.apache.flink.api.common.InvalidProgramException;
 import org.apache.flink.api.common.functions.InvalidTypesException;
 import org.apache.flink.api.common.functions.Partitioner;
 import org.apache.flink.api.common.operators.Keys;
@@ -29,11 +30,20 @@ import org.apache.flink.api.common.typeutils.CompositeType;
 import org.apache.flink.api.common.typeutils.TypeComparator;
 import org.apache.flink.api.java.functions.KeySelector;
 import org.apache.flink.api.java.tuple.Tuple;
+import org.apache.flink.api.java.typeutils.EnumTypeInfo;
+import org.apache.flink.api.java.typeutils.ObjectArrayTypeInfo;
+import org.apache.flink.api.java.typeutils.PojoTypeInfo;
 import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
 import org.apache.flink.api.java.typeutils.TupleTypeInfo;
+import org.apache.flink.api.java.typeutils.TupleTypeInfoBase;
+
+import org.apache.commons.lang3.StringUtils;
 
 import java.lang.reflect.Array;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Stack;
 
 import static java.util.Objects.requireNonNull;
 
@@ -117,6 +127,92 @@ public final class KeySelectorUtil {
         return new OneKeySelector<>(comparator);
     }
 
+    /**
+     * Validates that a given type of element (as encoded by the provided 
{@link TypeInformation})
+     * can be used as a key. This is done by searching depth-first the key 
type and checking if each
+     * of the composite types satisfies the required conditions (see {@link
+     * #isKeyTypeHashable(TypeInformation)}).
+     *
+     * @param keyType The {@link TypeInformation} of the key.
+     * @return The validated key type.
+     * @throws InvalidProgramException If the key type, or one of its 
contained types, cannot be
+     *     used as a key.
+     */
+    @SuppressWarnings("rawtypes")
+    public static <KEY> TypeInformation<KEY> 
validateKeyType(TypeInformation<KEY> keyType) {
+        Stack<TypeInformation<?>> stack = new Stack<>();
+        stack.push(keyType);
+
+        List<TypeInformation<?>> unsupportedTypes = new ArrayList<>();
+
+        while (!stack.isEmpty()) {
+            TypeInformation<?> typeInfo = stack.pop();
+
+            if (!isKeyTypeHashable(typeInfo)) {
+                unsupportedTypes.add(typeInfo);
+            }
+
+            if (typeInfo instanceof TupleTypeInfoBase) {
+                for (int i = 0; i < typeInfo.getArity(); i++) {
+                    stack.push(((TupleTypeInfoBase) typeInfo).getTypeAt(i));
+                }
+            }
+        }
+
+        if (!unsupportedTypes.isEmpty()) {
+            throw new InvalidProgramException(
+                    "Type "
+                            + keyType
+                            + " cannot be used as key. Contained "
+                            + "UNSUPPORTED key types: "
+                            + StringUtils.join(unsupportedTypes, ", ")
+                            + ". Look "
+                            + "at the keyBy() documentation for the conditions 
a type has to satisfy in order to be "
+                            + "eligible for a key.");
+        }
+
+        return keyType;
+    }
+
+    /**
+     * Checks that a given type of element (as encoded by the provided {@link 
TypeInformation}) can
+     * be used as a key.
+     *
+     * @param type The {@link TypeInformation} of the type to check.
+     * @return {@code false} if:
+     *     <ol>
+     *       <li>it is a POJO type but does not override the {@link 
Object#hashCode()} method and
+     *           relies on the {@link Object#hashCode()} implementation.
+     *       <li>it is an array of any type (see {@link 
PrimitiveArrayTypeInfo}, {@link
+     *           BasicArrayTypeInfo}, {@link ObjectArrayTypeInfo}).
+     *       <li>it is enum type
+     *     </ol>
+     *     , {@code true} otherwise.
+     */
+    private static boolean isKeyTypeHashable(TypeInformation<?> type) {
+        try {
+            return (type instanceof PojoTypeInfo)
+                    ? !type.getTypeClass()
+                            .getMethod("hashCode")
+                            .getDeclaringClass()
+                            .equals(Object.class)
+                    : !(isArrayType(type) || isEnumType(type));
+        } catch (NoSuchMethodException ignored) {
+            // this should never happen as we are just searching for the 
hashCode() method.
+        }
+        return false;
+    }
+
+    private static boolean isArrayType(TypeInformation<?> type) {
+        return type instanceof PrimitiveArrayTypeInfo
+                || type instanceof BasicArrayTypeInfo
+                || type instanceof ObjectArrayTypeInfo;
+    }
+
+    private static boolean isEnumType(TypeInformation<?> type) {
+        return type instanceof EnumTypeInfo;
+    }
+
     // ------------------------------------------------------------------------
 
     /** Private constructor to prevent instantiation. */

Reply via email to