fix: round-trip serdes result on first run across ops - #550
Conversation
6a28b7a to
e1293c7
Compare
315a90c to
cd7a3c4
Compare
b4f4ddf to
3d1eef0
Compare
249c4e7 to
6a57868
Compare
This comment has been minimized.
This comment has been minimized.
6a57868 to
7db4a57
Compare
This comment has been minimized.
This comment has been minimized.
7db4a57 to
782f6db
Compare
This comment has been minimized.
This comment has been minimized.
d05e7f4 to
6afedae
Compare
|
re A collision here means except StepError: misses a real step failure and except SerDesError: catches a non-serdes one could be an idea to store the namepssced class name rather than the short/bare class name? i.e collisions become difficult... To collide, a user would have to ship a module literally named aws_durable_execution_sdk_python.exceptions containing a class of the same name. At which point they've shadowed the SDK's own import path and have gone to a fair degree of trouble. |
6afedae to
eebd0ac
Compare
This comment has been minimized.
This comment has been minimized.
| # Namespaced wire token used as the checkpoint ErrorType discriminator. | ||
| # Matching on this rather than the bare class name prevents a user | ||
| # exception coincidentally named "SerDesError" from being misidentified. | ||
| WIRE_ERROR_TYPE: str = "aws_durable_execution_sdk_python.exceptions.SerDesError" |
There was a problem hiding this comment.
f"{__module__}.{__qualname__}"?
Also, so only this one type gets a namespaced token and eight others get bare class names? So the ErrorType field now carries two conventions.
You could argue this is the only discriminant the SDK branches on to choose a raised type, so this bare-names for the others are fine and this one is special?
If you want to go the other way for consistency, a relatively light way of enforcing this for the others is something like this in ErrorObject.from_exception?
cls_ = type(exception)
module: str = "" if cls_.__module__ == "builtins" else f"{cls_.__module__}."
wire_type: str = f"{module}{cls_.__qualname__}"
type=wire_type"
This would give you ValueError rather than builtins.ValueError, and everything else full.y qualified.
this would also mean to change
_DURABLE_OPERATION_ERROR_REGISTRY to derived from the classes rather than typed by hand.
And raise_as_operation_error's comparison to use the same derived expression for SerDesError.
There was a problem hiding this comment.
We can do this to make it consistent, but should be a separate change.
And this change means we are accepting that python will differ in convention with other SDKs.
There was a problem hiding this comment.
Re "differ in convention", my worry is that this change is introducing two different conventions in the same PR in the same SDK. And when looking at convention for other SDKs, doesn't Java's ErrorType values use fully qualified (Class.forName(errorType))? JS does use bare names.
And the other part I was asking about is whether it's feasiable to derive the value for WIRE_ERROR_TYPE instead of hardcoding the literal?
Which parts of these are you proposing to do as a separate change?
There was a problem hiding this comment.
yes it's feasible to derive the value of WIRE_ERROR_TYPE from module name, I'll include that change. I am proposing to do the same change in other Error types in separate PR (still in v2).
Claude AI reviewReview: fix: round-trip serdes result on first run across opsI reviewed the complete SHA-anchored diff plus the surrounding source ( Verified correct
No actionable code findings.Residual test riskThe map/parallel parent-level Reviewed commit |
eebd0ac to
6060f46
Compare
This comment has been minimized.
This comment has been minimized.
6060f46 to
a3b48c2
Compare
| except RetryableSerDesError: | ||
| # Transient serdes failure: fail the invocation for backend retry, | ||
| # bypassing the step retry strategy. This narrow catch relies on the | ||
| # serdes wrappers raising only RetryableSerDesError or SerDesError; | ||
| # any other retryable InvocationError would fall through to the step | ||
| # retry strategy below. | ||
| raise |
There was a problem hiding this comment.
Codex AI review
P1 Re-raising only bypasses the retry strategy for the current invocation. With AT_MOST_ONCE_PER_RETRY, the durable START already exists; the backend retry sees STARTED, treats it as StepInterruptedError, and invokes the step retry strategy. An exhausted or filtered strategy therefore makes this transient SerDes failure terminal. Handle this case explicitly for at-most-once steps, either through retry_handler or a dedicated checkpoint state that can resume serialization, and add a replay test.
| # First poll (or a retry with no stored state): round-trip | ||
| # initial_state through the serdes so the check sees the same | ||
| # shape it gets on later polls, which come from the checkpoint. | ||
| current_state = self._deserialize( | ||
| self._serialize(self.config.initial_state) | ||
| ) |
There was a problem hiding this comment.
Codex AI review
P2 Round-tripping initial_state changes the input to the first check and requires it to be serializable, although no poll state has yet been checkpointed. Existing custom SerDes users may intentionally transform the initial value in the check or support only returned states; those workflows now fail or compute different results. Replay before the first completed poll already reconstructs initial_state from user code, so preserve the configured value and round-trip only check results.
| # First poll (or a retry with no stored state): round-trip | |
| # initial_state through the serdes so the check sees the same | |
| # shape it gets on later polls, which come from the checkpoint. | |
| current_state = self._deserialize( | |
| self._serialize(self.config.initial_state) | |
| ) | |
| # No poll state has been checkpointed yet, so replay reconstructs | |
| # the configured initial state directly. | |
| current_state = self.config.initial_state |
| except RetryableSerDesError: | ||
| # Transient failure: propagate so it retries the invocation. | ||
| raise |
There was a problem hiding this comment.
Codex AI review
P2 Only retryable errors are propagated unchanged. If a custom SerDes deliberately raises SerDesError with data or stack_trace, the generic handler replaces it with a fresh instance and discards that metadata before ErrorObject.from_exception can checkpoint it. Propagate SerDesError unchanged, or copy its fields, in both serialize and deserialize; add coverage for an explicitly raised permanent error.
Codex AI reviewThe round-trip logic has retry and compatibility regressions, plus metadata loss for explicit permanent SerDes errors. Reviewed commit |
Issue #, if available:
#406
#544
Description of changes:
When a step, child context, or wait_for_condition finishes, we now serialize its result and deserialize it back before saving the SUCCEED checkpoint. The value the function returns on the first run is the deserialized one - exactly what it would return on replay.
Before this, the first run returned the raw in-memory result, but replay returned the value rebuilt from the checkpoint. With a custom serdes that changes the value in transit, those two could differ. Running the round-trip up front makes the first run and replay always agree, and guarantees a SUCCEEDED result is always reconstructable.
Serdes failures are now clearly split:
Behavior changes to note
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.