peridot/vendor/go.temporal.io/sdk/internal/internal_workflow_client.go
2022-07-07 22:13:21 +02:00

1266 lines
45 KiB
Go

// 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 internal
import (
"context"
"errors"
"fmt"
"io"
"reflect"
"time"
"github.com/pborman/uuid"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
historypb "go.temporal.io/api/history/v1"
querypb "go.temporal.io/api/query/v1"
"go.temporal.io/api/serviceerror"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/converter"
"go.temporal.io/sdk/internal/common/metrics"
"go.temporal.io/sdk/internal/common/serializer"
"go.temporal.io/sdk/internal/common/util"
"go.temporal.io/sdk/log"
)
// Assert that structs do indeed implement the interfaces
var _ Client = (*WorkflowClient)(nil)
var _ NamespaceClient = (*namespaceClient)(nil)
const (
defaultGetHistoryTimeout = 65 * time.Second
)
var (
maxListArchivedWorkflowTimeout = time.Minute * 3
)
type (
// WorkflowClient is the client for starting a workflow execution.
WorkflowClient struct {
workflowService workflowservice.WorkflowServiceClient
connectionCloser io.Closer
namespace string
registry *registry
logger log.Logger
metricsHandler metrics.Handler
identity string
dataConverter converter.DataConverter
contextPropagators []ContextPropagator
workerInterceptors []WorkerInterceptor
interceptor ClientOutboundInterceptor
}
// namespaceClient is the client for managing namespaces.
namespaceClient struct {
workflowService workflowservice.WorkflowServiceClient
connectionCloser io.Closer
metricsHandler metrics.Handler
logger log.Logger
identity string
}
// WorkflowRun represents a started non child workflow
WorkflowRun interface {
// GetID return workflow ID, which will be same as StartWorkflowOptions.ID if provided.
GetID() string
// GetRunID return the first started workflow run ID (please see below) - empty string if no such run
GetRunID() string
// Get will fill the workflow execution result to valuePtr,
// if workflow execution is a success, or return corresponding,
// error. This is a blocking API.
Get(ctx context.Context, valuePtr interface{}) error
// NOTE: if the started workflow return ContinueAsNewError during the workflow execution, the
// return result of GetRunID() will be the started workflow run ID, not the new run ID caused by ContinueAsNewError,
// however, Get(ctx context.Context, valuePtr interface{}) will return result from the run which did not return ContinueAsNewError.
// Say ExecuteWorkflow started a workflow, in its first run, has run ID "run ID 1", and returned ContinueAsNewError,
// the second run has run ID "run ID 2" and return some result other than ContinueAsNewError:
// GetRunID() will always return "run ID 1" and Get(ctx context.Context, valuePtr interface{}) will return the result of second run.
// NOTE: DO NOT USE client.ExecuteWorkflow API INSIDE A WORKFLOW, USE workflow.ExecuteChildWorkflow instead
}
// workflowRunImpl is an implementation of WorkflowRun
workflowRunImpl struct {
workflowType string
workflowID string
firstRunID string
currentRunID *util.OnceCell
iterFn func(ctx context.Context, runID string) HistoryEventIterator
dataConverter converter.DataConverter
registry *registry
}
// HistoryEventIterator represents the interface for
// history event iterator
HistoryEventIterator interface {
// HasNext return whether this iterator has next value
HasNext() bool
// Next returns the next history events and error
// The errors it can return:
// - serviceerror.NotFound
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
Next() (*historypb.HistoryEvent, error)
}
// historyEventIteratorImpl is the implementation of HistoryEventIterator
historyEventIteratorImpl struct {
// whether this iterator is initialized
initialized bool
// local cached history events and corresponding consuming index
nextEventIndex int
events []*historypb.HistoryEvent
// token to get next page of history events
nexttoken []byte
// err when getting next page of history events
err error
// func which use a next token to get next page of history events
paginate func(nexttoken []byte) (*workflowservice.GetWorkflowExecutionHistoryResponse, error)
}
)
// ExecuteWorkflow starts a workflow execution and returns a WorkflowRun that will allow you to wait until this workflow
// reaches the end state, such as workflow finished successfully or timeout.
// The user can use this to start using a functor like below and get the workflow execution result, as EncodedValue
// Either by
// ExecuteWorkflow(options, "workflowTypeName", arg1, arg2, arg3)
// or
// ExecuteWorkflow(options, workflowExecuteFn, arg1, arg2, arg3)
// The current timeout resolution implementation is in seconds and uses math.Ceil(d.Seconds()) as the duration. But is
// subjected to change in the future.
// NOTE: the context.Context should have a fairly large timeout, since workflow execution may take a while to be finished
func (wc *WorkflowClient) ExecuteWorkflow(ctx context.Context, options StartWorkflowOptions, workflow interface{}, args ...interface{}) (WorkflowRun, error) {
// Default workflow ID
if options.ID == "" {
options.ID = uuid.New()
}
// Validate function and get name
if err := validateFunctionArgs(workflow, args, true); err != nil {
return nil, err
}
workflowType, err := getWorkflowFunctionName(wc.registry, workflow)
if err != nil {
return nil, err
}
// Set header before interceptor run
ctx = contextWithNewHeader(ctx)
// Run via interceptor
return wc.interceptor.ExecuteWorkflow(ctx, &ClientExecuteWorkflowInput{
Options: &options,
WorkflowType: workflowType,
Args: args,
})
}
// GetWorkflow gets a workflow execution and returns a WorkflowRun that will allow you to wait until this workflow
// reaches the end state, such as workflow finished successfully or timeout.
// The current timeout resolution implementation is in seconds and uses math.Ceil(d.Seconds()) as the duration. But is
// subjected to change in the future.
func (wc *WorkflowClient) GetWorkflow(ctx context.Context, workflowID string, runID string) WorkflowRun {
iterFn := func(fnCtx context.Context, fnRunID string) HistoryEventIterator {
return wc.GetWorkflowHistory(fnCtx, workflowID, fnRunID, true, enumspb.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT)
}
// The ID may not actually have been set - if not, we have to (lazily) ask the server for info about the workflow
// execution and extract run id from there. This is definitely less efficient than it could be if there was a more
// specific rpc method for this, or if there were more granular history filters - in which case it could be
// extracted from the `iterFn` inside of `workflowRunImpl`
var runIDCell util.OnceCell
if runID == "" {
fetcher := func() string {
execData, _ := wc.DescribeWorkflowExecution(ctx, workflowID, runID)
wei := execData.GetWorkflowExecutionInfo()
if wei != nil {
execution := wei.GetExecution()
if execution != nil {
return execution.RunId
}
}
return ""
}
runIDCell = util.LazyOnceCell(fetcher)
} else {
runIDCell = util.PopulatedOnceCell(runID)
}
return &workflowRunImpl{
workflowID: workflowID,
firstRunID: runID,
currentRunID: &runIDCell,
iterFn: iterFn,
dataConverter: wc.dataConverter,
registry: wc.registry,
}
}
// SignalWorkflow signals a workflow in execution.
func (wc *WorkflowClient) SignalWorkflow(ctx context.Context, workflowID string, runID string, signalName string, arg interface{}) error {
// Set header before interceptor run
ctx = contextWithNewHeader(ctx)
return wc.interceptor.SignalWorkflow(ctx, &ClientSignalWorkflowInput{
WorkflowID: workflowID,
RunID: runID,
SignalName: signalName,
Arg: arg,
})
}
// SignalWithStartWorkflow sends a signal to a running workflow.
// If the workflow is not running or not found, it starts the workflow and then sends the signal in transaction.
func (wc *WorkflowClient) SignalWithStartWorkflow(ctx context.Context, workflowID string, signalName string, signalArg interface{},
options StartWorkflowOptions, workflowFunc interface{}, workflowArgs ...interface{}) (WorkflowRun, error) {
// Due to the ambiguous way to provide workflow IDs, if options contains an
// ID, it must match the parameter
if options.ID != "" && options.ID != workflowID {
return nil, fmt.Errorf("workflow ID from options not used, must be unset or match workflow ID parameter")
}
// Default workflow ID to UUID
options.ID = workflowID
if options.ID == "" {
options.ID = uuid.New()
}
// Validate function and get name
if err := validateFunctionArgs(workflowFunc, workflowArgs, true); err != nil {
return nil, err
}
workflowType, err := getWorkflowFunctionName(wc.registry, workflowFunc)
if err != nil {
return nil, err
}
// Set header before interceptor run
ctx = contextWithNewHeader(ctx)
// Run via interceptor
return wc.interceptor.SignalWithStartWorkflow(ctx, &ClientSignalWithStartWorkflowInput{
SignalName: signalName,
SignalArg: signalArg,
Options: &options,
WorkflowType: workflowType,
Args: workflowArgs,
})
}
// CancelWorkflow cancels a workflow in execution. It allows workflow to properly clean up and gracefully close.
// workflowID is required, other parameters are optional.
// If runID is omit, it will terminate currently running workflow (if there is one) based on the workflowID.
func (wc *WorkflowClient) CancelWorkflow(ctx context.Context, workflowID string, runID string) error {
return wc.interceptor.CancelWorkflow(ctx, &ClientCancelWorkflowInput{WorkflowID: workflowID, RunID: runID})
}
// TerminateWorkflow terminates a workflow execution.
// workflowID is required, other parameters are optional.
// If runID is omit, it will terminate currently running workflow (if there is one) based on the workflowID.
func (wc *WorkflowClient) TerminateWorkflow(ctx context.Context, workflowID string, runID string, reason string, details ...interface{}) error {
return wc.interceptor.TerminateWorkflow(ctx, &ClientTerminateWorkflowInput{
WorkflowID: workflowID,
RunID: runID,
Reason: reason,
Details: details,
})
}
// GetWorkflowHistory return a channel which contains the history events of a given workflow
func (wc *WorkflowClient) GetWorkflowHistory(
ctx context.Context,
workflowID string,
runID string,
isLongPoll bool,
filterType enumspb.HistoryEventFilterType,
) HistoryEventIterator {
return wc.getWorkflowHistory(ctx, workflowID, runID, isLongPoll, filterType, wc.metricsHandler)
}
func (wc *WorkflowClient) getWorkflowHistory(
ctx context.Context,
workflowID string,
runID string,
isLongPoll bool,
filterType enumspb.HistoryEventFilterType,
rpcMetricsHandler metrics.Handler,
) HistoryEventIterator {
namespace := wc.namespace
paginate := func(nextToken []byte) (*workflowservice.GetWorkflowExecutionHistoryResponse, error) {
request := &workflowservice.GetWorkflowExecutionHistoryRequest{
Namespace: namespace,
Execution: &commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: runID,
},
WaitNewEvent: isLongPoll,
HistoryEventFilterType: filterType,
NextPageToken: nextToken,
SkipArchival: isLongPoll,
}
var response *workflowservice.GetWorkflowExecutionHistoryResponse
var err error
Loop:
for {
response, err = wc.getWorkflowExecutionHistory(ctx, rpcMetricsHandler, isLongPoll, request, filterType)
if err != nil {
return nil, err
}
if isLongPoll && len(response.History.Events) == 0 && len(response.NextPageToken) != 0 {
request.NextPageToken = response.NextPageToken
continue Loop
}
break Loop
}
return response, nil
}
return &historyEventIteratorImpl{
paginate: paginate,
}
}
func (wc *WorkflowClient) getWorkflowExecutionHistory(ctx context.Context, rpcMetricsHandler metrics.Handler, isLongPoll bool,
request *workflowservice.GetWorkflowExecutionHistoryRequest, filterType enumspb.HistoryEventFilterType) (*workflowservice.GetWorkflowExecutionHistoryResponse, error) {
grpcCtx, cancel := newGRPCContext(ctx, grpcMetricsHandler(rpcMetricsHandler), grpcLongPoll(isLongPoll), defaultGrpcRetryParameters(ctx), func(builder *grpcContextBuilder) {
if isLongPoll {
builder.Timeout = defaultGetHistoryTimeout
}
})
defer cancel()
response, err := wc.workflowService.GetWorkflowExecutionHistory(grpcCtx, request)
if err != nil {
return nil, err
}
if response.RawHistory != nil {
history, err := serializer.DeserializeBlobDataToHistoryEvents(response.RawHistory, filterType)
if err != nil {
return nil, err
}
response.History = history
}
return response, err
}
// CompleteActivity reports activity completed. activity Execute method can return activity.ErrResultPending to
// indicate the activity is not completed when it's Execute method returns. In that case, this CompleteActivity() method
// should be called when that activity is completed with the actual result and error. If err is nil, activity task
// completed event will be reported; if err is CanceledError, activity task canceled event will be reported; otherwise,
// activity task failed event will be reported.
func (wc *WorkflowClient) CompleteActivity(ctx context.Context, taskToken []byte, result interface{}, err error) error {
if taskToken == nil {
return errors.New("invalid task token provided")
}
dataConverter := WithContext(ctx, wc.dataConverter)
var data *commonpb.Payloads
if result != nil {
var err0 error
data, err0 = encodeArg(dataConverter, result)
if err0 != nil {
return err0
}
}
// We do allow canceled error to be passed here
cancelAllowed := true
request := convertActivityResultToRespondRequest(wc.identity, taskToken,
data, err, wc.dataConverter, wc.namespace, cancelAllowed)
return reportActivityComplete(ctx, wc.workflowService, request, wc.metricsHandler)
}
// CompleteActivityByID reports activity completed. Similar to CompleteActivity
// It takes namespace name, workflowID, runID, activityID as arguments.
func (wc *WorkflowClient) CompleteActivityByID(ctx context.Context, namespace, workflowID, runID, activityID string,
result interface{}, err error) error {
if activityID == "" || workflowID == "" || namespace == "" {
return errors.New("empty activity or workflow id or namespace")
}
dataConverter := WithContext(ctx, wc.dataConverter)
var data *commonpb.Payloads
if result != nil {
var err0 error
data, err0 = encodeArg(dataConverter, result)
if err0 != nil {
return err0
}
}
// We do allow canceled error to be passed here
cancelAllowed := true
request := convertActivityResultToRespondRequestByID(wc.identity, namespace, workflowID, runID, activityID,
data, err, wc.dataConverter, cancelAllowed)
return reportActivityCompleteByID(ctx, wc.workflowService, request, wc.metricsHandler)
}
// RecordActivityHeartbeat records heartbeat for an activity.
func (wc *WorkflowClient) RecordActivityHeartbeat(ctx context.Context, taskToken []byte, details ...interface{}) error {
dataConverter := WithContext(ctx, wc.dataConverter)
data, err := encodeArgs(dataConverter, details)
if err != nil {
return err
}
return recordActivityHeartbeat(ctx, wc.workflowService, wc.metricsHandler, wc.identity, taskToken, data)
}
// RecordActivityHeartbeatByID records heartbeat for an activity.
func (wc *WorkflowClient) RecordActivityHeartbeatByID(ctx context.Context,
namespace, workflowID, runID, activityID string, details ...interface{}) error {
dataConverter := WithContext(ctx, wc.dataConverter)
data, err := encodeArgs(dataConverter, details)
if err != nil {
return err
}
return recordActivityHeartbeatByID(ctx, wc.workflowService, wc.metricsHandler, wc.identity, namespace, workflowID, runID, activityID, data)
}
// ListClosedWorkflow gets closed workflow executions based on request filters
// The errors it can throw:
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
// - serviceerror.NotFound
func (wc *WorkflowClient) ListClosedWorkflow(ctx context.Context, request *workflowservice.ListClosedWorkflowExecutionsRequest) (*workflowservice.ListClosedWorkflowExecutionsResponse, error) {
if request.GetNamespace() == "" {
request.Namespace = wc.namespace
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.ListClosedWorkflowExecutions(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// ListOpenWorkflow gets open workflow executions based on request filters
// The errors it can throw:
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
// - serviceerror.NotFound
func (wc *WorkflowClient) ListOpenWorkflow(ctx context.Context, request *workflowservice.ListOpenWorkflowExecutionsRequest) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
if request.GetNamespace() == "" {
request.Namespace = wc.namespace
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.ListOpenWorkflowExecutions(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// ListWorkflow implementation
func (wc *WorkflowClient) ListWorkflow(ctx context.Context, request *workflowservice.ListWorkflowExecutionsRequest) (*workflowservice.ListWorkflowExecutionsResponse, error) {
if request.GetNamespace() == "" {
request.Namespace = wc.namespace
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.ListWorkflowExecutions(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// ListArchivedWorkflow implementation
func (wc *WorkflowClient) ListArchivedWorkflow(ctx context.Context, request *workflowservice.ListArchivedWorkflowExecutionsRequest) (*workflowservice.ListArchivedWorkflowExecutionsResponse, error) {
if request.GetNamespace() == "" {
request.Namespace = wc.namespace
}
timeout := maxListArchivedWorkflowTimeout
now := time.Now()
if ctx != nil {
if expiration, ok := ctx.Deadline(); ok && expiration.After(now) {
timeout = expiration.Sub(now)
if timeout > maxListArchivedWorkflowTimeout {
timeout = maxListArchivedWorkflowTimeout
} else if timeout < minRPCTimeout {
timeout = minRPCTimeout
}
}
}
grpcCtx, cancel := newGRPCContext(ctx, grpcTimeout(timeout), defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.ListArchivedWorkflowExecutions(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// ScanWorkflow implementation
func (wc *WorkflowClient) ScanWorkflow(ctx context.Context, request *workflowservice.ScanWorkflowExecutionsRequest) (*workflowservice.ScanWorkflowExecutionsResponse, error) {
if request.GetNamespace() == "" {
request.Namespace = wc.namespace
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.ScanWorkflowExecutions(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// CountWorkflow implementation
func (wc *WorkflowClient) CountWorkflow(ctx context.Context, request *workflowservice.CountWorkflowExecutionsRequest) (*workflowservice.CountWorkflowExecutionsResponse, error) {
if request.GetNamespace() == "" {
request.Namespace = wc.namespace
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.CountWorkflowExecutions(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// GetSearchAttributes implementation
func (wc *WorkflowClient) GetSearchAttributes(ctx context.Context) (*workflowservice.GetSearchAttributesResponse, error) {
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.GetSearchAttributes(grpcCtx, &workflowservice.GetSearchAttributesRequest{})
if err != nil {
return nil, err
}
return response, nil
}
// DescribeWorkflowExecution returns information about the specified workflow execution.
// The errors it can return:
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
// - serviceerror.NotFound
func (wc *WorkflowClient) DescribeWorkflowExecution(ctx context.Context, workflowID, runID string) (*workflowservice.DescribeWorkflowExecutionResponse, error) {
request := &workflowservice.DescribeWorkflowExecutionRequest{
Namespace: wc.namespace,
Execution: &commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: runID,
},
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := wc.workflowService.DescribeWorkflowExecution(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// QueryWorkflow queries a given workflow execution
// workflowID and queryType are required, other parameters are optional.
// - workflow ID of the workflow.
// - runID can be default(empty string). if empty string then it will pick the running execution of that workflow ID.
// - taskQueue can be default(empty string). If empty string then it will pick the taskQueue of the running execution of that workflow ID.
// - queryType is the type of the query.
// - args... are the optional query parameters.
// The errors it can return:
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
// - serviceerror.NotFound
// - serviceerror.QueryFailed
func (wc *WorkflowClient) QueryWorkflow(ctx context.Context, workflowID string, runID string, queryType string, args ...interface{}) (converter.EncodedValue, error) {
// Set header before interceptor run
ctx = contextWithNewHeader(ctx)
return wc.interceptor.QueryWorkflow(ctx, &ClientQueryWorkflowInput{
WorkflowID: workflowID,
RunID: runID,
QueryType: queryType,
Args: args,
})
}
// QueryWorkflowWithOptionsRequest is the request to QueryWorkflowWithOptions
type QueryWorkflowWithOptionsRequest struct {
// WorkflowID is a required field indicating the workflow which should be queried.
WorkflowID string
// RunID is an optional field used to identify a specific run of the queried workflow.
// If RunID is not provided the latest run will be used.
RunID string
// QueryType is a required field which specifies the query you want to run.
// By default, temporal supports "__stack_trace" as a standard query type, which will return string value
// representing the call stack of the target workflow. The target workflow could also setup different query handler to handle custom query types.
// See comments at workflow.SetQueryHandler(ctx Context, queryType string, handler interface{}) for more details on how to setup query handler within the target workflow.
QueryType string
// Args is an optional field used to identify the arguments passed to the query.
Args []interface{}
// QueryRejectCondition is an optional field used to reject queries based on workflow state.
// QUERY_REJECT_CONDITION_NONE indicates that query should not be rejected.
// QUERY_REJECT_CONDITION_NOT_OPEN indicates that query should be rejected if workflow is not open.
// QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY indicates that query should be rejected if workflow did not complete cleanly (e.g. terminated, canceled timeout etc...).
QueryRejectCondition enumspb.QueryRejectCondition
// Header is an optional header to include with the query.
Header *commonpb.Header
}
// QueryWorkflowWithOptionsResponse is the response to QueryWorkflowWithOptions
type QueryWorkflowWithOptionsResponse struct {
// QueryResult contains the result of executing the query.
// This will only be set if the query was completed successfully and not rejected.
QueryResult converter.EncodedValue
// QueryRejected contains information about the query rejection.
QueryRejected *querypb.QueryRejected
}
// QueryWorkflowWithOptions queries a given workflow execution and returns the query result synchronously.
// See QueryWorkflowWithOptionsRequest and QueryWorkflowWithOptionsResult for more information.
// The errors it can return:
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
// - serviceerror.NotFound
// - serviceerror.QueryFailed
func (wc *WorkflowClient) QueryWorkflowWithOptions(ctx context.Context, request *QueryWorkflowWithOptionsRequest) (*QueryWorkflowWithOptionsResponse, error) {
var input *commonpb.Payloads
if len(request.Args) > 0 {
var err error
if input, err = encodeArgs(wc.dataConverter, request.Args); err != nil {
return nil, err
}
}
req := &workflowservice.QueryWorkflowRequest{
Namespace: wc.namespace,
Execution: &commonpb.WorkflowExecution{
WorkflowId: request.WorkflowID,
RunId: request.RunID,
},
Query: &querypb.WorkflowQuery{
QueryType: request.QueryType,
QueryArgs: input,
Header: request.Header,
},
QueryRejectCondition: request.QueryRejectCondition,
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
resp, err := wc.workflowService.QueryWorkflow(grpcCtx, req)
if err != nil {
return nil, err
}
if resp.QueryRejected != nil {
return &QueryWorkflowWithOptionsResponse{
QueryRejected: resp.QueryRejected,
QueryResult: nil,
}, nil
}
return &QueryWorkflowWithOptionsResponse{
QueryRejected: nil,
QueryResult: newEncodedValue(resp.QueryResult, wc.dataConverter),
}, nil
}
// DescribeTaskQueue returns information about the target taskqueue, right now this API returns the
// pollers which polled this taskqueue in last few minutes.
// - taskqueue name of taskqueue
// - taskqueueType type of taskqueue, can be workflow or activity
// The errors it can return:
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
// - serviceerror.NotFound
func (wc *WorkflowClient) DescribeTaskQueue(ctx context.Context, taskQueue string, taskQueueType enumspb.TaskQueueType) (*workflowservice.DescribeTaskQueueResponse, error) {
request := &workflowservice.DescribeTaskQueueRequest{
Namespace: wc.namespace,
TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
TaskQueueType: taskQueueType,
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
resp, err := wc.workflowService.DescribeTaskQueue(grpcCtx, request)
if err != nil {
return nil, err
}
return resp, nil
}
// ResetWorkflowExecution reset an existing workflow execution to WorkflowTaskFinishEventId(exclusive).
// And it will immediately terminating the current execution instance.
// RequestId is used to deduplicate requests. It will be autogenerated if not set.
func (wc *WorkflowClient) ResetWorkflowExecution(ctx context.Context, request *workflowservice.ResetWorkflowExecutionRequest) (*workflowservice.ResetWorkflowExecutionResponse, error) {
if request != nil && request.GetRequestId() == "" {
request.RequestId = uuid.New()
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
resp, err := wc.workflowService.ResetWorkflowExecution(grpcCtx, request)
if err != nil {
return nil, err
}
return resp, nil
}
// WorkflowService implements Client.WorkflowService.
func (wc *WorkflowClient) WorkflowService() workflowservice.WorkflowServiceClient {
return wc.workflowService
}
// Close client and clean up underlying resources.
func (wc *WorkflowClient) Close() {
if wc.connectionCloser == nil {
return
}
if err := wc.connectionCloser.Close(); err != nil {
wc.logger.Warn("unable to close connection", tagError, err)
}
}
// Register a namespace with temporal server
// The errors it can throw:
// - NamespaceAlreadyExistsError
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
func (nc *namespaceClient) Register(ctx context.Context, request *workflowservice.RegisterNamespaceRequest) error {
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
var err error
_, err = nc.workflowService.RegisterNamespace(grpcCtx, request)
return err
}
// Describe a namespace. The namespace has 3 part of information
// NamespaceInfo - Which has Name, Status, Description, Owner Email
// NamespaceConfiguration - Configuration like Workflow Execution Retention Period In Days, Whether to emit metrics.
// ReplicationConfiguration - replication config like clusters and active cluster name
// The errors it can throw:
// - serviceerror.NotFound
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
func (nc *namespaceClient) Describe(ctx context.Context, namespace string) (*workflowservice.DescribeNamespaceResponse, error) {
request := &workflowservice.DescribeNamespaceRequest{
Namespace: namespace,
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err := nc.workflowService.DescribeNamespace(grpcCtx, request)
if err != nil {
return nil, err
}
return response, nil
}
// Update a namespace.
// The errors it can throw:
// - serviceerror.NotFound
// - serviceerror.InvalidArgument
// - serviceerror.Internal
// - serviceerror.Unavailable
func (nc *namespaceClient) Update(ctx context.Context, request *workflowservice.UpdateNamespaceRequest) error {
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
_, err := nc.workflowService.UpdateNamespace(grpcCtx, request)
return err
}
// Close client and clean up underlying resources.
func (nc *namespaceClient) Close() {
if nc.connectionCloser == nil {
return
}
if err := nc.connectionCloser.Close(); err != nil {
nc.logger.Warn("unable to close connection", tagError, err)
}
}
func (iter *historyEventIteratorImpl) HasNext() bool {
if iter.nextEventIndex < len(iter.events) || iter.err != nil {
return true
} else if !iter.initialized || len(iter.nexttoken) != 0 {
iter.initialized = true
response, err := iter.paginate(iter.nexttoken)
iter.nextEventIndex = 0
if err == nil {
iter.events = response.History.Events
iter.nexttoken = response.NextPageToken
iter.err = nil
} else {
iter.events = nil
iter.nexttoken = nil
iter.err = err
}
if iter.nextEventIndex < len(iter.events) || iter.err != nil {
return true
}
return false
}
return false
}
func (iter *historyEventIteratorImpl) Next() (*historypb.HistoryEvent, error) {
// if caller call the Next() when iteration is over, just return nil, nil
if !iter.HasNext() {
panic("HistoryEventIterator Next() called without checking HasNext()")
}
// we have cached events
if iter.nextEventIndex < len(iter.events) {
index := iter.nextEventIndex
iter.nextEventIndex++
return iter.events[index], nil
} else if iter.err != nil {
// we have err, clear that iter.err and return err
err := iter.err
iter.err = nil
return nil, err
}
panic("HistoryEventIterator Next() should return either a history event or a err")
}
func (workflowRun *workflowRunImpl) GetRunID() string {
return workflowRun.currentRunID.Get()
}
func (workflowRun *workflowRunImpl) GetID() string {
return workflowRun.workflowID
}
func (workflowRun *workflowRunImpl) Get(ctx context.Context, valuePtr interface{}) error {
iter := workflowRun.iterFn(ctx, workflowRun.currentRunID.Get())
if !iter.HasNext() {
panic("could not get last history event for workflow")
}
closeEvent, err := iter.Next()
if err != nil {
return err
}
switch closeEvent.GetEventType() {
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED:
attributes := closeEvent.GetWorkflowExecutionCompletedEventAttributes()
if attributes.NewExecutionRunId != "" {
return workflowRun.follow(ctx, valuePtr, attributes.NewExecutionRunId)
}
if valuePtr == nil || attributes.Result == nil {
return nil
}
rf := reflect.ValueOf(valuePtr)
if rf.Type().Kind() != reflect.Ptr {
return errors.New("value parameter is not a pointer")
}
return workflowRun.dataConverter.FromPayloads(attributes.Result, valuePtr)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_FAILED:
attributes := closeEvent.GetWorkflowExecutionFailedEventAttributes()
if attributes.NewExecutionRunId != "" {
return workflowRun.follow(ctx, valuePtr, attributes.NewExecutionRunId)
}
err = ConvertFailureToError(attributes.GetFailure(), workflowRun.dataConverter)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED:
attributes := closeEvent.GetWorkflowExecutionCanceledEventAttributes()
details := newEncodedValues(attributes.Details, workflowRun.dataConverter)
err = NewCanceledError(details)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED:
err = newTerminatedError()
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT:
attributes := closeEvent.GetWorkflowExecutionTimedOutEventAttributes()
if attributes.NewExecutionRunId != "" {
return workflowRun.follow(ctx, valuePtr, attributes.NewExecutionRunId)
}
err = NewTimeoutError("Workflow timeout", enumspb.TIMEOUT_TYPE_START_TO_CLOSE, nil)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CONTINUED_AS_NEW:
attributes := closeEvent.GetWorkflowExecutionContinuedAsNewEventAttributes()
return workflowRun.follow(ctx, valuePtr, attributes.NewExecutionRunId)
default:
return fmt.Errorf("unexpected event type %s when handling workflow execution result", closeEvent.GetEventType())
}
err = NewWorkflowExecutionError(
workflowRun.workflowID,
workflowRun.currentRunID.Get(),
workflowRun.workflowType,
err)
return err
}
// follow is used by Get to follow a chain of executions linked by NewExecutionRunId, so that Get
// doesn't return until the chain finishes. These can be ContinuedAsNew events, Completed events
// (for workflows with a cron schedule), or Failed or TimedOut events (for workflows with a retry
// policy or cron schedule).
func (workflowRun *workflowRunImpl) follow(ctx context.Context, valuePtr interface{}, newRunID string) error {
curRunID := util.PopulatedOnceCell(newRunID)
workflowRun.currentRunID = &curRunID
return workflowRun.Get(ctx, valuePtr)
}
func getWorkflowMemo(input map[string]interface{}, dc converter.DataConverter) (*commonpb.Memo, error) {
if input == nil {
return nil, nil
}
memo := make(map[string]*commonpb.Payload)
for k, v := range input {
// TODO (shtin): use dc here???
memoBytes, err := converter.GetDefaultDataConverter().ToPayload(v)
if err != nil {
return nil, fmt.Errorf("encode workflow memo error: %v", err.Error())
}
memo[k] = memoBytes
}
return &commonpb.Memo{Fields: memo}, nil
}
func serializeSearchAttributes(input map[string]interface{}) (*commonpb.SearchAttributes, error) {
if input == nil {
return nil, nil
}
attr := make(map[string]*commonpb.Payload)
for k, v := range input {
attrBytes, err := converter.GetDefaultDataConverter().ToPayload(v)
if err != nil {
return nil, fmt.Errorf("encode search attribute [%s] error: %v", k, err)
}
attr[k] = attrBytes
}
return &commonpb.SearchAttributes{IndexedFields: attr}, nil
}
type workflowClientInterceptor struct{ client *WorkflowClient }
func (w *workflowClientInterceptor) ExecuteWorkflow(
ctx context.Context,
in *ClientExecuteWorkflowInput,
) (WorkflowRun, error) {
// This is always set before interceptor is invoked
workflowID := in.Options.ID
if workflowID == "" {
return nil, fmt.Errorf("no workflow ID in options")
}
executionTimeout := in.Options.WorkflowExecutionTimeout
runTimeout := in.Options.WorkflowRunTimeout
workflowTaskTimeout := in.Options.WorkflowTaskTimeout
dataConverter := WithContext(ctx, w.client.dataConverter)
if dataConverter == nil {
dataConverter = converter.GetDefaultDataConverter()
}
// Encode input
input, err := encodeArgs(dataConverter, in.Args)
if err != nil {
return nil, err
}
memo, err := getWorkflowMemo(in.Options.Memo, dataConverter)
if err != nil {
return nil, err
}
searchAttr, err := serializeSearchAttributes(in.Options.SearchAttributes)
if err != nil {
return nil, err
}
// get workflow headers from the context
header, err := headerPropagated(ctx, w.client.contextPropagators)
if err != nil {
return nil, err
}
// run propagators to extract information about tracing and other stuff, store in headers field
startRequest := &workflowservice.StartWorkflowExecutionRequest{
Namespace: w.client.namespace,
RequestId: uuid.New(),
WorkflowId: workflowID,
WorkflowType: &commonpb.WorkflowType{Name: in.WorkflowType},
TaskQueue: &taskqueuepb.TaskQueue{Name: in.Options.TaskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
Input: input,
WorkflowExecutionTimeout: &executionTimeout,
WorkflowRunTimeout: &runTimeout,
WorkflowTaskTimeout: &workflowTaskTimeout,
Identity: w.client.identity,
WorkflowIdReusePolicy: in.Options.WorkflowIDReusePolicy,
RetryPolicy: convertToPBRetryPolicy(in.Options.RetryPolicy),
CronSchedule: in.Options.CronSchedule,
Memo: memo,
SearchAttributes: searchAttr,
Header: header,
}
var response *workflowservice.StartWorkflowExecutionResponse
grpcCtx, cancel := newGRPCContext(ctx, grpcMetricsHandler(
w.client.metricsHandler.WithTags(metrics.RPCTags(in.WorkflowType, metrics.NoneTagValue, in.Options.TaskQueue))),
defaultGrpcRetryParameters(ctx))
defer cancel()
response, err = w.client.workflowService.StartWorkflowExecution(grpcCtx, startRequest)
// Allow already-started error
var runID string
if e, ok := err.(*serviceerror.WorkflowExecutionAlreadyStarted); ok && !in.Options.WorkflowExecutionErrorWhenAlreadyStarted {
runID = e.RunId
} else if err != nil {
return nil, err
} else {
runID = response.RunId
}
iterFn := func(fnCtx context.Context, fnRunID string) HistoryEventIterator {
metricsHandler := w.client.metricsHandler.WithTags(metrics.RPCTags(in.WorkflowType,
metrics.NoneTagValue, in.Options.TaskQueue))
return w.client.getWorkflowHistory(fnCtx, workflowID, fnRunID, true,
enumspb.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, metricsHandler)
}
curRunIDCell := util.PopulatedOnceCell(runID)
return &workflowRunImpl{
workflowType: in.WorkflowType,
workflowID: workflowID,
firstRunID: runID,
currentRunID: &curRunIDCell,
iterFn: iterFn,
dataConverter: w.client.dataConverter,
registry: w.client.registry,
}, nil
}
func (w *workflowClientInterceptor) SignalWorkflow(ctx context.Context, in *ClientSignalWorkflowInput) error {
dataConverter := WithContext(ctx, w.client.dataConverter)
input, err := encodeArg(dataConverter, in.Arg)
if err != nil {
return err
}
// get workflow headers from the context
header, err := headerPropagated(ctx, w.client.contextPropagators)
if err != nil {
return err
}
request := &workflowservice.SignalWorkflowExecutionRequest{
Namespace: w.client.namespace,
RequestId: uuid.New(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: in.WorkflowID,
RunId: in.RunID,
},
SignalName: in.SignalName,
Input: input,
Identity: w.client.identity,
Header: header,
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
_, err = w.client.workflowService.SignalWorkflowExecution(grpcCtx, request)
return err
}
func (w *workflowClientInterceptor) SignalWithStartWorkflow(
ctx context.Context,
in *ClientSignalWithStartWorkflowInput,
) (WorkflowRun, error) {
dataConverter := WithContext(ctx, w.client.dataConverter)
signalInput, err := encodeArg(dataConverter, in.SignalArg)
if err != nil {
return nil, err
}
executionTimeout := in.Options.WorkflowExecutionTimeout
runTimeout := in.Options.WorkflowRunTimeout
taskTimeout := in.Options.WorkflowTaskTimeout
// Encode input
input, err := encodeArgs(dataConverter, in.Args)
if err != nil {
return nil, err
}
memo, err := getWorkflowMemo(in.Options.Memo, dataConverter)
if err != nil {
return nil, err
}
searchAttr, err := serializeSearchAttributes(in.Options.SearchAttributes)
if err != nil {
return nil, err
}
// get workflow headers from the context
header, err := headerPropagated(ctx, w.client.contextPropagators)
if err != nil {
return nil, err
}
signalWithStartRequest := &workflowservice.SignalWithStartWorkflowExecutionRequest{
Namespace: w.client.namespace,
RequestId: uuid.New(),
WorkflowId: in.Options.ID,
WorkflowType: &commonpb.WorkflowType{Name: in.WorkflowType},
TaskQueue: &taskqueuepb.TaskQueue{Name: in.Options.TaskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL},
Input: input,
WorkflowExecutionTimeout: &executionTimeout,
WorkflowRunTimeout: &runTimeout,
WorkflowTaskTimeout: &taskTimeout,
SignalName: in.SignalName,
SignalInput: signalInput,
Identity: w.client.identity,
RetryPolicy: convertToPBRetryPolicy(in.Options.RetryPolicy),
CronSchedule: in.Options.CronSchedule,
Memo: memo,
SearchAttributes: searchAttr,
WorkflowIdReusePolicy: in.Options.WorkflowIDReusePolicy,
Header: header,
}
var response *workflowservice.SignalWithStartWorkflowExecutionResponse
// Start creating workflow request.
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
response, err = w.client.workflowService.SignalWithStartWorkflowExecution(grpcCtx, signalWithStartRequest)
if err != nil {
return nil, err
}
iterFn := func(fnCtx context.Context, fnRunID string) HistoryEventIterator {
metricsHandler := w.client.metricsHandler.WithTags(metrics.RPCTags(in.WorkflowType,
metrics.NoneTagValue, in.Options.TaskQueue))
return w.client.getWorkflowHistory(fnCtx, in.Options.ID, fnRunID, true,
enumspb.HISTORY_EVENT_FILTER_TYPE_CLOSE_EVENT, metricsHandler)
}
curRunIDCell := util.PopulatedOnceCell(response.GetRunId())
return &workflowRunImpl{
workflowType: in.WorkflowType,
workflowID: in.Options.ID,
firstRunID: response.GetRunId(),
currentRunID: &curRunIDCell,
iterFn: iterFn,
dataConverter: w.client.dataConverter,
registry: w.client.registry,
}, nil
}
func (w *workflowClientInterceptor) CancelWorkflow(ctx context.Context, in *ClientCancelWorkflowInput) error {
request := &workflowservice.RequestCancelWorkflowExecutionRequest{
Namespace: w.client.namespace,
RequestId: uuid.New(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: in.WorkflowID,
RunId: in.RunID,
},
Identity: w.client.identity,
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
_, err := w.client.workflowService.RequestCancelWorkflowExecution(grpcCtx, request)
return err
}
func (w *workflowClientInterceptor) TerminateWorkflow(ctx context.Context, in *ClientTerminateWorkflowInput) error {
datailsPayload, err := w.client.dataConverter.ToPayloads(in.Details...)
if err != nil {
return err
}
request := &workflowservice.TerminateWorkflowExecutionRequest{
Namespace: w.client.namespace,
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: in.WorkflowID,
RunId: in.RunID,
},
Reason: in.Reason,
Identity: w.client.identity,
Details: datailsPayload,
}
grpcCtx, cancel := newGRPCContext(ctx, defaultGrpcRetryParameters(ctx))
defer cancel()
_, err = w.client.workflowService.TerminateWorkflowExecution(grpcCtx, request)
return err
}
func (w *workflowClientInterceptor) QueryWorkflow(
ctx context.Context,
in *ClientQueryWorkflowInput,
) (converter.EncodedValue, error) {
// get workflow headers from the context
header, err := headerPropagated(ctx, w.client.contextPropagators)
if err != nil {
return nil, err
}
result, err := w.client.QueryWorkflowWithOptions(ctx, &QueryWorkflowWithOptionsRequest{
WorkflowID: in.WorkflowID,
RunID: in.RunID,
QueryType: in.QueryType,
Args: in.Args,
Header: header,
})
if err != nil {
return nil, err
}
return result.QueryResult, nil
}
// Required to implement ClientOutboundInterceptor
func (*workflowClientInterceptor) mustEmbedClientOutboundInterceptorBase() {}