feat(client): allow setting memo on workflow start - #1443
Conversation
`WorkflowStartOptions` had no memo field, so there was no way to attach a memo when starting a workflow from the client. The read side and the workflow side already exist - continue-as-new can set one, and `WorkflowExecutionDescription::memo()` reads one back - it was only the client start path that was missing. Adds `memo: Option<ProtoMemo>` and wires it into both `StartWorkflowExecutionRequest` and `SignalWithStartWorkflowExecutionRequest`. Uses the proto type directly, matching the existing `header: Option<Header>` field. Both are `map<string, Payload>` and the caller has to convert values either way. Imported as `ProtoMemo` so it doesn't collide with the re-exported `temporalio_common::Memo`, which is the read-side wrapper. Two integ tests in workflow_client_tests.rs: one that a memo set at start comes back through describe, one that it's empty when unset.
|
|
|
Thanks for making a PR! We definitely do want to avoid asking users to supply the proto fields directly. That work is mostly complete. For memo, we should do something like what we're doing in the workflow: https://github.com/temporalio/sdk-core/blob/2e725f1349b8bcdc9ecf3b153c7a02cb9ba1c44e/crates/workflow/src/workflow_context.rs#L1976 But this looks like it will require moving the @chris-olszewski May have some input |
chris-olszewski
left a comment
There was a problem hiding this comment.
As Spencer mentioned, we should use MemoValues here. It is currently defined in temporalio_workflow, but only has deps on stdlib/temporalio_common so it should be a straightforward to move it to temporalio_common so we can use it from workflows and clients. Only thing to call out is to make sure we re-export it from temporalio_workflow after the move.
Per review, the client shouldn't ask callers to build the proto Memo themselves. It now takes `MemoValues`, the same type the workflow surface already uses for continue-as-new and `upsert_memo`. `MemoValue`/`MemoValues` move out of `temporalio_workflow` and into common-wasm, next to the existing `Memo` read wrapper. That's where they have to go for both sides to reach them... `temporalio_workflow` depends on common-wasm, not common. Re-exported from `temporalio_common`, `temporalio_workflow` and `temporalio_sdk`, so existing imports still work. `MemoValue` holds its value in an `Arc` now instead of an `Rc`, and wants `Send + Sync`. `WorkflowStartOptions` travels through the interceptor chain inside a `Send` future and an `Rc` can't cross that. Same shape as `DataConverter`, which already erases its converters as `Arc<dyn ... + Send + Sync>`. Also runs the memo through the payload codec, not just the payload converter. The read side already codec-decodes it... describe and list both run `decode_payloads` over the memo. So converter-only encoding broke the round trip for anyone with a real codec. Caught this after wiring up the tests below, the two client tests fail without it. Client unit tests cover start and signal-with-start under a codec, plus the serialization failure path. Integ tests moved over to `MemoValues`.
|
@chris-olszewski - this look any better? I was trying to avoid breaking changes before, tried this time to obut didnt like the options. Clauded the PR Desc and got a code review and fixed some bugs with it. |
What was changed
Updated per review, this no longer asks callers to build the proto
Memothemselves.WorkflowStartOptions::memois nowOption<MemoValues>, the same type continue-as-new andupsert_memoalready takeMemoValue/MemoValuesout oftemporalio_workflowand into common-wasm, next to the existingMemoread wrapperStartWorkflowExecutionRequestandSignalWithStartWorkflowExecutionRequestOn where the types landed
They went to
temporalio-common-wasmrather thantemporalio-common.temporalio_workflowdepends on common-wasm and not on common, so common is too high for the workflow crate to reach... common-wasm is the lowest common point.Memoalready lives there andtemporalio_commonre-exports it, so this follows the same shape. Re-exported fromtemporalio_common,temporalio_workflowandtemporalio_sdk, so existing import paths keep working.On the codec
Writing this turned up a round-trip bug worth calling out separately. The read side already codec-decodes memo...
WorkflowExecutionDescription::newrunsdecode_payloadsover the whole describe response, and the list path decodesexecution.memoexplicitly.describe_decodes_workflow_payload_fieldsasserts exactly this withXorCodec. So encoding with the payload converter alone would have broken the round trip for anyone running a real codec, which is mostly people encrypting payloads. Start now runs the memo throughencode_payloadstoo.The two new client tests fail without this, and the raw bytes on the wire are the plaintext
"memo-value", so it is easy to see. The existing integ tests did not catch it because they use the default identity codec.Why?
There's currently no way to set a memo when starting a workflow from the client.
The rest of memo support is already here.... continue-as-new can set one,
WorkflowExecutionDescription::memo()reads one back, and there's even amemo_warn_sizethreshold inPayloadLimitsOptionsfor outbound memos. It's only the client start path that was missing.I ran into this building an Elixir SDK on top of core. Memo at start is the primary path in the other SDKs and in the docs, so I spent a while assuming I'd missed the setter before concluding it wasn't there.
Checklist
Closes N/A
How was this tested:
Client unit tests on the mock start client, all under
XorCodecso the codec path is actually exercised:StartWorkflowExecutionRequest, is codec-encoded, and reads back through the same decode pathdescribeusesSignalWithStartWorkflowExecutionRequestNoneWorkflowStartError::PayloadConversionand never sends the requestPlus the two integ tests in
workflow_client_tests.rs, moved over toMemoValues. TheMemoValues::encodeunit test moved with the type into common-wasm.cargo integ-test(490 passed),cargo lintandcargo fmt --all --checkall pass locally.CHANGELOG updated. Fields are documented inline.
MemoValueheld its value in anRc.WorkflowStartOptionstravels through the interceptor chain inside aSendfuture, and anRccannot cross that, so sharing one type between the client and the workflow means it has to beArcand the value has to beSend + Sync:This only breaks memo values that are themselves non-
Send/non-Sync, so something holding anRcor aRefCell. Legal in workflow code today, but I'd guess rare. It also matches howDataConvertererases its own converters (Arc<dyn ... + Send + Sync>).I looked at avoiding it. An enum with both
RcandArcvariants doesn't work, auto traits are per type and not per value.Box<dyn Trait + Send>drops theSyncrequirement but needsT: CloneforMemoValue: Clone, which is a worse trade. Encoding to proto before the options enter the interceptor chain would leave a non-Sendfield onWorkflowStartOptionsitself, which seems worse again. Happy to go a different way if you'd rather, this just seemed like the cheapest option.