[api][runtime] Replay newObject memory updates as object creations during durable recovery - #1030
[api][runtime] Replay newObject memory updates as object creations during durable recovery#1030purushah wants to merge 2 commits into
Conversation
|
Hi @joeyutong, could you help review this pr? |
|
Thanks for addressing all three comments. I re-reviewed 4edd5f6 and don’t see any further issues with the change itself. The only remaining item is to rebase and resolve the conflicts with current main. When resolving ActionExecutionOperator, please keep notifyActionReused(actionTask) after both memory replays. Once the conflicts are resolved, this looks good to me. |
…ring durable recovery
MemoryObjectImpl.newObject() recorded itself in the durable ActionState as
MemoryUpdate(path, null), indistinguishable from set(path, null). The
recovery replay path applied every recorded update via MemoryObject.set(),
so replaying a completed action that created a nested object either threw
IllegalArgumentException ("Cannot overwrite object with value") against an
object restored from the checkpoint -- a permanent recovery crash loop --
or materialized the object as a null value leaf, after which replaying any
child write hit an UnsupportedOperationException from the value item's
immutable subKeys set.
Fix: add an objectCreation discriminator to MemoryUpdate (additive; the old
two-arg constructor is kept and Jackson defaults the missing field to false
for records written before the field existed, preserving their replay
behavior), record it from newObject(), and replay object-creation updates
via newObject(path, true) in the new MemoryUpdateReplayer, which the
operator's short-term and sensory replay loops now share.
Tests: MemoryUpdateReplayerTest covers replay into empty state, replay over
an already existing object (the crash-loop case), overwrite of a value
leaf, and a user's genuine set(path, null) staying a value write. Two
operator-level recovery tests exercise both scenarios end to end through
the Flink operator harness with an ActionStateStore, and
ActionStateSerdeTest verifies the discriminator survives the serde
round-trip. All verified red before the fix and green after.
…s, add legacy-serde and lone-newObject tests - MemoryUpdate's creator now throws IllegalArgumentException when objectCreation=true carries a non-null value, so malformed updates fail loudly instead of being silently ignored by the replayer. - ActionStateSerdeTest gains a legacy-record case: objectCreation stripped from the serialized JSON deserializes with the field defaulting to false, preserving pre-existing records' replay semantics. - MemoryUpdateReplayerTest gains a lone-newObject case: replay of an action that only created an object yields an empty nested object, not a null leaf.
4edd5f6 to
fd466cc
Compare
|
Rebased onto current main (0e36efd). The |
Linked issue: #1029
Purpose of change
With durable execution enabled,
MemoryObjectImpl.newObject()records itself in theActionStateasMemoryUpdate(path, null)— indistinguishable from a user callingset(path, null). The recovery replay path applies every recorded update viaset(), so replaying a completed action that created a nested object either throwsIllegalArgumentException: Cannot overwrite object with valueagainst an object restored from the checkpoint (a permanent recovery crash loop — reproduced on a local mini-cluster job with a Kafka action-state store, where the job burned all restart attempts and died), or materializes the object as anullvalue leaf, after which replaying any child write throwsUnsupportedOperationException(and the memory tree is silently corrupted).This PR adds an
objectCreationdiscriminator toMemoryUpdate, records it fromnewObject(), and replays object-creation updates vianewObject(path, true)in a new sharedMemoryUpdateReplayerused by the operator's short-term and sensory replay loops.Tests
MemoryUpdateReplayerTest(4 cases): replay into empty state restores a real nested object with children; replay over an already existing object (the crash-loop case) succeeds;newObject(path, overwrite=true)over a value leaf replays correctly; a user's genuineset(path, null)still replays as a null value, not an object.ActionExecutionOperatorTest(testReplayReappliesNewObjectMemoryUpdatesIntoEmptyState,testReplayReappliesNewObjectMemoryUpdatesOverRestoredState) run both scenarios end to end throughKeyedOneInputStreamOperatorTestHarnesswith anActionStateStore, including checkpoint snapshot/restore.ActionStateSerdeTestextended with a round-trip assertion for the new field.ActionExecutionOperatorTestandShortTermMemoryTTLIntegrationTest) pass.mainthe job crash-loops and dies withCannot overwrite object with value: user; with this patch the identical job recovers and completes with correct memory state. Logs attached to the linked issue.API
MemoryUpdategains a three-arg constructor andisObjectCreation(); the existing two-arg constructor is kept (source- and binary-compatible). Compatibility notes:objectCreationfield defaults tofalse, so pre-existing records replay exactly as before.ActionStateSerde's documented recovery-compat contract ("the durable-execution journal ... never survives a code or Flink upgrade").MemoryItemor state schemas).MemoryUpdate.equals/hashCode/toStringnow include the flag, so a recordednewObjectupdate no longer compares equal toset(path, null)— which is the semantic distinction this fix introduces.Documentation
doc-neededdoc-not-neededdoc-includedWas this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code 2.1.234 (Claude Fable 5)
🤖 Generated with Claude Code