Repository navigation
[Bug] continue_as_new() loses memo upserts when memo is omitted #12912
Jackmeson1
started this conversation in
Temporal SDKs
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Memo behavior on continue as new is inconsistent.
Some quick AI research suggests that the behavior of a pending memo upsert varies depending on language:
NoneandSome(empty)both become an empty command map; Core treats it as omittedThere is no indication as yet that anyone has actually been impacted by this behavior, and it could easily be worked around by passing the memos to the continue as new.
Original Report in Python
What are you really trying to do?
Keep the memo of a workflow, including changes made with
workflow.upsert_memo(), acrossworkflow.continue_as_new(). This follows the maintainer's invitation to discuss the behavior when temporalio/sdk-python#2058 was declined (comment).Describe the bug
workflow.continue_as_new()documentsmemoas "Memo for the workflow. Defaults to the current workflow's memo." (temporalio/workflow/_workflow_ops.py:881). Whenmemois omitted, the next run starts with the memo that the previous run started with: a changed value reverts, an added key is missing and a removed key comes back. Search attributes upserted withworkflow.upsert_search_attributes()are carried over, and an explicitly supplied nonempty memo is preserved.continue_as_newworkflow.memo()in run 1 after the upsertworkflow.memo()at the start of run 2memoomitted{'changed': 'old value', 'removed': 'removed', 'kept': 'kept'}{'added': 'added', 'changed': 'new value', 'kept': 'kept'}{'changed': 'old value', 'kept': 'kept', 'removed': 'removed'}memo=dict(workflow.memo()){'added': 'added', 'changed': 'new value', 'kept': 'kept'}memoomitted, every key removed{'a': '1', 'b': '2'}{}{'a': '1', 'b': '2'}memo={}, every key removed{'a': '1', 'b': '2'}{}{'a': '1', 'b': '2'}In all four runs the keyword search attribute upserted in run 1 (
initialat start,upsertedin run 1) isupsertedin run 2, anddescribe()of run 2 shows the same memo asworkflow.memo(). The releasedtemporalio1.35.0 and 1.30.0 give the same memo and search-attribute values with the same script (dictionary order and generatedBuildIdsdiffer).Where it comes from: when
memois omitted the SDK builds no memo for the command (temporalio/worker/_workflow_instance.py:3912-3920and:3942-3944; the payloads are only builtif self._input.memo, so{}counts as omitted). sdk-core fills an empty memo of the command from the start info of the run, whereas search attributes use the current values (augment_continue_as_new_with_current_values,workflow_machines.rs:1712-1735, sdk-coref49305f1). The proto comment of the command says "If unset, re-uses the current workflow's memo" (temporalio/bridge/proto/workflow_commands/workflow_commands_pb2.pyi:999).PR temporalio/sdk-python#2058 proposed sending the current memo when none was supplied (its description notes that this cannot cover removing every key, because the current memo is then empty and sdk-core treats an empty memo as unset). It was declined: "This doesn't sound like a change we want to make. If you want to summarize in an issue, we can consider how we want to address it." This issue follows that invitation to discuss the behavior.
Minimal Reproduction
python memocan_repro.pystarts a dev server withWorkflowEnvironment.start_local()(the script passesmemo=Nonefor "omitted").memocan_repro.py
Environment/Versions
mainatd488ff59(temporalio.__version__is 1.35.0; built from source withpoe build-develop; sdk-coref49305f1), Python 3.12.3, dev server fromWorkflowEnvironment.start_local()(Temporal CLI 1.10.0, server 1.32.2)Additional context
Alternatives, for discussion:
memomeans the memo the run started with.Empty memo. The memo of the command is a map and sdk-core treats an empty one as unset (
attrs.memo.is_empty()); the SDK also treatsmemo={}as omitted. Afterupsert_memoremoved every key (last two rows) the next run gets the start memo back. When the run started with a nonempty memo,memo={}cannot explicitly replace it with an empty memo during continue-as-new. Tracking current memo in Core could preserve the result of removing every key before continuing. Distinguishing an explicit empty override from omission would still require a separate representation or API decision.Replay. I recorded the first run of a workflow that omits
memoand replayed it with a workflow that passesmemo=dict(workflow.memo()), and the other way round (script available on request). Both cross-direction replays and their two unchanged-code controls pass for this one-upsert example with the default converter and no payload codec. This checks changing the supplied memo contents, not a patched SDK/Core implementation. sdk-core's continue-as-new state machine checks event and command types without comparing memo contents (continue_as_new_workflow_state_machine.rs:68-95).AI disclosure: this was prepared with AI assistance (Claude Opus 5.5 and Claude Sonnet 5.5).
All reactions