wenjin272 commented on code in PR #1024:
URL: https://github.com/apache/flink-agents/pull/1024#discussion_r3842891090


##########
runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStore.java:
##########
@@ -105,12 +106,20 @@ public class FlussActionStateStore implements 
ActionStateStore {
     /** In-memory cache for O(1) state lookups; rebuilt from Fluss log on 
recovery. */
     private final Map<String, ActionState> actionStates;
 
+    // When set, only records whose key-group is accepted by this predicate 
are kept in the
+    // in-memory cache during rebuildState; null means retain all keys 
(default).
+    private IntPredicate ownershipFilter;
+
+    // The operator's maximum parallelism, used to compute key-groups 
consistently with Flink.
+    private int maxParallelism;

Review Comment:
   `maxParallelism` remains `0` when the public `FlussActionStateStore(config)` 
constructor is used, and the Kafka constructor has the same issue. Any direct 
`put()` or `get()` then fails in `generateKey()` because it requires a positive 
value. I reproduced this by running `FlussActionStateStoreIT` explicitly: 10 of 
its 11 tests fail with `maxParallelism must be positive but was 0`. Since API 
compatibility is not required during beta, could we make `maxParallelism` a 
required constructor argument and final, rather than relying on a later mutable 
setter?



##########
runtime/src/main/java/org/apache/flink/agents/runtime/actionstate/ActionStateUtil.java:
##########
@@ -58,10 +83,110 @@ public static String generateKey(
     public static List<String> parseKey(String key) {
         Preconditions.checkNotNull(key, "key cannot be null.");
         String[] parts = key.split(KEY_SEPARATOR);
-        Preconditions.checkArgument(parts.length == 4, "Invalid key format.");
+        Preconditions.checkArgument(parts.length == KEY_SEGMENT_COUNT, 
"Invalid key format.");
         return List.of(parts);
     }
 
+    /**
+     * Extracts the key-group from a composite state key. The key-group is the 
first segment and was
+     * computed from the original typed key via {@link 
KeyGroupRangeAssignment#assignToKeyGroup}.
+     * Rejects keys without the expected segment layout, including keys 
written in the pre-key-group
+     * 4-segment format.
+     */
+    public static int parseKeyGroup(String key) {
+        Preconditions.checkNotNull(key, "key cannot be null.");
+        String[] parts = key.split(KEY_SEPARATOR);
+        Preconditions.checkArgument(parts.length == KEY_SEGMENT_COUNT, 
"Invalid key format.");
+        return Integer.parseInt(parts[KEY_GROUP_SEGMENT]);
+    }
+
+    /**
+     * Returns {@code true} when {@code stateKey} has the expected segment 
layout and its
+     * business-key segment equals {@code businessKey}. Comparison is 
segment-exact; substring
+     * matching is deliberately avoided because a numeric business key can 
collide with another
+     * record's sequence-number segment.
+     */
+    public static boolean matchesBusinessKey(String stateKey, Object 
businessKey) {
+        String[] parts = stateKey.split(KEY_SEPARATOR);
+        return parts.length == KEY_SEGMENT_COUNT
+                && parts[BUSINESS_KEY_SEGMENT].equals(businessKey.toString());
+    }
+
+    /** Like {@link #matchesBusinessKey} with an additional exact 
sequence-number segment match. */
+    public static boolean matchesBusinessKeyAndSeqNum(
+            String stateKey, Object businessKey, long seqNum) {
+        String[] parts = stateKey.split(KEY_SEPARATOR);
+        return parts.length == KEY_SEGMENT_COUNT
+                && parts[BUSINESS_KEY_SEGMENT].equals(businessKey.toString())
+                && parts[SEQ_NUM_SEGMENT].equals(String.valueOf(seqNum));
+    }
+
+    /**
+     * Like {@link #matchesBusinessKey} with an additional predicate over the 
parsed sequence-number
+     * segment. Returns {@code false} for keys that cannot be attributed 
(malformed layout or
+     * unparsable sequence number): never prune what cannot be attributed.
+     */
+    public static boolean matchesBusinessKeyWithSeqNum(
+            String stateKey, Object businessKey, LongPredicate seqNumFilter) {
+        String[] parts = stateKey.split(KEY_SEPARATOR);
+        if (parts.length != KEY_SEGMENT_COUNT
+                || 
!parts[BUSINESS_KEY_SEGMENT].equals(businessKey.toString())) {
+            return false;
+        }
+        try {
+            return seqNumFilter.test(Long.parseLong(parts[SEQ_NUM_SEGMENT]));
+        } catch (NumberFormatException e) {
+            LOG.warn("Failed to parse sequence number from state key: {}", 
stateKey);
+            return false;
+        }
+    }
+
+    /**
+     * Returns {@code true} if the composite {@code stateKey}'s key-group is 
accepted by the given
+     * ownership filter. A {@code null} filter retains every key (the default 
for in-memory and test
+     * backends).
+     *
+     * <p>Keys without the expected 5-segment layout — including records 
written in the
+     * pre-key-group 4-segment format — have UNKNOWN ownership: they cannot be 
attributed to a
+     * key-group, so they are retained in every subtask rather than dropped. 
This preserves durable
+     * state across a key-group upgrade at the cost of a bounded, one-time 
memory amplification for
+     * the legacy recovery tail, which ages out once a new checkpoint marker 
advances past those
+     * records. Lookups still find such records via {@link #legacyKeyOf}. A 
5-segment key whose
+     * key-group segment fails to parse is likewise retained as a fail-safe.
+     */
+    public static boolean isKeyRetained(@Nullable IntPredicate 
ownershipFilter, String stateKey) {
+        if (ownershipFilter == null) {
+            return true;
+        }
+        String[] parts = stateKey.split(KEY_SEPARATOR);

Review Comment:
   Could we avoid using the number of underscore-separated segments to parse 
the state key? `businessKey` is inserted without escaping, so a key such as 
`tenant_user` produces more than five segments and is treated as UNKNOWN, 
causing every subtask to retain it and leaving the orphan-state leak unfixed. 
Since the project is still beta and previous ActionState data does not need to 
be preserved, I suggest defining a single new, unambiguous key format and 
removing the legacy compatibility logic entirely. For example, the business key 
could be length-prefixed, or the fixed fields could be parsed from both ends.



##########
runtime/src/test/java/org/apache/flink/agents/runtime/actionstate/FlussActionStateStoreIT.java:
##########
@@ -206,6 +206,44 @@ void testRebuildStateWithRecoveryMarkers() throws 
Exception {
         }
     }
 
+    /**
+     * Reproduces the orphan-state leak fix: after recovery, a subtask must 
keep only the keys it
+     * owns and drop keys owned by other subtasks. Here "A" is owned and "B" 
is foreign, so the
+     * rebuilt cache must contain "A" but not "B".
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    void testRebuildStateFiltersForeignKeys() throws Exception {

Review Comment:
   This regression test is not executed by the current CI. The class name ends 
with `IT`, while the repository runs ordinary `mvn test` with Surefire default 
discovery patterns, which do not include `*IT`. Could we either rename this 
class to something ending in `Test`, such as 
`FlussActionStateStoreIntegrationTest`, or explicitly wire `*IT` tests into 
Surefire/Failsafe and CI? Once enabled, the `maxParallelism=0` failure above 
will also need to be fixed.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to