CodeTrainerMan commented on issue #1084:
URL: https://github.com/apache/flink-agents/issues/1084#issuecomment-5767599098

   Thanks for the design work here. I read the latest proposal against 
`ActionStateUtil` on `main` and found three things that I think need settling 
before any code lands. I am not claiming this issue -- just hoping to save the 
implementer a round trip.
   
   **1. `generateKey()` has to stay a pure function; a self-incrementing 
counter inside it breaks that.**
   
   The proposal tracks "the number of times `generateKey()` is called for this 
tuple" and appends it. But `generateKey()` is called from *both* directions of 
the store, not only when a new state is written:
   
   - `KafkaActionStateStore.put()` (L160) and `.get()` (L178)
   - `FlussActionStateStore.put()` (L215) and `.get()` (L238)
   
   `ActionExecutionOperator.processActionTaskForKey()` calls 
`durableExecManager.maybeGetActionState(...)` (L513-515) *before* deciding 
whether to execute, and only writes afterwards. If the ordinal is bumped inside 
`generateKey()`, the lookup that misses consumes ordinal N and the following 
write gets N+1, so the state lands under a key that no subsequent `get()` can 
derive. Durable execution would silently stop hitting its own cache, and the 
symptom is "state never found" rather than an error.
   
   The ordinal therefore needs to be passed *into* `generateKey()` by the 
caller (or folded into the event identity), keeping the method deterministic 
and idempotent the way it is today.
   
   **2. A sixth segment makes every already-persisted action state 
unrecoverable.**
   
   The layout is fixed at `KEY_SEGMENT_COUNT = 5`, and `isKeyRetained()` 
hard-fails rather than skips on any key it cannot attribute:
   
   ```java
   String[] parts = splitValidatedKey(stateKey);
   if (parts == null) {
       throw new IllegalStateException(
               "Malformed action-state key during recovery: expected five 
fields. Key: " + ...);
   }
   ```
   
   `isKeyRetained()` runs over every record during recovery -- 
`KafkaActionStateStore.poll()` (L277) and `FlussActionStateStore` (L455). That 
throwing behavior is pinned by 
`ActionStateUtilTest.testIsKeyRetainedRejectsUnrecognizedFormatKeys`, which 
asserts that even a bare `"malformed-key"` throws. This differs from 
`matchesBusinessKeyIdentity*`, which return `false` under the documented "never 
prune what cannot be attributed" rule; `isKeyRetained()` has no such fallback.
   
   So a five-to-six segment change turns every pre-existing Kafka/Fluss action 
state, and anything in an older savepoint, into a hard recovery failure on 
upgrade -- the opposite of what durable execution exists for. Since Kafka/Fluss 
are external stores, there is no serializer-snapshot upgrade path to lean on 
either.
   
   A format-preserving option: keep five segments and fold the occurrence 
ordinal into the event segment's derivation, e.g. 
`UUID.nameUUIDFromBytes(attributesBytes + ordinal)`, with the ordinal supplied 
by the caller. Old keys stay parseable and simply behave as the first 
occurrence.
   
   **3. The counter re-introduces the collision on recovery -- @wenjin272's 
point (2) in another form.**
   
   An ordinal derived from call order is still an order-dependent resource. On 
recovery, a sibling whose state is already completed takes the skip-and-replay 
branch at `ActionExecutionOperator` L518 (`actionState != null && 
actionState.isCompleted()`): it only calls `get()`, never `put()`. A sibling 
that had *not* completed must re-execute and `put()`. The sequence in which 
ordinals are consumed therefore differs from the original run, and a 
re-executed sibling can land on the ordinal that belonged to its completed 
sibling -- look up that sibling's completed state and replay its output. That 
is precisely the bug this issue is trying to fix, arriving through the new 
mechanism.
   
   This is why the lineage-based identity sketched on Sep 14 (which @wenjin272 
said looked reasonable) seems the safer direction: identity derived from 
*where* an event sits in the causal structure, carried with the pending event 
and checkpointed with it, rather than from *when* a call happens.
   
   Happy to be corrected if I have misread the recovery path.


-- 
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