[runtime] Preserve typed Flink key identity in action state - #1100
[runtime] Preserve typed Flink key identity in action state#1100rob-9 wants to merge 13 commits into
Conversation
…action name Action.hashCode() folds in JavaFunction's Class[] parameterTypes, and Class.hashCode() is the per-JVM identity hash, so every durable-state key changes across a process restart and recovery lookups can never hit. Kill/restore trials: 0/134 replays before this fix; 90/90 with 0% divergence after (non-deterministic strategy, Kafka action-state store).
wenjin272
left a comment
There was a problem hiding this comment.
Thanks for working on this and for adding the key-isolation coverage! I left two suggestions to simplify the state-key format and avoid rejecting valid recovery.
| DataOutputSerializer output = new DataOutputSerializer(128); | ||
| try { | ||
| TypeSerializerSnapshotSerializationUtil.writeSerializerSnapshot( | ||
| output, keySerializer.snapshotConfiguration()); |
There was a problem hiding this comment.
Could we remove the serializer snapshot fingerprint from both the state key and recovery validation? Flink’s PojoSerializer snapshots include a runtime subclass cache, so an unchanged job can produce a different fingerprint after checkpoint recovery even though the key bytes remain identical. I reproduced this with a POJO subclass: Flink keyed state restored successfully, but Kafka action-state recovery failed. For this PR, documenting that changes to key types or serializer configurations are unsupported during recovery seems sufficient.
There was a problem hiding this comment.
removed. I added a Pojo recovery test that fails before this fix, and documented that key type and serializer config changes are unsupported during recovery.
| .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) | ||
| .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) | ||
| .build(); | ||
| private static final String KEY_SEPARATOR = "_"; |
There was a problem hiding this comment.
Could we also remove the v2 prefix and keep a single state-key format? Since we’re still in 0.x and don’t need backward compatibility for existing action-state records, the version marker adds unnecessary complexity and may suggest a compatibility mechanism we don’t intend to provide.
Closes #1099.
This branch is stacked on PR #1094, which makes the action identifier stable across JVM restarts. The two changes fix separate parts of the action-state key.
Purpose of change
Flink Agents stores action progress under the current key so recovery can find completed work and avoid repeating its side effects. The existing format turns that key into text. Different typed keys with the same text, such as
Long(1)andString("1"), can therefore point to the same action state. A lookup or prune for one key can remove the other key's completed state, allowing recovery to run that action again.This change gives each typed key a durable identity derived from its serialized bytes. Kafka and Fluss use that identity consistently for writes, lookups, cleanup, partitioning, and recovery.
Runtime flow
ActionStateKeyEncoderserializes the typed key and hashes the bytes with SHA-256. The state key contains that digest, the key-group, sequence number, event ID, and action ID.Key decisions
Related work
Issue #1034 manages the Kafka offset boundary for deleting an older log prefix, while this PR defines the identity of each record. PR #885 manages Kafka tombstones. When those changes are combined, replay must validate the record key before applying a value or tombstone, and pruning must derive the same typed identity.
Behavioral Semantics
Interaction decisions
Behavioral contracts
Long(1),String("1"), and custom keys sharing a string representation receive separate identities, including at maximum parallelism1.Failure behavior
API
The supported user-facing API and configuration remain unchanged. The stores, key utilities, and Kafka partitioner now carry explicit
@Internalannotations. Their construction path receives anActionStateKeyEncoder, so direct callers of these implementation classes must update their constructor calls.The durable record format changes incompatibly. Existing action-state records are unsupported; use a fresh Kafka topic or Fluss table and start without an older checkpoint or savepoint when upgrading. For subsequent recovery, preserve the key type and serializer configuration. Flink may accept a serializer change that produces different key bytes; the action-state store does not detect that change and may miss completed work.
Documentation
doc-neededdoc-not-neededdoc-includedThe deployment and configuration documentation explains the format transition, serializer compatibility, backend isolation, and digest security boundary.
Was this patch authored or co-authored using generative AI tooling?