diff --git a/docs/develop/dotnet/index.mdx b/docs/develop/dotnet/index.mdx index 511614c28c..10bb652da4 100644 --- a/docs/develop/dotnet/index.mdx +++ b/docs/develop/dotnet/index.mdx @@ -64,7 +64,7 @@ The [Workflow messages feature guide](/develop/dotnet/message-passing) shows how **Signals** -- [Define Signal](/develop/dotnet/message-passing#define-a-signal): A Signal is a message sent to a running Workflow Execution. +- [Define Signal](/develop/dotnet/message-passing#signals): A Signal is a message sent to a running Workflow Execution. - [Send a Signal from a Temporal Client](/develop/dotnet/message-passing#send-signal-from-client): Send a Signal to a Workflow from a Temporal Client. - [Send a Signal from a Workflow](/develop/dotnet/message-passing#send-signal-from-workflow): Send a Signal to another Workflow from within a Workflow, this would also be called an External Signal. - [Signal-With-Start](/develop/dotnet/message-passing#signal-with-start): Start a Workflow and send it a Signal in a single operation used from the Client. @@ -73,13 +73,13 @@ The [Workflow messages feature guide](/develop/dotnet/message-passing) shows how **Queries** -- [Define a Query](/develop/dotnet/message-passing#define-query): A Query is a synchronous operation that is used to get the state of a Workflow Execution. +- [Define a Query](/develop/dotnet/message-passing#queries): A Query is a synchronous operation that is used to get the state of a Workflow Execution. - [Send Queries](/develop/dotnet/message-passing#send-query): Queries are sent from the Temporal Client. -- [Set a Dynamic Query](/develop/dotnet/message-passing#set-a-dynamic-query): A Dynamic Query in Temporal is a Query that is invoked dynamically at runtime if no other Query with the same name is registered. +- [Set a Dynamic Query](/develop/dotnet/message-passing#set-a-dynamic-signal): A Dynamic Query in Temporal is a Query that is invoked dynamically at runtime if no other Query with the same name is registered. **Updates** -- [Define an Update](/develop/dotnet/message-passing#define-update): An Update is an operation that can mutate the state of a Workflow Execution and return a response. +- [Define an Update](/develop/dotnet/message-passing#updates): An Update is an operation that can mutate the state of a Workflow Execution and return a response. - [Send an Update](/develop/dotnet/message-passing#send-update-from-client): An Update is sent from the Temporal Client. ## Interrupt a Workflow diff --git a/docs/develop/dotnet/message-passing.mdx b/docs/develop/dotnet/message-passing.mdx index 2ea868744a..a861a23265 100644 --- a/docs/develop/dotnet/message-passing.mdx +++ b/docs/develop/dotnet/message-passing.mdx @@ -1,214 +1,693 @@ --- id: message-passing -title: Workflow message passing - .NET SDK feature guide +title: Messages - Temporal .NET SDK feature guide sidebar_label: Messages -description: Learn how to develop with Signals, Queries, and Updates using the Temporal .NET SDK. Get in-depth guidance on defining, sending, and dynamically handling them to enhance your Workflow Execution. -toc_max_heading_level: 5 +description: Develop with Queries, Signals, and Updates with the Temporal .NET SDK. +toc_max_heading_level: 4 keywords: - - message passing - - signals - - queries - - updates + - temporal dotnet signals + - send signal from client + - send signal from workflow + - signal with start + - workflow queries + - sending queries + - workflow updates + - dynamic workflows + - dynamic activities + - dynamic signals + - dynamic queries tags: - - message-passing + - dotnet + - dotnet-sdk + - workflows + - messages - signals - queries - updates + - dynamic-handlers --- -This page shows how to do the following: +A Workflow can act like a stateful web service that receives messages: Queries, Signals, and Updates. +The Workflow implementation defines these endpoints via handler methods that can react to incoming messages and return values. +Temporal Clients use messages to read Workflow state and control execution. +See [Workflow message passing](/encyclopedia/workflow-message-passing) for a general overview of this topic. +This page introduces these features for the Temporal .NET SDK. + +## Write message handlers {#writing-message-handlers} + +:::info +The code that follows is part of a [working solution](https://github.com/temporalio/samples-dotnet/tree/main/src/MessagePassing). +::: + +Follow these guidelines when writing your message handlers: + +- Message handlers are defined as methods on the Workflow class, using one of the three attributes: [`WorkflowQueryAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowQueryAttribute.html), [`WorkflowSignalAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowSignalAttribute.html), and [`WorkflowUpdateAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowUpdateAttribute.html). +- The parameters and return values of handlers and the main Workflow function must be [serializable](/dataconversion). +- Prefer data classes to multiple input parameters. + Data class parameters allow you to add fields without changing the calling signature. + +### Query handlers {#queries} + +A [Query](/encyclopedia/workflow-message-passing#sending-queries) is a synchronous operation that retrieves state from a Workflow Execution. +Define as a method: + +```csharp +[Workflow] +public class GreetingWorkflow +{ + public enum Language + { + Chinese, + English, + French, + Spanish, + Portuguese, + } + + public record GetLanguagesInput(bool IncludeUnsupported); + + // ... + + [WorkflowQuery] + public IList GetLanguages(GetLanguagesInput input) => + Enum.GetValues(). + Where(language => input.IncludeUnsupported || Greetings.ContainsKey(language)). + ToList(); + + // ... +``` + +Or as a property getter: + +```csharp +[Workflow] +public class GreetingWorkflow +{ + public enum Language + { + Chinese, + English, + French, + Spanish, + Portuguese, + } + + // ... + + [WorkflowQuery] + public Language CurrentLanguage { get; private set; } = Language.English; + + // ... +``` + +- The Query attribute can accept arguments. + See the API reference docs: [`WorkflowQueryAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowQueryAttribute.html). +- A Query handler must not modify Workflow state. +- You can't perform async blocking operations such as executing an Activity in a Query handler. + +### Signal handlers {#signals} -- [Develop with Signals](#signals) -- [Develop with Queries](#queries) -- [Develop with Updates](#updates) +A [Signal](/encyclopedia/workflow-message-passing#sending-signals) is an asynchronous message sent to a running Workflow Execution to change its state and control its flow: -## Signals {#signals} +```csharp +[Workflow] +public class GreetingWorkflow +{ + public record ApproveInput(string Name); -**How to develop with Signals using the Temporal .NET SDK** + // ... -A [Signal](/encyclopedia/workflow-message-passing#sending-signals) is a message sent to a running Workflow Execution. + [WorkflowSignal] + public async Task ApproveAsync(ApproveInput input) + { + approvedForRelease = true; + approverName = input.Name; + } -Signals are defined in your code and handled in your Workflow Definition. -Signals can be sent to Workflow Executions from a Temporal Client or from another Workflow Execution. + // ... +``` -### Define Signal {#define-a-signal} +- The Signal attribute can accept arguments. + Refer to the API docs: [`WorkflowSignalAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowSignalAttribute.html). -**How to define a Signal using the Temporal .NET SDK** +- The handler should not return a value. + The response is sent immediately from the server, without waiting for the Workflow to process the Signal. -A Signal has a name and can have arguments. +- Signal (and Update) handlers can be asynchronous and blocking. + This allows you to use Activities, Child Workflows, durable [`Workflow.DelayAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_DelayAsync_System_Int32_System_Nullable_System_Threading_CancellationToken__) Timers, [`Workflow.WaitConditionAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_WaitConditionAsync_System_Func_System_Boolean__System_Int32_System_Nullable_System_Threading_CancellationToken__) conditions, and more. + See [Async handlers](#async-handlers) and [Workflow message passing](/encyclopedia/workflow-message-passing) for guidelines on safely using async Signal and Update handlers. -- The name, also called a Signal type, is a string. -- The arguments must be serializable. - To define a Signal, set the `[WorkflowSignal]`(https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowSignalAttribute.html) attribute on the Signal method inside your Workflow. - A string name can be provided to the attribute, otherwise the Signal's name defaults to the unqualified method name (sans an "Async" suffix if it is async). +### Update handlers and validators {#updates} -Temporal suggests taking a single argument that is an object that can be added to as needed. +An [Update](/encyclopedia/workflow-message-passing#sending-updates) is a trackable synchronous request sent to a running Workflow Execution. +It can change the Workflow state, control its flow, and return a result. +The sender must wait until the Worker accepts or rejects the Update. +The sender may wait further to receive a returned value or an exception if something goes wrong: ```csharp -[WorkflowSignal] -public async Task DoSomethingAsync(DoSomethingParam input) => pendingThings.Add(input); +[Workflow] +public class GreetingWorkflow +{ + public enum Language + { + Chinese, + English, + French, + Spanish, + Portuguese, + } + + // ... + + [WorkflowUpdateValidator(nameof(SetCurrentLanguageAsync))] + public void ValidateLanguage(Language language) + { + if (!Greetings.ContainsKey(language)) + { + throw new ApplicationFailureException($"{language} is not supported"); + } + } + + [WorkflowUpdate] + public async Task SetCurrentLanguageAsync(Language language) + { + var previousLanguage = CurrentLanguage; + CurrentLanguage = language; + return previousLanguage; + } + + // ... ``` -### Send a Signal from a Temporal Client {#send-signal-from-client} +- The Update attribute can take arguments (like, `Name`, `Dynamic` and `UnfinishedPolicy`) as described in the API reference docs for [`WorkflowUpdateAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowUpdateAttribute.html). -**How to send a Signal from a Temporal Client using the Temporal .NET SDK** +- About validators: + - Use validators to reject an Update before it is written to History. + Validators are always optional. + If you don't need to reject Updates, you can skip them. + - Define an Update validator with the [`WorkflowUpdateValidatorAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowUpdateValidatorAttribute.html) attribute. + Use the Name argument when declaring the validator to connect it to its Update. + The validator must be a `void` type and accept the same argument types as the handler. -When a Signal is sent successfully from the Temporal Client, the [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the Event History of the Workflow that receives the Signal. +- Accepting and rejecting Updates with validators: + - To reject an Update, raise an exception of any type in the validator. + - Without a validator, Updates are always accepted. +- Validators and Event History: + - The `WorkflowExecutionUpdateAccepted` event is written into the History whether the acceptance was automatic or programmatic. + - When a Validator raises an error, the Update is rejected, the Update is not run, and `WorkflowExecutionUpdateAccepted` _won't_ be added to the Event History. + The caller receives an "Update failed" error. -To send a Signal from the Client, use the `SignalAsync()` method on the Workflow handle. -The Workflow handle can be obtained via `StartWorkflowAsync()` or `GetWorkflowHandle()` methods on the client. +- Use [`Workflow.CurrentUpdateInfo`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_CurrentUpdateInfo) to obtain information about the current Update. + This includes the Update ID, which can be useful for deduplication when using Continue-As-New: see [Ensuring your messages are processed exactly once](/encyclopedia/workflow-message-passing#exactly-once-message-processing). +- Update (and Signal) handlers can be asynchronous and blocking. + This allows you to use Activities, Child Workflows, durable [`Workflow.DelayAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_DelayAsync_System_Int32_System_Nullable_System_Threading_CancellationToken__) Timers, [`Workflow.WaitConditionAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_WaitConditionAsync_System_Func_System_Boolean__System_Int32_System_Nullable_System_Threading_CancellationToken__) conditions, and more. + See [Async handlers](#async-handlers) and [Workflow message passing](/encyclopedia/workflow-message-passing) for guidelines on safely using async Update and Signal handlers. + +## Send messages {#send-messages} + +To send Queries, Signals, or Updates you call methods on a [`WorkflowHandle`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowHandle.html) object. +To obtain the WorkflowStub, you can: + +- Use [`TemporalClient.StartWorkflowAsync`](https://dotnet.temporal.io/api/Temporalio.Client.TemporalClient.html#Temporalio_Client_TemporalClient_StartWorkflowAsync_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowOptions_) to start a Workflow and return its handle. +- Use the [`TemporalClient.GetWorkflowHandle`](https://dotnet.temporal.io/api/Temporalio.Client.TemporalClient.html#Temporalio_Client_TemporalClient_GetWorkflowHandle_System_String_System_String_System_String_) method to retrieve a Workflow handle by its Workflow Id. + +For example: ```csharp var client = await TemporalClient.ConnectAsync(new("localhost:7233")); -var handle = await client.StartWorkflowAsync( - (MyWorkflow wf) => wf.RunAsync(), - new(id: "my-workflow-id", taskQueue: "my-task-queue")); -var param = new DoSomethingParam("something"); -await handle.SignalAsync(wf => wf.DoSomethingAsync(param)); +var workflowHandle = await client.StartWorkflowAsync( + (GreetingWorkflow wf) => wf.RunAsync(), + new(id: "message-passing-workflow-id", taskQueue: "message-passing-sample")); ``` -### Send a Signal from a Workflow {#send-signal-from-workflow} +To check the argument types required when sending messages -- and the return type for Queries and Updates -- refer to the corresponding handler method in the Workflow Definition. -**How to send a Signal from a Workflow using the Temporal .NET SDK** +### Send a Query {#send-query} -A Workflow can send a Signal to another Workflow, in which case it's called an _External Signal_. +Call a Query method with [`WorkflowHandle.QueryAsync`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowHandle.html#Temporalio_Client_WorkflowHandle_QueryAsync__1_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowQueryOptions_): -When an External Signal is sent: +```csharp +var supportedLanguages = await workflowHandle.QueryAsync(wf => wf.GetLanguages(new(false))); +``` -- A [SignalExternalWorkflowExecutionInitiated](/references/events#signalexternalworkflowexecutioninitiated) Event appears in the sender's Event History. -- A [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the recipient's Event History. +- Sending a Query doesn’t add events to a Workflow's Event History. + +- You can send Queries to closed Workflow Executions within a Namespace's Workflow retention period. + This includes Workflows that have completed, failed, or timed out. + Querying terminated Workflows is not safe and, therefore, not supported. + +- A Worker must be online and polling the Task Queue to process a Query. -Use `GetExternalWorkflowHandle` to get a Workflow handle to an existing Workflow by its identifier. +### Send a Signal {#send-signal} + +You can send a Signal to a Workflow Execution from a Temporal Client or from another Workflow Execution. +However, you can only send Signals to Workflow Executions that haven’t closed. + +#### Send a Signal from a Client {#send-signal-from-client} + +Use [`WorkflowHandle.SignalAsync`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowHandle.html#Temporalio_Client_WorkflowHandle_SignalAsync_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowSignalOptions_) from Client code to send a Signal: ```csharp -var handle = Workflow.GetExternalWorkflowHandle("other-workflow-id"); -var param = new DoSomethingParam("something"); -await handle.SignalAsync(wf => wf.DoSomethingAsync(param)); +await workflowHandle.SignalAsync(wf => wf.ApproveAsync(new("MyUser"))); ``` -### Signal-With-Start {#signal-with-start} +- The call returns when the server accepts the Signal; it does _not_ wait for the Signal to be delivered to the Workflow Execution. + +- The [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the Workflow's Event History. + +#### Send a Signal from a Workflow {#send-signal-from-workflow} -**How to Signal-With-Start using the Temporal .NET SDK** +A Workflow can send a Signal to another Workflow, known as an _External Signal_. +In this case you need to obtain a Workflow handle for the external Workflow. +Use `Workflow.GetExternalWorkflowHandle`, passing a running Workflow Id, to retrieve a typed Workflow handle: -Signal-With-Start is used from the Client. -It takes a Workflow Id, Workflow arguments, a Signal name, and Signal arguments. +```csharp +// ... -If there's a Workflow running with the given Workflow Id, it will be signaled. If there isn't, a new Workflow will be started and immediately signaled. +[Workflow] +public class WorkflowB +{ + [WorkflowRun] + public async Task RunAsync() + { + var handle = Workflow.GetExternalWorkflowHandle("workflow-a"); + await handle.SignalAsync(wf => wf.YourSignalAsync("signal argument")); + } + + // ... +``` -To send a Signal-With-Start in .NET, set the `StartSignal` property in `WorkflowOptions` for `StartWorkflowAsync` or `ExecuteWorkflowAsync` with the name of your Signal. -Arguments for the signal can be set with the `StartSignalArgs`. -A `SignalWithStart` helper exists on the options for type-safe invocation. +When an External Signal is sent: +- A [SignalExternalWorkflowExecutionInitiated](/references/events#signalexternalworkflowexecutioninitiated) Event appears in the sender's Event History. +- A [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the recipient's Event History. + +#### Signal-With-Start {#signal-with-start} + +Signal-With-Start allows a Client to send a Signal to a Workflow Execution, starting the Execution if it is not already running. +If there's a Workflow running with the given Workflow Id, it will be signaled. +If there isn't, a new Workflow will be started and immediately signaled. +To use Signal-With-Start, call `SignalWithStart` with a lambda expression invoking it: ```csharp var client = await TemporalClient.ConnectAsync(new("localhost:7233")); - -// Create options for signal-with-start -var options = new WorkflowOptions(id: "my-workflow-id", taskQueue: "my-task-queue"); -var param = new DoSomethingParam("something"); -options.SignalWithStart((MyWorkflow wf) => wf.DoSomethingAsync(param)); -await client.StartWorkflowAsync((MyWorkflow wf) => wf.RunAsync(), options); +var options = new WorkflowOptions(id: "your-signal-with-start-workflow", taskQueue: "signal-tq"); +options.SignalWithStart((GreetingWorkflow wf) => wf.SubmitGreetingAsync("User Signal with Start")); +await client.StartWorkflowAsync((GreetingWorkflow wf) => wf.RunAsync(), options); ``` -### Dynamic Handler {#dynamic-handler} +### Send an Update {#send-update-from-client} -**How to set a Dynamic Handler** +An Update is a synchronous, blocking call that can change Workflow state, control its flow, and return a result. -Temporal supports Dynamic Signals, Queries, Workflows, and Activities, -These are unnamed handlers that are invoked if no other statically defined handler with the given name exists. +A Client sending an Update must wait until the Server delivers the Update to a Worker. +Workers must be available and responsive. +If you need a response as soon as the Server receives the request, use a Signal instead. +Also note that you can't send Updates to other Workflow Executions or perform an Update equivalent of Signal-With-Start. -Dynamic Handlers provide flexibility to handle cases where the names of Signals, Queries, Workflows, or Activities, aren't known at run time. +- `WorkflowExecutionUpdateAccepted` is added to the Event History when the Worker confirms that the Update passed validation. +- `WorkflowExecutionUpdateCompleted` is added to the Event History when the Worker confirms that the Update has finished. -:::caution +To send an Update to a Workflow Execution, you can: -Dynamic Handlers should be used judiciously as a fallback mechanism rather than the primary approach. -Overusing them can lead to maintainability and debugging issues down the line. +- Call the Update method with `ExecuteUpdateAsync` from the Client and wait for the Update to complete. + This code fetches an Update result: + + ```csharp +var previousLanguage = await workflowHandle.ExecuteUpdateAsync( + wf => wf.SetCurrentLanguageAsync(GreetingWorkflow.Language.Chinese)); + ``` + +- 2. Use `StartUpdateAsync` to receive a handle as soon as the Update is accepted or rejected + It returns an `UpdateHandle` + + - Use this `UpdateHandle` later to fetch your results. + - Asynchronous Update handlers normally perform long-running async Activities. + - `StartUpdateAsync` only waits until the Worker has accepted or rejected the Update, not until all asynchronous operations are complete. + + For example: + + ```csharp + // Wait until the update is accepted + var updateHandle = await workflowHandle.StartUpdateAsync( + wf => wf.SetGreetingAsync(new HelloWorldInput("World"))); + // Wait until the update is completed + var updateResult = await updateHandle.GetResultAsync(); + ``` + + For more details, see the "Async handlers" section. + +:::info NON-TYPE SAFE API CALLS + +In real-world development, sometimes you may be unable to import Workflow Definition method signatures. +When you don't have access to the Workflow Definition or it isn't written in .NET, you can still use non-type safe APIs and dynamic method invocation. +Pass method names instead of method objects to: + +- [`TemporalClient.StartWorkflowAsync`](https://dotnet.temporal.io/api/Temporalio.Client.TemporalClient.html#Temporalio_Client_TemporalClient_StartWorkflowAsync_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowOptions_) +- [`WorkflowHandle.QueryAsync`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowHandle.html#Temporalio_Client_WorkflowHandle_QueryAsync__1_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowQueryOptions_) +- [`WorkflowHandle.SignalAsync`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowHandle.html#Temporalio_Client_WorkflowHandle_SignalAsync_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowSignalOptions_) +- [`WorkflowHandle.ExecuteUpdateAsync`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowHandle.html#Temporalio_Client_WorkflowHandle_ExecuteUpdateAsync_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowUpdateOptions_) +- [`WorkflowHandle.StartUpdateAsync`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowHandle.html#Temporalio_Client_WorkflowHandle_StartUpdateAsync_System_String_System_Collections_Generic_IReadOnlyCollection_System_Object__Temporalio_Client_WorkflowUpdateStartOptions_) + +Use non-type safe overloads of these APIs: + +- [`TemporalClient.GetWorkflowHandle`](https://dotnet.temporal.io/api/Temporalio.Client.TemporalClient.html#Temporalio_Client_TemporalClient_GetWorkflowHandle_System_String_System_String_System_String_) +- [`Workflow.GetExternalWorkflowHandle`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_GetExternalWorkflowHandle_System_String_System_String_) -Instead, Signals, Queries, Workflows, or Activities should be defined statically whenever possible, with clear names that indicate their purpose. -Use static definitions as the primary way of structuring your Workflows. -Reserve Dynamic Handlers for cases where the handler names are not known at compile time and need to be looked up dynamically at runtime. -They are meant to handle edge cases and act as a catch-all, not as the main way of invoking logic. ::: -### Set a Dynamic Signal {#set-a-dynamic-signal} +## Message handler patterns {#message-handler-patterns} -**How to set a Dynamic Signal using the Temporal .NET SDK** +This section covers common write operations, such as Signal and Update handlers. +It doesn't apply to pure read operations, like Queries or Update Validators. -A Dynamic Signal in Temporal is a Signal that is invoked dynamically at runtime if no other Signal with the same input is registered. -A Signal can be made dynamic by setting `Dynamic` to `true` on the `[WorkflowSignal]` attribute. -Only one Dynamic Signal can be present on a Workflow. +:::tip -The Signal Handler parameters must accept a `string` name and `Temporalio.Converters.IRawValue[]` for the arguments. -The [Workflow.PayloadConverter](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_PayloadConverter) property is used to convert an `IRawValue` object to the desired type using extension methods in the `Temporalio.Converters` namespace. +For additional information, see [Inject work into the main Workflow](/encyclopedia/workflow-message-passing#injecting-work-into-main-workflow) and [Ensuring your messages are processed exactly once](/encyclopedia/workflow-message-passing#exactly-once-message-processing). + +::: + +### Add async handlers to use `await` {#async-handlers} + +Signal and Update handlers can be asynchronous as well as blocking. +Using asynchronous calls allows you to `await` Activities, Child Workflows, [`Workflow.DelayAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_DelayAsync_System_Int32_System_Nullable_System_Threading_CancellationToken__) Timers, [`Workflow.WaitConditionAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_WaitConditionAsync_System_Func_System_Boolean__System_Int32_System_Nullable_System_Threading_CancellationToken__) wait conditions, etc. +This expands the possibilities for what can be done by a handler but it also means that handler executions and your main Workflow method are all running concurrently, with switching occurring between them at await calls. + +It's essential to understand the things that could go wrong in order to use asynchronous handlers safely. +See [Workflow message passing](/encyclopedia/workflow-message-passing) for guidance on safe usage of async Signal and Update handlers, and the [Controlling handler concurrency](#control-handler-concurrency) and [Waiting for message handlers to finish](#wait-for-message-handlers) sections below. + +The following code executes an Activity that simulates a network call to a remote service: ```csharp -[WorkflowSignal(Dynamic = true)] -public async Task DynamicSignalAsync(string signalName, IRawValue[] args) +public class MyActivities { - var input = Workflow.PayloadConverter.ToValue(args.Single()); - pendingThings.Add(input); + private static readonly Dictionary Greetings = new() + { + [Language.Arabic] = "مرحبا بالعالم", + [Language.Chinese] = "你好,世界", + [Language.English] = "Hello, world", + [Language.French] = "Bonjour, monde", + [Language.Hindi] = "नमस्ते दुनिया", + [Language.Spanish] = "Hola mundo", + }; + + [Activity] + public async Task CallGreetingServiceAsync(Language language) + { + // Pretend that we are calling a remove service + await Task.Delay(200); + return Greetings.TryGetValue(language, out var value) ? value : null; + } +} +``` + +The following code modifies a `WorkflowUpdate` for asynchronous use of the preceding Activity: + + +```csharp +[Workflow] +public class GreetingWorkflow +{ + private readonly Mutex mutex = new(); + + // ... + + [WorkflowUpdate] + public async Task SetLanguageAsync(Language language) + { + // 👉 Use a mutex here to ensure that multiple calls to SetLanguageAsync are processed in order. + await mutex.WaitOneAsync(); + try + { + if (!greetings.ContainsKey(language)) + { + var greeting = Workflow.ExecuteActivityAsync( + (MyActivities acts) => acts.CallGreetingServiceAsync(language), + new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) }); + if (greeting == null) + { + // 👉 An update validator cannot be async, so cannot be used to check that the remote + // CallGreetingServiceAsync supports the requested language. Throwing ApplicationFailureException + // will fail the Update, but the WorkflowExecutionUpdateAccepted event will still be + // added to history. + throw new ApplicationFailureException( + $"Greeting service does not support {language}"); + } + greetings[language] = greeting; + } + var previousLanguage = CurrentLanguage; + CurrentLanguage = language; + return previousLanguage; + } + finally + { + mutex.ReleaseMutex(); + } + } } ``` -## Queries {#queries} +After updating the code for asynchronous calls, your Update handler can schedule an Activity and await the result. +Although an async Signal handler can initiate similar network tasks, using an Update handler allows the Client to receive a result or error once the Activity completes. +This lets your Client track the progress of asynchronous work performed by the Update's Activities, Child Workflows, etc. -A [Query](/encyclopedia/workflow-message-passing#sending-queries) is a synchronous operation that is used to get the state of a Workflow Execution. +### Add wait conditions to block {#block-with-wait} -### Define a Query {#define-query} +Sometimes, async Signal or Update handlers need to meet certain conditions before they should continue. +Using a wait condition with [`Workflow.WaitConditionAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_WaitConditionAsync_System_Func_System_Boolean__System_Int32_System_Nullable_System_Threading_CancellationToken__) sets a function that prevents the code from proceeding until the condition returns `true`. +This is an important feature that helps you control your handler logic. -**How to define a Query using the Temporal .NET SDK** +Here are two important use cases for `Workflow.await`: -A Query has a name and can have arguments. +- Waiting in a handler until it is appropriate to continue. +- Waiting in the main Workflow until all active handlers have finished. -- The name, also called a Query type, is a string. -- The arguments must be [serializable](/dataconversion). +The condition state you're waiting for can be updated by and reflect any part of the Workflow code. +This includes the main Workflow method, other handlers, or child coroutines spawned by the main Workflow method, and so forth. -Queries may be methods or properties (only the getter is used). -Queries must be synchronous and must not mutate workflow state in any way or issue any [Commands](/workflows#command). -To define a Query, set the `[WorkflowQuery]`(https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowQueryAttribute.html) attribute on the Query method or property inside your Workflow. -A string name can be provided to the attribute, otherwise the Query's name defaults to the unqualified method/property name. +#### Use wait conditions in handlers {#wait-in-handlers} -Queries can be methods that can accept arguments: +Sometimes, async Signal or Update handlers need to meet certain conditions before they should continue. +Using a wait condition with [`Workflow.WaitConditionAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_WaitConditionAsync_System_Func_System_Boolean__System_Int32_System_Nullable_System_Threading_CancellationToken__) sets a function that prevents the code from proceeding until the condition returns `true`. +This is an important feature that helps you control your handler logic. + +Consider a `ReadyForUpdateToExecute` method that runs before your Update handler executes. +The [`Workflow.WaitConditionAsync`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html?#Temporalio_Workflows_Workflow_WaitConditionAsync_System_Func_System_Boolean__System_Int32_System_Nullable_System_Threading_CancellationToken__) call waits until your condition is met: ```csharp -[WorkflowQuery] -public string GetMyStatus(MyStatusParam input) => statuses[input.Type]; + [WorkflowUpdate] + public async Task MyUpdateAsync(UpdateInput updateInput) + { + await Workflow.WaitConditionAsync(() => ReadyForUpdateToExecute(updateInput)); + // ... + } ``` -Or properties: +Remember: Handlers can execute before the main Workflow method starts. + +#### Ensure your handlers finish before the Workflow completes {#wait-for-message-handlers} + +Workflow wait conditions can ensure your handler completes before a Workflow finishes. +When your Workflow uses async Signal or Update handlers, your main Workflow method can return or continue-as-new while a handler is still waiting on an async task, such as an Activity result. +The Workflow completing may interrupt the handler before it finishes crucial work and cause Client errors when trying retrieve Update results. +Use `Workflow.AllHandlersFinished` to address this problem and allow your Workflow to end smoothly: ```csharp -[WorkflowQuery] -public string MyStatus { get; private set; } +[Workflow] +public class MyWorkflow +{ + [WorkflowRun] + public async Task RunAsync() + { + // ... + await Workflow.WaitConditionAsync(() => Workflow.AllHandlersFinished); + return "workflow-result"; + } + + // ... ``` -### Send a Query from a Temporal Client {#send-query} +By default, your Worker will log a warning when you allow a Workflow Execution to finish with unfinished handler executions. +You can silence these warnings on a per-handler basis by passing the `UnfinishedPolicy` argument to the [`WorkflowSignalAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowSignalAttribute.html) / [`WorkflowUpdateAttribute`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowUpdateAttribute.html) decorator: + +```csharp + [WorkflowUpdate(UnfinishedPolicy = HandlerUnfinishedPolicy.Abandon)] + public async Task MyUpdateAsync() + { + // ... +``` -**How to send a Query using the Temporal .NET SDK** +See [Finishing handlers before the Workflow completes](/encyclopedia/workflow-message-passing#finishing-message-handlers) for more information. -Queries are sent from a Temporal Client. +### Use locks to prevent concurrent handler execution {#control-handler-concurrency} -To send a Query to a Workflow Execution from Client code, use the `QueryAsync()` method on the Workflow handle. +Concurrent processes can interact in unpredictable ways. +Incorrectly written [concurrent message-passing](/encyclopedia/workflow-message-passing#message-handler-concurrency) code may not work correctly when multiple handler instances run simultaneously. +Here's an example of a pathological case: ```csharp -var client = await TemporalClient.ConnectAsync(new("localhost:7233")); -var handle = await client.StartWorkflowAsync( - (MyWorkflow wf) => wf.RunAsync(), - new(id: "my-workflow-id", taskQueue: "my-task-queue")); -var status = await handle.QueryAsync(wf => wf.MyStatus); +[Workflow] +public class MyWorkflow +{ + // ... + + [WorkflowSignal] + public async Task BadHandlerAsync() + { + var data = await Workflow.ExecuteActivityAsync( + (MyActivities acts) => acts.FetchDataAsync(), + new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) }); + this.x = data.X; + // 🐛🐛 Bug!! If multiple instances of this handler are executing concurrently, then + // there may be times when the Workflow has this.x from one Activity execution and this.y from another. + await Workflow.DelayAsync(1000); + this.y = data.Y; + } +} ``` -### Set a Dynamic Query {#set-a-dynamic-query} +Coordinating access with [`Workflows.Mutex`](https://dotnet.temporal.io/api/Temporalio.Workflows.Mutex.html), a mutual exclusion lock, corrects this code. +Locking makes sure that only one handler instance can execute a specific section of code at any given time: + +```csharp +[Workflow] +public class MyWorkflow +{ + private readonly Mutex mutex = new(); + + // ... + + [WorkflowSignal] + public async Task SafeHandlerAsync() + { + await mutex.WaitOneAsync(); + try + { + var data = await Workflow.ExecuteActivityAsync( + (MyActivities acts) => acts.FetchDataAsync(), + new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) }); + this.x = data.X; + // ✅ OK: the scheduler may switch now to a different handler execution, or to the main workflow + // method, but no other execution of this handler can run until this execution finishes. + await Workflow.DelayAsync(1000); + this.y = data.Y; + } + finally + { + mutex.ReleaseMutex(); + } + } +} +``` + +For additional concurrency options, you can use [`Workflows.Semaphore`](https://dotnet.temporal.io/api/Temporalio.Workflows.Semaphore.html). +Semaphores manage access to shared resources and coordinate the order in which threads or processes execute. + +## Message handler troubleshooting {#message-handler-troubleshooting} + +When sending a Signal, Update, or Query to a Workflow, your Client might encounter the following errors: + +- **The Client can't contact the server**: + You'll receive a [`Temporalio.Exceptions.RpcException`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.html) exception whose `Code` property is [`RpcException.StatusCode`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.StatusCode.html) with a status of `Unavailable` (after some retries). + +- **The Workflow does not exist**: + You'll receive a [`Temporalio.Exceptions.RpcException`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.html) exception whose `Code` property is [`RpcException.StatusCode`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.StatusCode.html) with a status of `NotFound`. + +See [Exceptions in message handlers](/encyclopedia/workflow-message-passing#exceptions) for a non–.NET-specific discussion of this topic. + +### Problems when sending a Signal {#signal-problems} + +When using Signal, the only exception that will result from your requests during its execution is `RpcException`. +All handlers may experience additional exceptions during the initial (pre-Worker) part of a handler request lifecycle. + +For Queries and Updates, the Client waits for a response from the Worker. +If an issue occurs during the handler Execution by the Worker, the Client may receive an exception. + +### Problems when sending an Update {#update-problems} + +When working with Updates, you may encounter these errors: + +- **No Workflow Workers are polling the Task Queue**: + Your request will be retried by the SDK Client indefinitely. + Use a `CancellationToken` in your [RPC options](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowUpdateOptions.html#Temporalio_Client_WorkflowUpdateOptions_Rpc) to cancel the Update. + This raises a [Temporalio.Exceptions.WorkflowUpdateRpcTimeoutOrCanceledException](https://dotnet.temporal.io/api/Temporalio.Exceptions.WorkflowUpdateRpcTimeoutOrCanceledException.html) exception . + +- **Update failed**: You'll receive a [`Temporalio.Exceptions.WorkflowUpdateFailedException`](https://dotnet.temporal.io/api/Temporalio.Exceptions.WorkflowUpdateFailedException.html) exception. + There are two ways this can happen: + + - The Update was rejected by an Update validator defined in the Workflow alongside the Update handler. + + - The Update failed after having been accepted. + + Update failures are like [Workflow failures](/references/failures). + Issues that cause a Workflow failure in the main method also cause Update failures in the Update handler. + These might include: + + - A failed Child Workflow + - A failed Activity (if the Activity retries have been set to a finite number) + - The Workflow author raising `ApplicationFailure` + - Any error listed in [`TemporalWorkerOptions.WorkflowFailureExceptionTypes`](https://dotnet.temporal.io/api/Temporalio.Worker.TemporalWorkerOptions.html#Temporalio_Worker_TemporalWorkerOptions_WorkflowFailureExceptionTypes) on the Worker or [`WorkflowAttribute.FailureExceptionTypes`](https://dotnet.temporal.io/api/Temporalio.Workflows.WorkflowAttribute.html#Temporalio_Workflows_WorkflowAttribute_FailureExceptionTypes) on the Workflow (empty by default) + +- **The handler caused the Workflow Task to fail**: + A [Workflow Task Failure](/references/failures) causes the server to retry Workflow Tasks indefinitely. What happens to your Update request depends on its stage: + - If the request hasn't been accepted by the server, you receive a `FAILED_PRECONDITION` [`Temporalio.Exceptions.RpcException`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.html) exception. + - If the request has been accepted, it is durable. + Once the Workflow is healthy again after a code deploy, use an [`UpdateHandle`](https://dotnet.temporal.io/api/Temporalio.Client.WorkflowUpdateHandle.html) to fetch the Update result. + +- **The Workflow finished while the Update handler execution was in progress**: + You'll receive a [`Temporalio.Exceptions.RpcException`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.html) "workflow execution already completed". + + This will happen if the Workflow finished while the Update handler execution was in progress, for example because + + - The Workflow was canceled or failed. + + - The Workflow completed normally or continued-as-new and the Workflow author did not [wait for handlers to be finished](/encyclopedia/workflow-message-passing#finishing-message-handlers). -**How to set a Dynamic Query using the Temporal .NET SDK** +### Problems when sending a Query {#query-problems} + +When working with Queries, you may encounter these errors: + +- **There is no Workflow Worker polling the Task Queue**: + You'll receive a [`Temporalio.Exceptions.RpcException`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.html) on which the `Code` is a [`RpcException.StatusCode`](https://dotnet.temporal.io/api/Temporalio.Exceptions.RpcException.StatusCode.html) with a status of `FailedPrecondition`. + +- **Query failed**: + You'll receive a [`Temporalio.Exceptions.WorkflowQueryFailedException`](https://dotnet.temporal.io/api/Temporalio.Exceptions.WorkflowQueryFailedException.html) exception if something goes wrong during a Query. + Any exception in a Query handler will trigger this error. + This differs from Signal and Update requests, where exceptions can lead to Workflow Task Failure instead. + +- **The handler caused the Workflow Task to fail.** + This would happen, for example, if the Query handler blocks the thread for too long without yielding. + + +## Dynamic Handler {#dynamic-handler} + +Temporal supports Dynamic Queries, Signals, Updates, Workflows, and Activities. +These are unnamed handlers that are invoked if no other statically defined handler with the given name exists. + +Dynamic Handlers provide flexibility to handle cases where the names of Queries, Signals, Updates, Workflows, or Activities, aren't known at run time. + +:::caution + +Dynamic Handlers should be used judiciously as a fallback mechanism rather than the primary approach. +Overusing them can lead to maintainability and debugging issues down the line. + +Instead, Signals, Queries, Workflows, or Activities should be defined statically whenever possible, with clear names that indicate their purpose. +Use static definitions as the primary way of structuring your Workflows. + +Reserve Dynamic Handlers for cases where the handler names are not known at compile time and need to be looked up dynamically at runtime. +They are meant to handle edge cases and act as a catch-all, not as the main way of invoking logic. + +::: + +### Set a Dynamic Query {#set-a-dynamic-query} A Dynamic Query in Temporal is a Query method that is invoked dynamically at runtime if no other Query with the same name is registered. A Query can be made dynamic by setting `Dynamic` to `true` on the `[WorkflowQuery]` attribute. Only one Dynamic Query can be present on a Workflow. The Query Handler parameters must accept a `string` name and `Temporalio.Converters.IRawValue[]` for the arguments. -The [Workflow.PayloadConverter](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_PayloadConverter) property is used to convert an `IRawValue` object to the desired type using extension methods in the `Temporalio.Converters` namespace. +The [Workflow.PayloadConverter](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_PayloadConverter) property is used to convert an `IRawValue` object to the desired type using extension methods in the `Temporalio.Converters` Namespace. ```csharp [WorkflowQuery(Dynamic = true)] @@ -219,43 +698,39 @@ public string DynamicQueryAsync(string queryName, IRawValue[] args) } ``` -## Updates {#updates} - -**How to develop with Updates using the Temporal .NET SDK** - -An [Update](/encyclopedia/workflow-message-passing#sending-updates) is an operation that can mutate the state of a Workflow Execution and return a response. - -### Define an Update {#define-update} - -**How to define an Update using the Temporal .NET SDK** - -Workflow Updates handlers are methods in your Workflow Definition designed to handle updates. -These updates can be triggered during the lifecycle of a Workflow Execution. - -**Define an Update Handler** +### Set a Dynamic Signal {#set-a-dynamic-signal} -To define an update handler, use the `[WorkflowUpdate]` attribute on a method within your Workflow. +A Dynamic Signal in Temporal is a Signal that is invoked dynamically at runtime if no other Signal with the same input is registered. +A Signal can be made dynamic by setting `Dynamic` to `true` on the `[WorkflowSignal]` attribute. +Only one Dynamic Signal can be present on a Workflow. -- **Attribute Usage:** Apply `[WorkflowUpdate]` to the method intended to handle updates. -- **Overriding:** If a method with this attribute is overridden, the overriding method should also have the `[WorkflowUpdate]` attribute. -- **Validator Method:** Optionally, you can define a validator method for the update handler. This validator is specified using `[WorkflowUpdateValidator]` attribute with the argument of the update method (e.g. `[WorkflowUpdateValidator(nameof(MyUpdateMethod))]`) and is invoked before the update handler. -- **Return Values:** The update handler can return a serializable value. This value is sent back to the caller of the update. +The Signal Handler parameters must accept a `string` name and `Temporalio.Converters.IRawValue[]` for the arguments. +The [Workflow.PayloadConverter](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_PayloadConverter) property is used to convert an `IRawValue` object to the desired type using extension methods in the `Temporalio.Converters` Namespace. ```csharp -[WorkflowUpdate] -public async Task UpdateStatusAsync(Status status) +[WorkflowSignal(Dynamic = true)] +public async Task DynamicSignalAsync(string signalName, IRawValue[] args) { - this.status = status; - return "Status updated"; + var input = Workflow.PayloadConverter.ToValue(args.Single()); + pendingThings.Add(input); } ``` -### Send an Update from a Temporal Client {#send-update-from-client} +### Set a Dynamic Update {#set-a-dynamic-update} -**How to send an Update from a Temporal Client using the Temporal .NET SDK** +A Dynamic Update in Temporal is an Update that is invoked dynamically at runtime if no other Update with the same input is registered. +An Update can be made dynamic by setting `Dynamic` to `true` on the `[WorkflowUpdate]` attribute. +Only one Dynamic Update can be present on a Workflow. -To send a Workflow Update from a Temporal Client, call the `ExecuteUpdateAsync` method on the `WorkflowHandle` class. +The Update Handler parameters must accept a `string` name and `Temporalio.Converters.IRawValue[]` for the arguments. +The [Workflow.PayloadConverter](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_PayloadConverter) property is used to convert an `IRawValue` object to the desired type using extension methods in the `Temporalio.Converters` Namespace. ```csharp -var result = await handle.ExecuteUpdateAsync(wf => wf.UpdateStatusAsync(newStatus)); +[WorkflowUpdate(Dynamic = true)] +public async Task DynamicUpdateAsync(string updateName, IRawValue[] args) +{ + var input = Workflow.PayloadConverter.ToValue(args.Single()); + pendingThings.Add(input); + return statuses[input.Type]; +} ``` diff --git a/docs/develop/go/message-passing.mdx b/docs/develop/go/message-passing.mdx index ce5b7713ca..1a8e4f8533 100644 --- a/docs/develop/go/message-passing.mdx +++ b/docs/develop/go/message-passing.mdx @@ -1,8 +1,8 @@ --- id: message-passing -title: Workflow message passing - Temporal Go SDK feature guide +title: Messages - Go SDK feature guide sidebar_label: Messages -description: Learn how to develop with Signals, Queries, and Updates using the Go SDK in Temporal. Methods covered include Signal handling, Query definitions, and Update integrations. +description: Develop with Queries, Signals, and Updates with the Temporal Go SDK. toc_max_heading_level: 4 keywords: - temporal go signals @@ -27,62 +27,95 @@ tags: - dynamic-handlers --- -This page shows how to do the following: +A Workflow can act like a stateful web service that receives messages: Queries, Signals, and Updates. +The Workflow implementation defines these endpoints via handler methods that can react to incoming Queries and Updates, and via Signal channels. +Temporal Clients use messages to read Workflow state and control its execution. +See [Workflow message passing](/encyclopedia/workflow-message-passing) for a general overview of this topic. +This page introduces these features for the Temporal Go SDK. -- [Develop with Signals](#signals) -- [Develop with Queries](#queries) -- [Develop with Updates](#updates) -## Signals {#signals} +## Handle messages {#handling-messages} -A [Signal](/encyclopedia/workflow-message-passing#sending-signals) is a message sent to a running Workflow Execution. +:::info +The code that follows is part of a working message passing [sample](https://github.com/temporalio/samples-go/tree/message-passing/message-passing-intro). +::: -Signals are defined in your code and handled in your Workflow Definition. -Signals can be sent to Workflow Executions from a Temporal Client or from another Workflow Execution. +Follow these guidelines when writing message handlers: -### Define a Signal {#define-signal} -** How to define a Signal using the Go SDK.** +- Values sent in messages, and the return values of message handlers and the main Workflow function, must be [serializable](/dataconversion). +- Prefer using a single struct over multiple input parameters. + This allows you to add fields without changing the calling signature. -A Signal has a name and can have arguments. -- The name, also called a Signal type, is a string. -- The arguments must be [serializable](/dataconversion). +### Query handlers {#queries} -Structs should be used to define Signals and carry data, as long as the struct is [serializable via the Data Converter](https://pkg.go.dev/go.temporal.io/sdk/converter#CompositeDataConverter.ToPayload). -The `Receive()` method on the Data Converter decodes the data into the Struct within the Workflow. -Only public fields are serializable. +A [Query](/encyclopedia/workflow-message-passing#sending-queries) is a synchronous operation that retrieves state from a Workflow Execution: ```go -type MySignal struct { - Message string // serializable - message string // not serializable +type Language string + +const Chinese Language = "chinese" +const English Language = "english" +const French Language = "french" +const Spanish Language = "spanish" +const Portuguese Language = "portuguese" + +const GetLanguagesQuery = "GetLanguages" + +type GetLanguagesInput struct { + IncludeUnsupported bool +} + +func GreetingWorkflow(ctx workflow.Context) (string, error) { + ... + greeting := map[Language]string{English: "Hello", Chinese: "你好,世界"} + err := workflow.SetQueryHandler(ctx, GetLanguagesQuery, func(input GetLanguagesInput) ([]Language, error) { + // 👉 A Query handler returns a value: it can inspect but must not mutate the Workflow state. + if input.IncludeUnsupported { + return []Language{Chinese, English, French, Spanish, Portuguese}, nil + } else { + // Range over map is a nondeterministic operation. + // It is OK to have a non-deterministic operation in a query function. + //workflowcheck:ignore + return maps.Keys(greeting), nil + } + }) + ... } ``` -### Handle a Signal {#handle-signal} +- Use [`SetQueryHandler`](https://pkg.go.dev/go.temporal.io/sdk/workflow#SetQueryHandler) to set a Query Handler that listens for a Query by name. +- The handler must be a function that returns two values, a serializable result and an error. +- You can't perform async operations such as executing an Activity in a Query handler. -**How to handle a Signal using the Go SDK.** +### Signal Channels {#signals} -Workflows listen for Signals by the Signal's name. +A [Signal](/encyclopedia/workflow-message-passing#sending-signals) is an asynchronous message sent to a running Workflow Execution to change its state and control its flow. +Handle Signal messages by receiving them from their channel: -Use the `GetSignalChannel()` API from the `go.temporal.io/sdk/workflow` package to get the Signal Channel. +```go +const ApproveSignal = "approve" -A common use-case is to block a Workflow while waiting for a Signal, like in the following snippet: +type ApproveInput struct { + Name string +} -```go -func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error { - // ... - var signal MySignal - signalChan := workflow.GetSignalChannel(ctx, "your-signal-name") - signalChan.Receive(ctx, &signal) - if len(signal.Message) > 0 && signal.Message != "SOME_VALUE" { - return errors.New("signal") - } - // ... +func GreetingWorkflow(ctx workflow.Context) error { + logger := workflow.GetLogger(ctx) + approverName := "" + ... + // Block until the language is approved + var approveInput ApproveInput + workflow.GetSignalChannel(ctx, ApproveSignal).Receive(ctx, &approveInput) + approverName = approveInput.Name + logger.Info("Received approval", "Approver", approverName) + ... } ``` +- Pass the Signal's name to [`GetSignalChannel`](https://pkg.go.dev/go.temporal.io/sdk/workflow#GetSignalChannel) to get the Signal Channel that listen for Signals of that type. + Alternatively, you might want the Workflow to proceed and still be capable of handling external Signals. ```go @@ -98,64 +131,156 @@ func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error selector.Select(ctx) } }) - // submit activity one - // signal can be received while activity one is pending - + // You could now submit an activity; any signals will still be received while the activity is pending. } ``` In the example above, the Workflow code uses `workflow.GetSignalChannel` to open a `workflow.Channel` for the Signal type (identified by the Signal name). -Before completing the Workflow or using [Continue-As-New](/develop/go/continue-as-new), make sure to do an asynchronous drain on the Signal channel. + +- Before completing the Workflow or using [Continue-As-New](/develop/go/continue-as-new), make sure to do an asynchronous drain on the Signal channel. Otherwise, the Signals will be lost. +The [batch sliding window](https://github.com/temporalio/samples-go/tree/main/batch-sliding-window) sample contains an example: -### Send a Signal from a Temporal Client {#send-signal-from-client} +```go + reportCompletionChannel := workflow.GetSignalChannel(ctx, "ReportCompletion") + // Drain signals async + for { + var recordId int + ok := reportCompletionChannel.ReceiveAsync(&recordId) + if !ok { + break + } + s.recordCompletion(ctx, recordId) + } +``` -**How to send a Signal from a Temporal Client using the Go SDK.** +### Update handlers and validators {#updates} -When a Signal is sent successfully from the Temporal Client, the [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the Event History of the Workflow that receives the Signal. +An [Update](/encyclopedia/workflow-message-passing#sending-updates) is a trackable synchronous request sent to a running Workflow Execution. +It can change the Workflow state, control its flow, and return a result. +The sender must wait until the Worker accepts or rejects the Update. +The sender may wait further to receive a returned value or an exception if something goes wrong: -Use the `SignalWorkflow()` method on an instance of the [Go SDK Temporal Client](https://pkg.go.dev/go.temporal.io/sdk/client#Client) to send a [Signal](/encyclopedia/workflow-message-passing#sending-signals) to a [Workflow Execution](/workflows#workflow-execution). +```go +type Language string + +const SetLanguageUpdate = "set-language" + +func GreetingWorkflow(ctx workflow.Context) error { + language := English + + err = workflow.SetUpdateHandlerWithOptions(ctx, SetLanguageUpdate, func(ctx workflow.Context, newLanguage Language) (Language, error) { + // 👉 An Update handler can mutate the Workflow state and return a value. + var previousLanguage Language + previousLanguage, language = language, newLanguage + return previousLanguage, nil + }, workflow.UpdateHandlerOptions{ + Validator: func(ctx workflow.Context, newLanguage Language) error { + if _, ok := greeting[newLanguage]; !ok { + // 👉 In an Update validator you return any error to reject the Update. + return fmt.Errorf("%s unsupported language", newLanguage) + } + return nil + }, + }) + ... +} +``` -Pass in both the [Workflow Id](/workflows#workflow-id) and [Run Id](/workflows#run-id) to uniquely identify the Workflow Execution. -If only the Workflow Id is supplied (provide an empty string as the Run Id param), the Workflow Execution that is Running receives the Signal. +- Register an Update handler for a given name using either [workflow.SetUpdateHandler](https://pkg.go.dev/go.temporal.io/sdk/workflow#SetUpdateHandler) or [workflow.SetUpdateHandlerWithOptions](https://pkg.go.dev/go.temporal.io/sdk/workflow#SetUpdateHandlerWithOptions). +- The handler must be a function that accepts a `workflow.Context` as its first parameter. +- The function can return either a serializable value with an error or just an error. + +- About validators: + - Use validators to reject an Update before it is written to History. + Validators are always optional. + If you don't need to reject Updates, you don't need a validator. + - To set a validator, pass the validator function in the [workflow.UpdateHandlerOptions](https://pkg.go.dev/go.temporal.io/sdk@v1.29.1/internal#UpdateHandlerOptions) when calling [workflow.SetUpdateHandlerWithOptions](https://pkg.go.dev/go.temporal.io/sdk/workflow#SetUpdateHandlerWithOptions). + The validator must be a function that accepts the same argument types as the handler and returns a single value of type error. + +- Accepting and rejecting Updates with validators: + - To reject an Update you must return an error or panic in the validator. + The Workflow's `WorkflowPanicPolicy` determines how panics are handled inside the Handler function. + - Without a validator, Updates are always accepted. +- Validators and Event History: + - The `WorkflowExecutionUpdateAccepted` event is written into History whether the acceptance was automatic or due to a validator function not throwing an error or panicking. + - When a validator throws an error, the Update is rejected and `WorkflowExecutionUpdateAccepted` _won't_ be added to the Event History. + The caller receives an "Update failed" error. + +- Use [`workflow.GetCurrentUpdateInfo`](https://pkg.go.dev/go.temporal.io/sdk/workflow#GetCurrentUpdateInfo) to obtain information about the current Update. + This includes the Update ID, which can be useful for deduplication when using Continue-As-New: see [Ensuring your messages are processed exactly once](/encyclopedia/workflow-message-passing#exactly-once-message-processing). +- Update handlers can use Activities, Child Workflows, durable [workflow.Sleep](https://pkg.go.dev/go.temporal.io/sdk/workflow#Sleep) Timers, [`workflow.Await`](https://pkg.go.dev/go.temporal.io/sdk/workflow#Await) conditions, and more. + See [Blocking handlers](#blocking-handlers) and [Workflow message passing](/encyclopedia/workflow-message-passing) for safe usage guidelines. + + +## Send messages {#send-messages} + +To send Queries, Signals, or Updates, you call methods on a Temporal [Client](https://pkg.go.dev/go.temporal.io/sdk/client#Client). +To check the argument types required when sending messages -- and the return type for Queries and Updates -- refer to the corresponding handler method in the Workflow Definition. + + +### Send a Query {#send-query} + +Queries are sent from a Temporal Client. + +Use [`Client.QueryWorkflow`](https://pkg.go.dev/go.temporal.io/sdk/client#Client.QueryWorkflow) or [`Client.QueryWorkflowWithOptions`](https://pkg.go.dev/go.temporal.io/sdk/client#Client.QueryWorkflowWithOptions). ```go // ... -signal := MySignal { - Message: "Some important data", +supportedLangResult, err := temporalClient.QueryWorkflow(context.Background(), we.GetID(), we.GetRunID(), message.GetLanguagesQuery, message.GetLanguagesInput{IncludeUnsupported: false}) +if err != nil { + log.Fatalf("Unable to query workflow: %v", err) } -err = temporalClient.SignalWorkflow(context.Background(), "your-workflow-id", runID, "your-signal-name", signal) +var supportedLang []message.Language +err = supportedLangResult.Get(&supportedLang) if err != nil { - log.Fatalln("Error sending the Signal", err) - return + log.Fatalf("Unable to get query result: %v", err) } +log.Println("Supported languages:", supportedLang) // ... ``` -Possible errors: +- Sending a Query doesn’t add events to a Workflow's Event History. -- `serviceerror.NotFound` -- `serviceerror.Internal` -- `serviceerror.Unavailable` +- You can send Queries to closed Workflow Executions within a Namespace's Workflow retention period. + This includes Workflows that have completed, failed, or timed out. + Querying terminated Workflows is not supported. -### Send a Signal from a Workflow {#send-signal-from-workflow} +- A Worker must be online and polling the Task Queue to process a Query. -**How to send a Signal from a Workflow using the Go SDK.** +### Send a Signal {#send-signal} -A Workflow can send a Signal to another Workflow, in which case it's called an _External Signal_. +You can send a Signal to a Workflow Execution from a Temporal Client or from another Workflow Execution. +However, you can only send Signals to Workflow Executions that haven’t closed. -When an External Signal is sent: +#### Send a Signal from a Client {#send-signal-from-client} -- A [SignalExternalWorkflowExecutionInitiated](/references/events#signalexternalworkflowexecutioninitiated) Event appears in the sender's Event History. -- A [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the recipient's Event History. +Use [`Client.SignalWorkflow`](https://pkg.go.dev/go.temporal.io/sdk/client#Client.SignalWorkflow). + +Pass in both the [Workflow Id](/workflows#workflow-id) and [Run Id](/workflows#run-id) to uniquely identify the Workflow Execution. +If only the Workflow Id is supplied (provide an empty string as the Run Id param), the Workflow Execution that is running receives the Signal. + +```go +// ... +err = temporalClient.SignalWorkflow(context.Background(), we.GetID(), we.GetRunID(), message.ApproveSignal, message.ApproveInput{Name: ""}) +if err != nil { + log.Fatalf("Unable to signal workflow: %v", err) +} +// ... +``` +- The call returns when the server accepts the Signal; it does _not_ wait for the Signal to be delivered to the Workflow Execution. + +- The [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the Workflow's Event History. + +#### Sending a Signal from a Workflow {#send-signal-from-workflow} -A Signal can be sent from within a Workflow to a different Workflow Execution using the [`SignalExternalWorkflow`](https://pkg.go.dev/go.temporal.io/sdk/workflow#SignalExternalWorkflow) API from the `go.temporal.io/sdk/workflow` package. +A Workflow can send a Signal to another Workflow, in which case it's called an External Signal. ```go // ... func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error { - //... + ... signal := MySignal { Message: "Some important data", } @@ -167,16 +292,18 @@ func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error } ``` -### Signal-With-Start {#signal-with-start} +When an External Signal is sent: +- A [SignalExternalWorkflowExecutionInitiated](/references/events#signalexternalworkflowexecutioninitiated) Event appears in the sender's Event History. +- A [WorkflowExecutionSignaled](/references/events#workflowexecutionsignaled) Event appears in the recipient's Event History. -**How to use Signal-With-Start using the Go SDK.** +#### Signal-With-Start {#signal-with-start} Signal-With-Start is used from the Client. It takes a Workflow Id, Workflow arguments, a Signal name, and Signal arguments. If there's a Workflow running with the given Workflow Id, it will be signaled. If there isn't, a new Workflow will be started and immediately signaled. -Use the `SignalWithStartWorkflow()` API on the Go SDK Temporal Client to start a Workflow Execution (if not already running) and pass it the Signal at the same time. +Use the [`Client.SignalWithStartWorkflow`](https://pkg.go.dev/go.temporal.io/sdk/client#Client.SignalWithStartWorkflow) API to start a Workflow Execution (if not already running) and pass it the Signal at the same time. Because the Workflow Execution might not exist, this API does not take a Run ID as a parameter @@ -192,309 +319,265 @@ if err != nil { } ``` -## Queries {#queries} +### Send an Update {#send-update-from-client} -A [Query](/encyclopedia/workflow-message-passing#sending-queries) is a synchronous operation that is used to get the state of a Workflow Execution. +An Update is a synchronous, blocking call that can change Workflow state, control its flow, and return a result. -### How to define a Query {#define-query} +A Client sending an Update must wait until the Server delivers the Update to a Worker. +Workers must be available and responsive. +If you need a response as soon as the Server receives the request, use a Signal instead. +Also note that you can't send Updates to other Workflow Executions or perform an Update equivalent of Signal-With-Start. -**How to define a Query using the Go SDK.** +- `WorkflowExecutionUpdateAccepted` is added to the Event History when the Worker confirms that the Update passed validation. +- `WorkflowExecutionUpdateCompleted` is added to the Event History when the Worker confirms that the Update has finished. -A Query has a name and can have arguments. +Use the [`Client.UpdateWorkflow`](https://pkg.go.dev/go.temporal.io/sdk/client#Client.UpdateWorkflow) API to send an Update to a Workflow Execution. -- The name, also called a Query type, is a string. -- The arguments must be [serializable](/dataconversion). +You must provide the Workflow Id, but specifying a Run Id is optional. +If you supply only the Workflow Id (and provide an empty string as the Run Id param), the running Workflow Execution receives the Update. -In Go, a Query type, also called a Query name, is a `string` value. +You must provide a `WaitForStage` when calling `UpdateWorkflow()`. +This parameter controls what stage the update must reach before a handle is returned to the caller. If `WaitForStage` is set to `WorkflowUpdateStageCompleted` the handle is returned after the update completes; if `WaitForStage` is set to `WorkflowUpdateStageAccepted` the handle is returned after the Update is accepted (i.e. after the validator has run, if there is a validator). ```go -queryType := "your_query_name" +updateHandle, err := temporalClient.UpdateWorkflow(context.Background(), client.UpdateWorkflowOptions{ + WorkflowID: we.GetID(), + RunID: we.GetRunID(), + UpdateName: message.SetLanguageUpdate, + WaitForStage: client.WorkflowUpdateStageAccepted, + Args: []interface{}{message.Chinese}, +}) +if err != nil { + log.Fatalf("Unable to update workflow: %v", err) +} +var previousLang message.Language +err = updateHandle.Get(context.Background(), &previousLang) +if err != nil { + log.Fatalf("Unable to get update result: %v", err) +} ``` -### Handle a Query {#handle-query} +## Message handler patterns {#message-handler-patterns} -**How to handle a Query using the Go SDK.** +This section covers common write operations, such as Signal and Update handlers. +It doesn't apply to pure read operations, like Queries or Update Validators. -Queries are handled by your Workflow. +:::tip -Don't include any logic that causes [Command](/workflows#command) generation within a Query handler (such as executing Activities). -Including such logic causes unexpected behavior. +For additional information, see [Inject work into the main Workflow](/encyclopedia/workflow-message-passing#injecting-work-into-main-workflow), [Ensuring your messages are processed exactly once](/encyclopedia/workflow-message-passing#exactly-once-message-processing), and [this sample](https://github.com/temporalio/samples-typescript/blob/main/pdates-and-signals/safe-message-handlers/README.md) demonstrating safe blocking message handling. -Use the `SetQueryHandler` API from the `go.temporal.io/sdk/workflow` package to set a Query Handler that listens for a Query by name. +::: -The handler must be a function that returns two values: +### Blocking handlers {#blocking-handlers} -1. A serializable result -2. An error +Signal and Update handlers can block. +This allows you to use Activities, Child Workflows, durable [workflow.Sleep](https://pkg.go.dev/go.temporal.io/sdk/workflow#Sleep) Timers, [`workflow.Await`](https://pkg.go.dev/go.temporal.io/sdk/workflow#Await) conditions, etc. +This expands the possibilities for what can be done by a handler but it also means that handler executions and your main Workflow method are all running concurrently, with switching occurring between them at await calls. -The handler function can receive any number of input parameters, but all input parameters must be serializable. -The following sample code sets up a Query Handler that handles the `current_state` Query type: - -```go -func YourWorkflow(ctx workflow.Context, input string) error { - currentState := "started" // This could be any serializable struct. - queryType := "current_state" - err := workflow.SetQueryHandler(ctx, queryType, func() (string, error) { - return currentState, nil - }) - if err != nil { - currentState = "failed to register query handler" - return err - } - // Your normal Workflow code begins here, and you update the currentState as the code makes progress. - currentState = "waiting timer" - err = workflow.NewTimer(ctx, time.Hour).Get(ctx, nil) - if err != nil { - currentState = "timer failed" - return err - } - currentState = "waiting activity" - ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ - ScheduleToStartTimeout: time.Minute, - StartToCloseTimeout: time.Minute, - }) - err = workflow.ExecuteActivity(ctx, YourActivity, "your_input").Get(ctx, nil) - if err != nil { - currentState = "activity failed" - return err - } - currentState = "done" - return nil -} -``` +It's essential to understand the things that could go wrong in order to use blocking handlers safely. +See [Workflow message passing](/encyclopedia/workflow-message-passing) for guidance on safe usage of blocking Signal and Update handlers, and the [Controlling handler concurrency](#control-handler-concurrency) and [Waiting for message handlers to finish](#wait-for-message-handlers) sections below. -For example, suppose your query handler function takes two parameters: +The following code modifies the Update handler from earlier on in this page. +The Update handler now makes a blocking call to execute an Activity: ```go -err := workflow.SetQueryHandler(ctx, "current_state", func(prefix string, suffix string) (string, error) { - return prefix + currentState + suffix, nil -}) +func GreetingWorkflow(ctx workflow.Context) error { + language := English + + err = workflow.SetUpdateHandler(ctx, SetLanguageUpdate, func(ctx workflow.Context, newLanguage Language) (Language, error) { + if _, ok := greeting[newLanguage]; !ok { + ao := workflow.ActivityOptions{ + StartToCloseTimeout: 10 * time.Second, + } + ctx = workflow.WithActivityOptions(ctx, ao) + + var greeting string + err := workflow.ExecuteActivity(ctx, CallGreetingService, newLanguage).Get(ctx, &greeting) + if err != nil { + return nil, err + } + greeting[newLanguage] = greeting + } + var previousLanguage Language + previousLanguage, language = language, newLanguage + return previousLanguage, nil + }) + ... +} ``` -### Send a Query {#send-query} +### Add blocking wait conditions {#block-with-wait} -**How to send a Query using the Go SDK.** +Sometimes, blocking Signal or Update handlers need to meet certain conditions before they should continue. +You can use [`workflow.Await`](https://pkg.go.dev/go.temporal.io/sdk/workflow#Await) to prevent the code from proceeding until a condition is true. +You specify the condition by passing a function that returns `true` or `false`. +This is an important feature that helps you control your handler logic. -Queries are sent from a Temporal Client. +Here are three important use cases for `Workflow.await`: -Use the `QueryWorkflow()` API or the `QueryWorkflowWithOptions` API on the Temporal Client to send a Query to a Workflow Execution. +- Waiting until a specific Update has arrived. +- Waiting in a handler until it is appropriate to continue. +- Waiting in the main Workflow until all active handlers have finished. ```go -// ... -response, err := temporalClient.QueryWorkflow(context.Background(), workflowID, runID, queryType) -if err != nil { - // ... -} -// ... +err = workflow.SetUpdateHandler(ctx, "UpdateHandler", func(ctx workflow.Context, input UpdateInput) error { + workflow.Await(ctx, updateUnblockedFunc) + ... +}) ``` +This is necessary if your Update handlers require something in the main Workflow function to be done first, since an Update handler can execute concurrently with the main Workflow function. + +You can also use `Workflow.await` anywhere else in the handler to wait for a specific condition to become true. +This allows you to write handlers that pause at multiple points, each time waiting for a required condition to become true. + +#### Ensure your handlers finish before the Workflow completes {#wait-for-message-handlers} -You can pass an arbitrary number of arguments to the `QueryWorkflow()` function. +`Workflow.await` can ensure your handler completes before a Workflow finishes. +When your Workflow uses blocking Update handlers, your main Workflow method can return or Continue-as-New while a handler is still waiting on an async task, such as an Activity. +The Workflow completing may interrupt the handler before it finishes crucial work and cause client errors when trying to retrieve Update results. +Use [`workflow.Await`](https://pkg.go.dev/go.temporal.io/sdk/workflow#Await) to wait for [`AllHandlersFinished`](https://pkg.go.dev/go.temporal.io/sdk/workflow#AllHandlersFinished) to return `true` to address this problem and allow your Workflow to end smoothly: ```go -// ... -response, err := temporalClient.QueryWorkflow(context.Background(), workflowID, runID, queryType, "foo", "baz") -if err != nil { - // ... +func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error { + ... + err = workflow.Await(ctx, func() bool { + return workflow.AllHandlersFinished(ctx) + }) + return nil } -// ... ``` -The value of `response` returned by the Query needs to be decoded into `result`. -Because this is a future, use `Get()` on `response` to get the result, such as a string in this example. +By default, your Worker will log a warning if you allow your Workflow Execution to finish with unfinished Update handler executions. +You can silence these warnings on a per-handler basis by setting `UnfinishedPolicy` field on [`workflow.UpdateHandlerOptions`](https://pkg.go.dev/go.temporal.io/sdk/workflow#UpdateHandlerOptions) struct: ```go -var result string -if err != response.Get(&result); err != nil { - // ... -} -log.Println("Received Query result. Result: " + result) + err = workflow.SetUpdateHandlerWithOptions(ctx, UpdateHandlerName, UpdateFunc, workflow.UpdateHandlerOptions{ + UnfinishedPolicy: workflow.HandlerUnfinishedPolicyAbandon, + }) ``` -## Updates {#updates} +See [Finishing handlers before the Workflow completes](/encyclopedia/workflow-message-passing#finishing-message-handlers) for more information. -An [Update](/encyclopedia/workflow-message-passing#sending-updates) is an operation that can mutate the state of a Workflow Execution and return a response. -### Define an Update {#define-update} +#### Use `workflow.Mutex` to prevent concurrent handler execution {#control-handler-concurrency} +See [Message handler concurrency](/encyclopedia/workflow-message-passing#message-handler-concurrency). -**How to define an Update using the Go SDK.** +Concurrent processes can interact in unpredictable ways. +Incorrectly written [concurrent message-passing](/encyclopedia/workflow-message-passing#message-handler-concurrency) code may not work correctly when multiple handler instances run simultaneously. +Here's an example of a pathological case: -In Go, you define an Update type, also known as an Update name, as a `string` value. -You must ensure the arguments and result are [serializable](/dataconversion). -When sending and receiving the Update, use the Update name as an identifier. -The name does not link to the data type(s) sent with the Update. -Ensure that every Workflow listening to the same Update name can handle the same Update arguments. +```go +// ... +func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error { + ... + err := workflow.SetUpdateHandler(ctx, "BadUpdateHandler", func(ctx workflow.Context) error { + ao := workflow.ActivityOptions{ + StartToCloseTimeout: 10 * time.Second, + } + ctx = workflow.WithActivityOptions(ctx, ao) + + var result Data + err := workflow.ExecuteActivity(ctx, FetchData, name).Get(ctx, &result) + x = result.x + // 🐛🐛 Bug!! If multiple instances of this handler are executing concurrently, then + // there may be times when the Workflow has self.x from one Activity execution and self.y from another. + err = workflow.Sleep(ctx, time.Second) + if err != nil { + return err + } + y = result.y + }) + ... +} +``` -
- - View the source code - {' '} - in the context of the rest of the application code. -
+Coordinating access with `workflow.Mutex` corrects this code. +Locking makes sure that only one handler instance can execute a specific section of code at any given time: ```go -// YourUpdateName holds a string value used to correlate Updates. -const YourUpdateName = "your_update_name" -// ... -func YourUpdatableWorkflow(ctx workflow.Context, param WFParam) (WFResult, error) { -// ... - err := workflow.SetUpdateHandler(ctx, YourUpdateName, func(ctx workflow.Context, arg YourUpdateArg) (YourUpdateResult, error) { -// ... +func YourWorkflowDefinition(ctx workflow.Context, param YourWorkflowParam) error { + ... + err := workflow.SetUpdateHandler(ctx, "SafeUpdateHandler", func(ctx workflow.Context) error { + err := mutex.Lock(ctx) + if err != nil { + return err + } + defer mutex.Unlock() + ao := workflow.ActivityOptions{ + StartToCloseTimeout: 10 * time.Second, + } + ctx = workflow.WithActivityOptions(ctx, ao) + + var result Data + err := workflow.ExecuteActivity(ctx, FetchData, name).Get(ctx, &result) + x = data.x + err = workflow.Sleep(ctx, time.Second) + if err != nil { + return err + } + self.y = data.y }) - if err != nil { - return WFResult{}, err - } -// ... + ... } + ``` -### Handle an Update {#handle-update} +## Troubleshooting -**How to handle an Update using the Go SDK.** +See [Exceptions in message handlers](/encyclopedia/workflow-message-passing#exceptions) for a non–Go-specific discussion of this topic. -Register an Update handler for a given name using the [SetUpdateHandler](https://pkg.go.dev/go.temporal.io/sdk/workflow#SetUpdateHandler) API from the `go.temporal.io/sdk/workflow` package. -The handler function can accept multiple serializable input parameters, but we recommend using only a single parameter. -This practice enables you to add fields in future versions while maintaining backward compatibility. -You must include a `workflow.Context` parameter in the first position of the function. -The function can return either a serializable value with an error or just an error. -The Workflow's WorkflowPanicPolicy configuration determines how panics are handled inside the Handler function. -WorkflowPanicPolicy is set in the Worker Options. +When sending a Signal, Update, or Query to a Workflow, your Client might encounter the following errors: -Update handlers, unlike Query handlers, can block and change Workflow state. +- **The Client can't contact the server** -
- - View the source code - {' '} - in the context of the rest of the application code. -
+- **The Workflow does not exist** -```go -// ... -func YourUpdatableWorkflow(ctx workflow.Context, param WFParam) (WFResult, error) { - counter := param.StartCount - err := workflow.SetUpdateHandler(ctx, YourUpdateName, func(ctx workflow.Context, arg YourUpdateArg) (YourUpdateResult, error) { - counter += arg.Add - result := YourUpdateResult{ - Total: counter, - } - return result, nil - }) - if err != nil { - return WFResult{}, err - } -// ... -} -``` -### Set an Update validator function {#validator-function} +Unlike Signals, for Queries and Updates, the Client waits for a response from the Worker. +If an issue occurs during the handler execution by the Worker, the Client may receive an exception. -**How to set an Update validator function using the Go SDK.** -Validate certain aspects of the data sent to the Workflow using an Update validator function. -For instance, a counter Workflow might never want to accept a non-positive number. -Invoke the `SetUpdateHandlerWithOptions` API and define a validator function as one of the options. +### Problems when sending an Update +- **There is no Workflow Worker polling the Task Queue** -When you use a Validator function, the Worker receives the Update first, before any Events are written to the Event History. -If the Update is rejected, it's not recorded in the Event History. -If it's accepted, the `WorkflowExecutionUpdateAccepted` Event occurs. -Afterwards, the Worker executes the accepted Update and, upon completion, a `WorkflowExecutionUpdateCompleted` Event gets written into the Event History. -The Validator function, unlike the Update Handler, can not change the state of the Workflow. + Your request will be retried by the SDK Client until the calling context is cancelled. -The platform treats a panic in the Validator function as a rejection of the Update. +- **Update failed.** -
- - View the source code - {' '} - in the context of the rest of the application code. -
+ Update failures are like [Workflow failures](/references/failures). + Issues that cause a Workflow failure in the main method also cause Update failures in the Update handler. + These might include: -```go -// UpdatableWorkflowWithValidator is a Workflow Definition. -// This Workflow Definition has an Update handler that uses the isPositive() validator function. -// After setting the Update handler it sleeps for 1 minute. -// Updates can be sent to the Workflow during this time. -func UpdatableWorkflowWithValidator(ctx workflow.Context, param WFParam) (WFResult, error) { - counter := param.StartCount - err := workflow.SetUpdateHandlerWithOptions( - ctx, YourValidatedUpdateName, - func(ctx workflow.Context, arg YourUpdateArg) (YourUpdateResult, error) { - counter += arg.Add - result := YourUpdateResult{ - Total: counter, - } - return result, nil - }, - // Set the isPositive validator. - workflow.UpdateHandlerOptions{Validator: isPositive}, - ) - if err != nil { - return WFResult{}, err - } - if err := workflow.Sleep(ctx, time.Minute); err != nil { - return WFResult{}, err - } - return WFResult{Total: counter}, nil -} + - A failed Child Workflow + - A failed Activity if the activity retries have been set to a finite number + - The Workflow author returning an `error` + - A panic in the handler, depending on the `WorkflowPanicPolicy` -// isPositive is a validator function. -// It returns an error if the int value is below 1. -// This function can not change the state of the Workflow. -// workflow.Context can be used to log -func isPositive(ctx workflow.Context, u YourUpdateArg) error { - log := workflow.GetLogger(ctx) - if u.Add < 1 { - log.Debug("Rejecting non-positive number, positive integers only", "UpdateValue", u.Add) - return fmt.Errorf("addend must be a positive integer (%v)", u.Add) - } - log.Debug("Accepting Update", "UpdateValue", u.Add) - return nil -} -``` +- **The handler caused the Workflow Task to fail** + A [Workflow Task Failure](/references/failures) causes the server to retry Workflow Tasks indefinitely. What happens to your Update request depends on its stage: + - If the request hasn't been accepted by the server, you receive a [`FAILED_PRECONDITION`](https://pkg.go.dev/go.temporal.io/api/serviceerror#FailedPrecondition) error. + - If the request has been accepted, it is durable. + Once the Workflow is healthy again after a code deploy, use a [`WorkflowUpdateHandle`](https://pkg.go.dev/go.temporal.io/sdk/client#WorkflowUpdateHandle) to fetch the Update result. -### Send an Update from a Temporal Client {#send-update-from-client} +- **The Workflow finished while the Update handler execution was in progress**: + You'll receive a [`ServiceError`](https://pkg.go.dev/go.temporal.io/api/serviceerror#ServiceError) "workflow execution already completed"`. -**How to send an Update from a Temporal Client using the Go SDK.** + This will happen if the Workflow finished while the Update handler execution was in progress, for example because -Invoke the `UpdateWorkflow()` method on an instance of the [Go SDK Temporal Client](https://pkg.go.dev/go.temporal.io/sdk/client#Client) to dispatch an [Update](/encyclopedia/workflow-message-passing#sending-updates) to a Workflow Execution. + - The Workflow was canceled or failed. -You must provide the Workflow Id, but specifying a Run Id is optional. -If you supply only the Workflow Id (and provide an empty string as the Run Id param), the currently running Workflow Execution receives the Update. + - The Workflow completed normally or continued-as-new and the Workflow author did not [wait for handlers to be finished](/encyclopedia/workflow-message-passing#finishing-message-handlers). -You must provide a `WaitForStage` when calling `UpdateWorkflow()`. -This parameter controls what stage the update must reach before a handle is returned to the caller. If `WaitForStage` is set to `WorkflowUpdateStageCompleted` the handle is returned after the update completes, if `WaitForStage` is set to `WorkflowUpdateStageAccepted` the handle is returned after the update is accepted (i.e. the validator has run). +### Problems when sending a Query -
- - View the source code - {' '} - in the context of the rest of the application code. -
+- **There is no Workflow Worker polling the Task Queue** -```go -func main() { - // ... - // Set the Update argument values. - updateArg := YourUpdateArg{ - Add: 5, - } - // Call the UpdateWorkflow API. - // A blank RunID means that the Update is routed to the most recent Workflow Run of the specified Workflow ID. - updateHandle, err := temporalClient.UpdateWorkflow(context.Background(), client.UpdateWorkflowOptions{ - WorkflowID: "your-workflow-id", - UpdateName: YourUpdateName, - WaitForStage: client.WorkflowUpdateStageCompleted, - Args: []interface{}{updateArg}, - }) - if err != nil { - log.Fatalln("Error issuing Update request", err) - } - // Get the result of the Update. - var updateResult YourUpdateResult - err = updateHandle.Get(context.Background(), &updateResult) - if err != nil { - log.Fatalln("Update encountered an error", err) - } - log.Println("Update succeeded, new total: ", updateResult.Total) -} -``` + You'll receive a [`ServiceError`](https://pkg.go.dev/go.temporal.io/api/serviceerror#ServiceError) on which the `status` is `FAILED_PRECONDITION`. + +- **Query failed.** + You'll receive a [`QueryFailed`](https://pkg.go.dev/go.temporal.io/api/serviceerror#QueryFailed) error. + Any panic in a Query handler will trigger this error. + This differs from Signal and Update, where panics can lead to Workflow Task Failure instead. + +- **The handler caused the Workflow Task to fail.** + This would happen, for example, if the Query handler blocks the thread for too long without yielding. diff --git a/docs/encyclopedia/application-message-passing.mdx b/docs/encyclopedia/application-message-passing.mdx index 9aa30806d6..c17b3539b8 100644 --- a/docs/encyclopedia/application-message-passing.mdx +++ b/docs/encyclopedia/application-message-passing.mdx @@ -276,12 +276,12 @@ See the links below for examples of solving this in your SDK. See examples of the above patterns. - + + - ### Update Validators {#update-validators} @@ -300,12 +300,12 @@ Like Queries, Validators are not allowed to block. Once the Update handler is finished and has returned a value, the operation is considered Completed. - + + - @@ -340,7 +340,7 @@ In Go, returning an error behaves like an [Application Failure](/references/fail Use these links to see a simple Signal handler. - + @@ -353,7 +353,7 @@ Use these links to see a simple Signal handler. Use these links to see a simple update handler. - + @@ -367,7 +367,7 @@ Use these links to see a simple update handler. Author queries using these per-language guides. - +