fix: round-trip serdes result on first run across ops#550
Conversation
6a28b7a to
e1293c7
Compare
| # returned as-is. | ||
| if serialized_state is None: | ||
| return None # type: ignore[return-value] | ||
| return deserialize( |
There was a problem hiding this comment.
I think deserialize should happen before we checkpoint SUCCEEDED. If deserialize throws, we checkpointed SUCCEEDED and then checkpoint another FAILED for the same operation. Same issue in child operation.
There was a problem hiding this comment.
+1
just thinking through the various points this applies:
wait_for_condition:
serialize -> deserialize -> wait_strategy(deserialized_state, attempt) -> checkpoint SUCCEED -> return deserialized_state.
This fixes the double-checkpoint and also matches JS, where the wait strategy already sees the round-tripped state.
Note the wait_for_condition reordering fixes a second, separate divergence for free: today Python passes the raw state to wait_strategy while JS passes the round-tripped state, so with a non-identity serdes the same strategy can decide differently across SDKs. Deserializing before the wait_strategy call closes that gap too.
child:
deserialize before the SUCCEED checkpoint, in all three modes. Note this intentionally diverges from JS ordering (JS checkpoints first). JS is only safe there because safeDeserialize terminates the invocation instead of throwing, so it never writes a FAIL after a SUCCEED.
Deserialize-before-checkpoint is prob the correct Python equivalent, and matches how serialize failures already behave (FAIL, no SUCCEED)?
step
same pattern exists there from fix(step): Return round-tripped result on first run #543 (deserialize after SUCCEED, inside the try that routes to the retry handler)
There was a problem hiding this comment.
Yes my proposal is to keep it consistent across all operations in python.
- Deserialize before the SUCCEED checkpoint - a "SUCCEEDED" op should always be reconstructable.
- No automatic retry - the failure is usually deterministic; auto-retry poison-pills or re-runs side effects.
- Make it catchable as a distinct SerDesException - lets users recover / lets map/parallel tolerance absorb one bad item, while an uncaught one still fails loudly.
| # returned as-is. | ||
| if serialized_state is None: | ||
| return None # type: ignore[return-value] | ||
| return deserialize( |
There was a problem hiding this comment.
+1
just thinking through the various points this applies:
wait_for_condition:
serialize -> deserialize -> wait_strategy(deserialized_state, attempt) -> checkpoint SUCCEED -> return deserialized_state.
This fixes the double-checkpoint and also matches JS, where the wait strategy already sees the round-tripped state.
Note the wait_for_condition reordering fixes a second, separate divergence for free: today Python passes the raw state to wait_strategy while JS passes the round-tripped state, so with a non-identity serdes the same strategy can decide differently across SDKs. Deserializing before the wait_strategy call closes that gap too.
child:
deserialize before the SUCCEED checkpoint, in all three modes. Note this intentionally diverges from JS ordering (JS checkpoints first). JS is only safe there because safeDeserialize terminates the invocation instead of throwing, so it never writes a FAIL after a SUCCEED.
Deserialize-before-checkpoint is prob the correct Python equivalent, and matches how serialize failures already behave (FAIL, no SUCCEED)?
step
same pattern exists there from fix(step): Return round-tripped result on first run #543 (deserialize after SUCCEED, inside the try that routes to the retry handler)
315a90c to
cd7a3c4
Compare
Issue #, if available:
#406
#544
Description of changes:
Extends the step operation's first-run behavior (#543) to the remaining operations: on the first run, an operation now returns the serdes round-tripped value
deserialize(serialize(result))- instead of the raw in-memory result. This is the same value each operation reconstructson replay, so results stay identical across the first run and every replay when a non-identity custom SerDes is configured.
Covered operations:
wait_for_condition: returns the round-tripped state on success.run_in_child_context: round-trips in every mode - normal, virtual, and large-payload (ReplayChildren). Large payloads still checkpoint only a compact summary; the full result is round-tripped transiently for the return value. This also flows throughmap/parallel, whose items and BatchResult go through the child handler.invokeandcallbackneed no change (invoke always deserializes from the checkpoint; callback returns only an id).Testing:
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.