peridot/vendor/go.temporal.io/sdk/workflow/workflow.go

490 lines
25 KiB
Go
Raw Permalink Normal View History

2022-07-07 20:11:50 +00:00
// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package workflow
import (
"errors"
"go.temporal.io/sdk/converter"
"go.temporal.io/sdk/internal"
"go.temporal.io/sdk/internal/common/metrics"
"go.temporal.io/sdk/log"
)
type (
// ChildWorkflowFuture represents the result of a child workflow execution
ChildWorkflowFuture = internal.ChildWorkflowFuture
// Type identifies a workflow type.
Type = internal.WorkflowType
// Execution Details.
Execution = internal.WorkflowExecution
// Version represents a change version. See GetVersion call.
Version = internal.Version
// ChildWorkflowOptions stores all child workflow specific parameters that will be stored inside of a Context.
ChildWorkflowOptions = internal.ChildWorkflowOptions
// RegisterOptions consists of options for registering a workflow
RegisterOptions = internal.RegisterWorkflowOptions
// Info information about currently executing workflow
Info = internal.WorkflowInfo
// ContinueAsNewError can be returned by a workflow implementation function and indicates that
// the workflow should continue as new with the same WorkflowID, but new RunID and new history.
ContinueAsNewError = internal.ContinueAsNewError
)
// ExecuteActivity requests activity execution in the context of a workflow.
// Context can be used to pass the settings for this activity.
// For example: task queue that this need to be routed, timeouts that need to be configured.
// Use ActivityOptions to pass down the options.
//
// ao := ActivityOptions{
// TaskQueue: "exampleTaskQueue",
// ScheduleToStartTimeout: 10 * time.Second,
// StartToCloseTimeout: 5 * time.Second,
// ScheduleToCloseTimeout: 10 * time.Second,
// HeartbeatTimeout: 0,
// }
// ctx := WithActivityOptions(ctx, ao)
//
// Or to override a single option
//
// ctx := WithTaskQueue(ctx, "exampleTaskQueue")
//
// Input activity is either an activity name (string) or a function representing an activity that is getting scheduled.
// Note that the function implementation is ignored by this call.
// It uses function to extract activity type string from it.
// Input args are the arguments that need to be passed to the scheduled activity.
// To call an activity that is a member of a structure use the function reference with nil receiver.
// For example if an activity is defined as:
//
// type Activities struct {
// ... // members
// }
//
// func (a *Activities) Activity1() (string, error) {
// ...
// }
//
// Then a workflow can invoke it as:
//
// var a *Activities
// workflow.ExecuteActivity(ctx, a.Activity1)
//
// If the activity failed to complete then the future get error would indicate the failure.
// The error will be of type *ActivityError. It will have important activity information and actual error that caused
// activity failure. Use errors.Unwrap to get this error or errors.As to check it type which can be one of
// *ApplicationError, *TimeoutError, *CanceledError, or *PanicError.
//
// You can cancel the pending activity using context(workflow.WithCancel(ctx)) and that will fail the activity with
// *CanceledError set as cause for *ActivityError. The context in the activity only becomes aware of the cancellation
// when a heartbeat is sent to the server. Since heartbeats may be batched internally, this could take up to the
// HeartbeatTimeout to appear or several minutes by default if that value is not set.
//
// ExecuteActivity immediately returns a Future that can be used to block waiting for activity result or failure.
func ExecuteActivity(ctx Context, activity interface{}, args ...interface{}) Future {
return internal.ExecuteActivity(ctx, activity, args...)
}
// ExecuteLocalActivity requests to run a local activity. A local activity is like a regular activity with some key
// differences:
//
// • Local activity is scheduled and run by the workflow worker locally.
//
// • Local activity does not need Temporal server to schedule activity task and does not rely on activity worker.
//
// • No need to register local activity.
//
// • Local activity is for short living activities (usually finishes within seconds).
//
// • Local activity cannot heartbeat.
//
// Context can be used to pass the settings for this local activity.
// For now there is only one setting for timeout to be set:
// lao := LocalActivityOptions{
// ScheduleToCloseTimeout: 5 * time.Second,
// }
// ctx := WithLocalActivityOptions(ctx, lao)
// The timeout here should be relative shorter than the WorkflowTaskTimeout of the workflow. If you need a
// longer timeout, you probably should not use local activity and instead should use regular activity. Local activity is
// designed to be used for short living activities (usually finishes within seconds).
//
// Input args are the arguments that will to be passed to the local activity. The input args will be hand over directly
// to local activity function without serialization/deserialization because we don't need to pass the input across process
// boundary. However, the result will still go through serialization/deserialization because we need to record the result
// as history to temporal server so if the workflow crashes, a different worker can replay the history without running
// the local activity again.
//
// If the activity failed to complete then the future get error would indicate the failure.
// The error will be of type *ActivityError. It will have important activity information and actual error that caused
// activity failure. Use errors.Unwrap to get this error or errors.As to check it type which can be one of
// *ApplicationError, *TimeoutError, *CanceledError, or *PanicError.
//
// You can cancel the pending activity using context(workflow.WithCancel(ctx)) and that will fail the activity with
// *CanceledError set as cause for *ActivityError.
//
// ExecuteLocalActivity returns Future with local activity result or failure.
func ExecuteLocalActivity(ctx Context, activity interface{}, args ...interface{}) Future {
return internal.ExecuteLocalActivity(ctx, activity, args...)
}
// ExecuteChildWorkflow requests child workflow execution in the context of a workflow.
// Context can be used to pass the settings for the child workflow.
// For example: task queue that this child workflow should be routed, timeouts that need to be configured.
// Use ChildWorkflowOptions to pass down the options.
// cwo := ChildWorkflowOptions{
// WorkflowExecutionTimeout: 10 * time.Minute,
// WorkflowTaskTimeout: time.Minute,
// }
// ctx := WithChildOptions(ctx, cwo)
// Input childWorkflow is either a workflow name or a workflow function that is getting scheduled.
// Input args are the arguments that need to be passed to the child workflow function represented by childWorkflow.
//
// If the child workflow failed to complete then the future get error would indicate the failure.
// The error will be of type *ChildWorkflowExecutionError. It will have important child workflow information and actual error that caused
// child workflow failure. Use errors.Unwrap to get this error or errors.As to check it type which can be one of
// *ApplicationError, *TimeoutError, or *CanceledError.
//
// You can cancel the pending child workflow using context(workflow.WithCancel(ctx)) and that will fail the workflow with
// *CanceledError set as cause for *ChildWorkflowExecutionError.
//
// ExecuteChildWorkflow returns ChildWorkflowFuture.
func ExecuteChildWorkflow(ctx Context, childWorkflow interface{}, args ...interface{}) ChildWorkflowFuture {
return internal.ExecuteChildWorkflow(ctx, childWorkflow, args...)
}
// GetInfo extracts info of a current workflow from a context.
func GetInfo(ctx Context) *Info {
return internal.GetWorkflowInfo(ctx)
}
// GetLogger returns a logger to be used in workflow's context
func GetLogger(ctx Context) log.Logger {
return internal.GetLogger(ctx)
}
// GetMetricsHandler returns a metrics handler to be used in workflow's context.
// This handler does not record metrics during replay.
func GetMetricsHandler(ctx Context) metrics.Handler {
return internal.GetMetricsHandler(ctx)
}
// RequestCancelExternalWorkflow can be used to request cancellation of an external workflow.
// Input workflowID is the workflow ID of target workflow.
// Input runID indicates the instance of a workflow. Input runID is optional (default is ""). When runID is not specified,
// then the currently running instance of that workflowID will be used.
// By default, the current workflow's namespace will be used as target namespace. However, you can specify a different namespace
// of the target workflow using the context like:
// ctx := WithWorkflowNamespace(ctx, "namespace")
// RequestCancelExternalWorkflow return Future with failure or empty success result.
func RequestCancelExternalWorkflow(ctx Context, workflowID, runID string) Future {
return internal.RequestCancelExternalWorkflow(ctx, workflowID, runID)
}
// SignalExternalWorkflow can be used to send signal info to an external workflow.
// Input workflowID is the workflow ID of target workflow.
// Input runID indicates the instance of a workflow. Input runID is optional (default is ""). When runID is not specified,
// then the currently running instance of that workflowID will be used.
// By default, the current workflow's namespace will be used as target namespace. However, you can specify a different namespace
// of the target workflow using the context like:
// ctx := WithWorkflowNamespace(ctx, "namespace")
// SignalExternalWorkflow return Future with failure or empty success result.
func SignalExternalWorkflow(ctx Context, workflowID, runID, signalName string, arg interface{}) Future {
return internal.SignalExternalWorkflow(ctx, workflowID, runID, signalName, arg)
}
// GetSignalChannel returns channel corresponding to the signal name.
func GetSignalChannel(ctx Context, signalName string) ReceiveChannel {
return internal.GetSignalChannel(ctx, signalName)
}
// SideEffect executes the provided function once, records its result into the workflow history. The recorded result on
// history will be returned without executing the provided function during replay. This guarantees the deterministic
// requirement for workflow as the exact same result will be returned in replay.
// Common use case is to run some short non-deterministic code in workflow, like getting random number or new UUID.
// The only way to fail SideEffect is to panic which causes workflow task failure. The workflow task after timeout is
// rescheduled and re-executed giving SideEffect another chance to succeed.
//
// Caution: do not use SideEffect to modify closures. Always retrieve result from SideEffect's encoded return value.
// For example this code is BROKEN:
// // Bad example:
// var random int
// workflow.SideEffect(func(ctx workflow.Context) interface{} {
// random = rand.Intn(100)
// return nil
// })
// // random will always be 0 in replay, thus this code is non-deterministic
// if random < 50 {
// ....
// } else {
// ....
// }
// On replay the provided function is not executed, the random will always be 0, and the workflow could takes a
// different path breaking the determinism.
//
// Here is the correct way to use SideEffect:
// // Good example:
// encodedRandom := SideEffect(func(ctx workflow.Context) interface{} {
// return rand.Intn(100)
// })
// var random int
// encodedRandom.Get(&random)
// if random < 50 {
// ....
// } else {
// ....
// }
func SideEffect(ctx Context, f func(ctx Context) interface{}) converter.EncodedValue {
return internal.SideEffect(ctx, f)
}
// MutableSideEffect executes the provided function once, then it looks up the history for the value with the given id.
// If there is no existing value, then it records the function result as a value with the given id on history;
// otherwise, it compares whether the existing value from history has changed from the new function result by calling the
// provided equals function. If they are equal, it returns the value without recording a new one in history;
// otherwise, it records the new value with the same id on history.
//
// Caution: do not use MutableSideEffect to modify closures. Always retrieve result from MutableSideEffect's encoded
// return value.
//
// The difference between MutableSideEffect() and SideEffect() is that every new SideEffect() call in non-replay will
// result in a new marker being recorded on history. However, MutableSideEffect() only records a new marker if the value
// changed. During replay, MutableSideEffect() will not execute the function again, but it will return the exact same
// value as it was returning during the non-replay run.
//
// One good use case of MutableSideEffect() is to access dynamically changing config without breaking determinism.
func MutableSideEffect(ctx Context, id string, f func(ctx Context) interface{}, equals func(a, b interface{}) bool) converter.EncodedValue {
return internal.MutableSideEffect(ctx, id, f, equals)
}
// DefaultVersion is a version returned by GetVersion for code that wasn't versioned before
const DefaultVersion Version = internal.DefaultVersion
// GetVersion is used to safely perform backwards incompatible changes to workflow definitions.
// It is not allowed to update workflow code while there are workflows running as it is going to break
// determinism. The solution is to have both old code that is used to replay existing workflows
// as well as the new one that is used when it is executed for the first time.
// GetVersion returns maxSupported version when is executed for the first time. This version is recorded into the
// workflow history as a marker event. Even if maxSupported version is changed the version that was recorded is
// returned on replay. DefaultVersion constant contains version of code that wasn't versioned before.
// For example initially workflow has the following code:
// err = workflow.ExecuteActivity(ctx, foo).Get(ctx, nil)
// it should be updated to
// err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)
// The backwards compatible way to execute the update is
// v := GetVersion(ctx, "fooChange", DefaultVersion, 1)
// if v == DefaultVersion {
// err = workflow.ExecuteActivity(ctx, foo).Get(ctx, nil)
// } else {
// err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)
// }
//
// Then bar has to be changed to baz:
// v := GetVersion(ctx, "fooChange", DefaultVersion, 2)
// if v == DefaultVersion {
// err = workflow.ExecuteActivity(ctx, foo).Get(ctx, nil)
// } else if v == 1 {
// err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)
// } else {
// err = workflow.ExecuteActivity(ctx, baz).Get(ctx, nil)
// }
//
// Later when there are no workflow executions running DefaultVersion the correspondent branch can be removed:
// v := GetVersion(ctx, "fooChange", 1, 2)
// if v == 1 {
// err = workflow.ExecuteActivity(ctx, bar).Get(ctx, nil)
// } else {
// err = workflow.ExecuteActivity(ctx, baz).Get(ctx, nil)
// }
//
// It is recommended to keep the GetVersion() call even if single branch is left:
// GetVersion(ctx, "fooChange", 2, 2)
// err = workflow.ExecuteActivity(ctx, baz).Get(ctx, nil)
//
// The reason to keep it is: 1) it ensures that if there is older version execution still running, it will fail here
// and not proceed; 2) if you ever need to make more changes for “fooChange”, for example change activity from baz to qux,
// you just need to update the maxVersion from 2 to 3.
//
// Note that, you only need to preserve the first call to GetVersion() for each changeID. All subsequent call to GetVersion()
// with same changeID are safe to remove. However, if you really want to get rid of the first GetVersion() call as well,
// you can do so, but you need to make sure: 1) all older version executions are completed; 2) you can no longer use “fooChange”
// as changeID. If you ever need to make changes to that same part like change from baz to qux, you would need to use a
// different changeID like “fooChange-fix2”, and start minVersion from DefaultVersion again. The code would looks like:
//
// v := workflow.GetVersion(ctx, "fooChange-fix2", workflow.DefaultVersion, 1)
// if v == workflow.DefaultVersion {
// err = workflow.ExecuteActivity(ctx, baz, data).Get(ctx, nil)
// } else {
// err = workflow.ExecuteActivity(ctx, qux, data).Get(ctx, nil)
// }
func GetVersion(ctx Context, changeID string, minSupported, maxSupported Version) Version {
return internal.GetVersion(ctx, changeID, minSupported, maxSupported)
}
// SetQueryHandler sets the query handler to handle workflow query. The queryType specify which query type this handler
// should handle. The handler must be a function that returns 2 values. The first return value must be a serializable
// result. The second return value must be an error. The handler function could receive any number of input parameters.
// All the input parameter must be serializable. You should call workflow.SetQueryHandler() at the beginning of the workflow
// code. When client calls Client.QueryWorkflow() to temporal server, a task will be generated on server that will be dispatched
// to a workflow worker, which will replay the history events and then execute a query handler based on the query type.
// The query handler will be invoked out of the context of the workflow, meaning that the handler code must not use workflow
// context to do things like workflow.NewChannel(), workflow.Go() or to call any workflow blocking functions like
// Channel.Get() or Future.Get(). Trying to do so in query handler code will fail the query and client will receive
// QueryFailedError.
// Example of workflow code that support query type "current_state":
// func MyWorkflow(ctx workflow.Context, input string) error {
// currentState := "started" // this could be any serializable struct
// err := workflow.SetQueryHandler(ctx, "current_state", 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 = NewTimer(ctx, time.Hour).Get(ctx, nil)
// if err != nil {
// currentState = "timer failed"
// return err
// }
//
// currentState = "waiting activity"
// ctx = WithActivityOptions(ctx, myActivityOptions)
// err = ExecuteActivity(ctx, MyActivity, "my_input").Get(ctx, nil)
// if err != nil {
// currentState = "activity failed"
// return err
// }
// currentState = "done"
// return nil
// }
func SetQueryHandler(ctx Context, queryType string, handler interface{}) error {
return internal.SetQueryHandler(ctx, queryType, handler)
}
// IsReplaying returns whether the current workflow code is replaying.
//
// Warning! Never make commands, like schedule activity/childWorkflow/timer or send/wait on future/channel, based on
// this flag as it is going to break workflow determinism requirement.
// The only reasonable use case for this flag is to avoid some external actions during replay, like custom logging or
// metric reporting. Please note that Temporal already provide standard logging/metric via workflow.GetLogger(ctx) and
// workflow.GetMetricsHandler(ctx), and those standard mechanism are replay-aware and it will automatically suppress
// during replay. Only use this flag if you need custom logging/metrics reporting, for example if you want to log to
// kafka.
//
// Warning! Any action protected by this flag should not fail or if it does fail should ignore that failure or panic
// on the failure. If workflow don't want to be blocked on those failure, it should ignore those failure; if workflow do
// want to make sure it proceed only when that action succeed then it should panic on that failure. Panic raised from a
// workflow causes workflow task to fail and temporal server will rescheduled later to retry.
func IsReplaying(ctx Context) bool {
return internal.IsReplaying(ctx)
}
// HasLastCompletionResult checks if there is completion result from previous runs.
// This is used in combination with cron schedule. A workflow can be started with an optional cron schedule.
// If a cron workflow wants to pass some data to next schedule, it can return any data and that data will become
// available when next run starts.
// This HasLastCompletionResult() checks if there is such data available passing down from previous successful run.
func HasLastCompletionResult(ctx Context) bool {
return internal.HasLastCompletionResult(ctx)
}
// GetLastCompletionResult extract last completion result from previous run for this cron workflow.
// This is used in combination with cron schedule. A workflow can be started with an optional cron schedule.
// If a cron workflow wants to pass some data to next schedule, it can return any data and that data will become
// available when next run starts.
// This GetLastCompletionResult() extract the data into expected data structure.
// See TestWorkflowEnvironment.SetLastCompletionResult() for unit test support.
func GetLastCompletionResult(ctx Context, d ...interface{}) error {
return internal.GetLastCompletionResult(ctx, d...)
}
// GetLastError extracts the latest failure from any from previous run for this workflow, if one has failed. If none
// have failed, nil is returned.
//
// See TestWorkflowEnvironment.SetLastError() for unit test support.
func GetLastError(ctx Context) error {
return internal.GetLastError(ctx)
}
// UpsertSearchAttributes is used to add or update workflow search attributes.
// The search attributes can be used in query of List/Scan/Count workflow APIs.
// The key and value type must be registered on temporal server side;
// The value has to deterministic when replay;
// The value has to be Json serializable.
// UpsertSearchAttributes will merge attributes to existing map in workflow, for example workflow code:
// func MyWorkflow(ctx workflow.Context, input string) error {
// attr1 := map[string]interface{}{
// "CustomIntField": 1,
// "CustomBoolField": true,
// }
// workflow.UpsertSearchAttributes(ctx, attr1)
//
// attr2 := map[string]interface{}{
// "CustomIntField": 2,
// "CustomKeywordField": "seattle",
// }
// workflow.UpsertSearchAttributes(ctx, attr2)
// }
// will eventually have search attributes:
// map[string]interface{}{
// "CustomIntField": 2,
// "CustomBoolField": true,
// "CustomKeywordField": "seattle",
// }
// This is only supported when using ElasticSearch.
func UpsertSearchAttributes(ctx Context, attributes map[string]interface{}) error {
return internal.UpsertSearchAttributes(ctx, attributes)
}
// NewContinueAsNewError creates ContinueAsNewError instance
// If the workflow main function returns this error then the current execution is ended and
// the new execution with same workflow ID is started automatically with options
// provided to this function.
// ctx - use context to override any options for the new workflow like execution timeout, workflow task timeout, task queue.
// if not mentioned it would use the defaults that the current workflow is using.
// ctx := WithWorkflowExecutionTimeout(ctx, 30 * time.Minute)
// ctx := WithWorkflowTaskTimeout(ctx, time.Minute)
// ctx := WithWorkflowTaskQueue(ctx, "example-group")
// wfn - workflow function. for new execution it can be different from the currently running.
// args - arguments for the new workflow.
//
func NewContinueAsNewError(ctx Context, wfn interface{}, args ...interface{}) error {
return internal.NewContinueAsNewError(ctx, wfn, args...)
}
// IsContinueAsNewError return if the err is a ContinueAsNewError
func IsContinueAsNewError(err error) bool {
var continueAsNewErr *ContinueAsNewError
return errors.As(err, &continueAsNewErr)
}