Repository navigation
[FLINK-40932][state] Arm RowData state schema evolution for list state elements and map state values - #29449
Draft
weiqingy wants to merge 3 commits into
Draft
[FLINK-40932][state] Arm RowData state schema evolution for list state elements and map state values#29449weiqingy wants to merge 3 commits into
weiqingy wants to merge 3 commits into
Conversation
…Data Make RowData state survive a backward-compatible schema change behind table.exec.state.schema-evolution.enabled, default false. Compatibility becomes name based when the flag is armed: added fields must be nullable, removed fields and changed leaf types are rejected, and nested ROW fields are validated by recursion. A prior snapshot carrying no field names falls back to positional mapping bounded to an append-only change, since without names an insertion is indistinguishable from a retype. The flag is in-memory and never persisted. It is set only on a state's own value serializer, and only on a keyed backend that performs object-level value migration, so a serializer can carry it only where migrate is actually invoked. A RowData serializer nested below a composite serializer, the heap and batch backends, and operator and broadcast state all leave it off and reject the restore rather than accepting a migration that nobody performs. Generated-by: Claude Code (Opus 5)
…cover direct backend arming A nested ROW field pair skipped the exact type check so its contents could be compared by the nested snapshot recursion. That recursion compares the ROW's fields and never the ROW's own nullability, so narrowing a nullable ROW to NOT NULL was accepted and migration then carried an existing null straight into the non-null field. Settle nullability before delegating, which matches how a leaf field is treated, since LogicalType#equals compares nullability too. Arming a descriptor registered directly through AbstractKeyedStateBackend#getOrCreateKeyedState had no coverage: the existing tests drive DefaultKeyedStateStore and StreamingRuntimeContext, and their fake backend overrides getOrCreateKeyedState. Window and async operators reach that method directly. Cover both capability values against a real AbstractKeyedStateBackend, which the mock backend now reports through an opt-in builder flag. Generated-by: Claude Code (Opus 5)
…e elements and map state values Extend the arming seam by exactly one structural level: a list state's element serializer and a map state's value serializer, never a map key. This matches what the RocksDB list and map states unwrap before they call migrate, so a serializer is still armed only where migrate is invoked on it. Arming is decided by state kind rather than serializer shape. RocksDB migrates value, reducing and aggregating states through the top-level serializer, so a ValueState<List<RowData>> must not have its element armed even though its type information matches a ListState<RowData>. The arming factory therefore takes the descriptor's StateDescriptor.Type, and those kinds keep arming the top-level serializer only. Descending into list and map serializers removes the structural reason operator and broadcast state could never hold an armed serializer. A state descriptor caches its serializer per instance, so one registered as keyed state first would carry the armed serializer into a backend that accepts compatibleAfterMigration and never migrates. The operator state backend now rejects an armed serializer, using an isArmed check that performs the same one-level descent as arming. Generated-by: Claude Code (Opus 5)
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the PR-3b step of the FLIP-527 implementation, split into a stack of small, independently reviewable PRs under the umbrella issue FLINK-37732. Landing order:
migratehook onTypeSerializerSnapshot(merged)RowData(#28973, in review)ListState<RowData>andMapState<K, RowData>This PR is stacked on #28973 and stays a draft until it merges. Until then the diff below includes PR-3's two commits; only the last commit belongs to this step.
What is the purpose of the change
PR-3 arms schema evolution only on a state's own value serializer, so
ListState<RowData>andMapState<K, RowData>still reject any schema change on restore. This PR extends the arming seam by exactly one structural level: a list state's element serializer and a map state's value serializer. That matches what the RocksDB list and map states already unwrap before they callmigrate, so the invariant from PR-3 still holds: a serializer is armed only if some backend will invokemigrateon that exact serializer.Three constraints shape the change.
Arming is decided by state kind, not by serializer shape. RocksDB chooses its migration routine by state kind: list and map states descend to the element or value, while value, reducing and aggregating states call
migrateon the top-level serializer. AValueState<List<RowData>>has the sameListTypeInfoas aListState<RowData>, so a shape-based descent would arm a serializer that nothing migrates and reintroduce silent corruption.StateSchemaEvolvingSerializer.armingtherefore takes theStateDescriptor.Type, and value, reducing and aggregating states keep exactly PR-3's behavior.The descent stops at one level, and never reaches a map key. A nested
compatibleAfterMigrationpropagates up throughCompositeTypeSerializerSnapshot, so arming two levels down would make the top-level verdictcompatibleAfterMigrationwhile the onlymigratecall lands one level up, on a snapshot that does not override it. Shapes such as the interval join'sMapState<Long, List<Tuple2<RowData, Boolean>>>stay unarmed and fail closed. Map keys are never armed because RocksDB requires the map key to becompatibleAsIs.Operator and broadcast state now reject an armed serializer. In PR-3 they were excluded partly because their own serializers are a
ListSerializerorMapSerializer, which this PR now descends into. AStateDescriptorcaches its serializer per instance, so a descriptor first registered as keyed state on RocksDB and then reused for operator or broadcast state would carry the armed serializer into a backend that acceptscompatibleAfterMigrationand never migrates.DefaultOperatorStateBackendnow throws aStateMigrationExceptionfor an armed serializer, using a staticisArmedcheck that performs the same one-level descent as arming. A job that does not opt in cannot reach the check.Brief change log
StateSchemaEvolvingSerializer: kind-awarearming(SerializerFactory, StateDescriptor.Type), one-level descent to a list element or map value, original composite instance returned when nothing inside it was armed,isStateSchemaEvolutionEnabled()and a staticisArmedmirroring the descentDefaultKeyedStateStoreandAbstractKeyedStateBackendpass the descriptor's state kind to the arming factoryDefaultOperatorStateBackendrejects an armed serializer for list, union list and broadcast state, which also covers the state-v2 getters that delegate to themRowDataSerializer.isStateSchemaEvolutionEnabled()becomes the public implementation of the interface methodtable.exec.state.schema-evolution.enableddescription now states that list state elements and map state values are coveredVerifying this change
This change added tests and can be verified as follows:
ListState<RowData>element and aMapState<K, RowData>value are armed, and the map key of the same descriptor is notValueStatewhose value is a list or a map stays unarmed below the top level, through bothDefaultKeyedStateStoreand a directAbstractKeyedStateBackend#getOrCreateKeyedStateregistrationMapState<Long, List<Tuple2<RowData, Boolean>>>stays unarmed and its evolved schema is rejected (existing test, unchanged)equalsandhashCodeequal to its unarmed originalisArmedignores the map keyEach new test was checked to fail with the corresponding part of the fix reverted. The RocksDB state migration suites and
flink-architecture-tests-productionpass.Does this pull request potentially affect one of the following parts:
@Public(Evolving): no. The changed interface and methods are@Internal.RowDataaccept a backward-compatible schema change. With the option off, restore behavior is unchanged.Documentation
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 5)