feat: marker state machine#142
Conversation
Signed-off-by: Tim Li <ltim@uber.com>
Codecov Report❌ Patch coverage is
🚀 New features to boost your workflow:
|
| from cadence.api.v1 import decision, history | ||
| from cadence.api.v1.common_pb2 import Payload | ||
|
|
||
| MARKER_ID_HEADER_KEY = "__cadence_python_marker_id" |
There was a problem hiding this comment.
How does this compare to the MarkerName in RecordMarkerDecisionAttributes?
I'm not super familiar with Markers, but the Go SDK seems to use that as the ID.
There was a problem hiding this comment.
I checked with Go SDK, and I added a permalink for the formatting "%v_%v" in the comments
| DecisionId(DecisionType.MARKER, marker_id), | ||
| { | ||
| "marker_name": attrs.marker_name, | ||
| "details": attrs.details, |
There was a problem hiding this comment.
I don't think we need to store the details in the Expectation, I think we'll have to handle it special in DecisionManager. The Expectation exists only for non-determinism tracking.
There was a problem hiding this comment.
Thanks, I removed details from both handlers.
| # on name+id alone. The history-side handler (below) carries 'details' so DecisionManager | ||
| # can retrieve the recorded value and return it to the caller on replay. | ||
| @to_expectation.register | ||
| def _(attrs: decision.RecordMarkerDecisionAttributes) -> Expectation | None: |
There was a problem hiding this comment.
The Go SDK (haven't looked at Java) names markers with as %s_%s, where the first parameter is the type of the marker (local activity, side effect, version, etc.) and the second is the contextual identifier for that marker (activity ID, side effect ID, version marker). I think our state machine will unfortunately have to do something similar and behave differently depending on the type. We could even consider a different state machine for each, but LocalActivities is really the only one that's massively different.
Adding a new Version Marker for example is totally fine and we don't want to create an Expectation for either the decision or the event.
Adding a new LocalActivity on the other hand should be flagged on nondeterminism. I think side effects are similar.
For LocalActivities we'll also have to do immediate cancellation and expose a Future and everything, but we can ignore that for now.
There was a problem hiding this comment.
Thanks, I have made those changes.
| ) | ||
|
|
||
| def _validate_expectation(self, actual: Expectation) -> None: | ||
| def _validate_expectation(self, actual: Expectation) -> Expectation: |
There was a problem hiding this comment.
Nit: Can you document the new return value?
There was a problem hiding this comment.
Added comments for checking expectation.
Signed-off-by: Tim Li <ltim@uber.com>
Code Review 👍 Approved with suggestions 1 resolved / 2 findingsImplements the Marker state machine for tracking decision lifecycles and enforcing replay determinism. Update 💡 Edge Case: Marker detail decoding can misread foreign/raw payloads📄 cadence/_internal/workflow/statemachine/marker_state_machine.py:38-52 📄 cadence/_internal/workflow/statemachine/decision_manager.py:291-305
Within a pure Python client this is unlikely because every marker this SDK writes is length-prefixed, so the risk is low; hence minor. If cross-SDK histories are ever replayed, consider adding a distinguishing magic prefix/version byte to the encoding so foreign payloads are reliably rejected rather than heuristically parsed. ✅ 1 resolved✅ Quality: Marker event routing silently no-ops on unknown ids
🤖 Prompt for agentsOptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
| Format: 4-byte big-endian length of context_id, then context_id UTF-8, then user_data. | ||
| """ | ||
| ctx = context_id.encode("utf-8") | ||
| return len(ctx).to_bytes(4, "big") + ctx + user_data |
There was a problem hiding this comment.
I think the other SDKs align on this at least being a JSON object, let's do the same rather than introducing a more complex encoding.
I think we can punt that to a subsequent PR as well. For the state machine and non-determinism I don't think we really care what specifically is in the Marker, just the type (which we infer from the name).
When adding LocalActivities or other things we might alter the non-determinism
There was a problem hiding this comment.
Thanks, they are now encoded as a json object.
| if 4 + ctx_len > len(data): | ||
| return None, data | ||
| try: | ||
| context_id = data[4 : 4 + ctx_len].decode("utf-8") |
There was a problem hiding this comment.
We can parse this from the marker name rather than the data.
There was a problem hiding this comment.
since we encode them as json object, for decoding part, we can just json.decode to get the context_id and data
| LOCAL_ACTIVITY_MARKER_NAME = "LocalActivity" | ||
| MUTABLE_SIDE_EFFECT_MARKER_NAME = "MutableSideEffect" | ||
|
|
||
| KNOWN_MARKER_NAMES = frozenset( |
There was a problem hiding this comment.
One additional one is the immediate cancellation marker Cancel_ which is unique to Python.
There was a problem hiding this comment.
I added a cancel bypass in decision_manager.
| return None | ||
| return Expectation( | ||
| DecisionId(DecisionType.MARKER, f"{attrs.marker_name}_{context_id}"), | ||
| {"marker_name": attrs.marker_name}, |
There was a problem hiding this comment.
We can have an empty expectation body, just the correct DecisionID is enough for now. That covers type and name. When we add LocalActivities and other things with more parameters we might introduce additional checks, but we want to be really specifi with them.
Signed-off-by: Tim Li <ltim@uber.com>
Signed-off-by: Tim Li <ltim@uber.com>
Code Review ✅ Approved 2 resolved / 2 findingsImplements the MarkerStateMachine and side_effect API to ensure deterministic marker recording and replay. Resolved silent no-ops on unknown marker IDs and potential payload decoding errors. ✅ 2 resolved✅ Quality: Marker event routing silently no-ops on unknown ids
✅ Edge Case: Marker detail decoding can misread foreign/raw payloads
OptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
What changed?
Implement marker state machine
Why?
Need the state machine for marker feature
How did you test it?
unit tests
Potential risks
Release notes
Documentation Changes