mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
1386 lines
53 KiB
Go
1386 lines
53 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 history
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
commandpb "go.temporal.io/api/command/v1"
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
historypb "go.temporal.io/api/history/v1"
|
|
protocolpb "go.temporal.io/api/protocol/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"
|
|
|
|
enumsspb "go.temporal.io/server/api/enums/v1"
|
|
"go.temporal.io/server/api/historyservice/v1"
|
|
"go.temporal.io/server/api/matchingservice/v1"
|
|
tokenspb "go.temporal.io/server/api/token/v1"
|
|
"go.temporal.io/server/common"
|
|
"go.temporal.io/server/common/clock"
|
|
"go.temporal.io/server/common/collection"
|
|
"go.temporal.io/server/common/definition"
|
|
"go.temporal.io/server/common/dynamicconfig"
|
|
"go.temporal.io/server/common/failure"
|
|
"go.temporal.io/server/common/log"
|
|
"go.temporal.io/server/common/log/tag"
|
|
"go.temporal.io/server/common/metrics"
|
|
"go.temporal.io/server/common/namespace"
|
|
"go.temporal.io/server/common/payloads"
|
|
"go.temporal.io/server/common/persistence"
|
|
"go.temporal.io/server/common/persistence/visibility/manager"
|
|
"go.temporal.io/server/common/searchattribute"
|
|
serviceerrors "go.temporal.io/server/common/serviceerror"
|
|
"go.temporal.io/server/common/tasktoken"
|
|
"go.temporal.io/server/common/worker_versioning"
|
|
"go.temporal.io/server/internal/effect"
|
|
"go.temporal.io/server/service/history/api"
|
|
"go.temporal.io/server/service/history/configs"
|
|
"go.temporal.io/server/service/history/consts"
|
|
"go.temporal.io/server/service/history/events"
|
|
"go.temporal.io/server/service/history/shard"
|
|
"go.temporal.io/server/service/history/workflow"
|
|
"go.temporal.io/server/service/history/workflow/update"
|
|
)
|
|
|
|
type (
|
|
// workflow task business logic handler
|
|
workflowTaskHandlerCallbacks interface {
|
|
handleWorkflowTaskScheduled(context.Context, *historyservice.ScheduleWorkflowTaskRequest) error
|
|
handleWorkflowTaskStarted(context.Context,
|
|
*historyservice.RecordWorkflowTaskStartedRequest) (*historyservice.RecordWorkflowTaskStartedResponse, error)
|
|
handleWorkflowTaskFailed(context.Context,
|
|
*historyservice.RespondWorkflowTaskFailedRequest) error
|
|
handleWorkflowTaskCompleted(context.Context,
|
|
*historyservice.RespondWorkflowTaskCompletedRequest) (*historyservice.RespondWorkflowTaskCompletedResponse, error)
|
|
verifyFirstWorkflowTaskScheduled(context.Context, *historyservice.VerifyFirstWorkflowTaskScheduledRequest) error
|
|
// TODO also include the handle of workflow task timeout here
|
|
}
|
|
|
|
workflowTaskHandlerCallbacksImpl struct {
|
|
currentClusterName string
|
|
config *configs.Config
|
|
shardContext shard.Context
|
|
workflowConsistencyChecker api.WorkflowConsistencyChecker
|
|
timeSource clock.TimeSource
|
|
namespaceRegistry namespace.Registry
|
|
eventNotifier events.Notifier
|
|
tokenSerializer common.TaskTokenSerializer
|
|
metricsHandler metrics.Handler
|
|
logger log.Logger
|
|
throttledLogger log.Logger
|
|
commandAttrValidator *commandAttrValidator
|
|
searchAttributesMapperProvider searchattribute.MapperProvider
|
|
searchAttributesValidator *searchattribute.Validator
|
|
persistenceVisibilityMgr manager.VisibilityManager
|
|
commandHandlerRegistry *workflow.CommandHandlerRegistry
|
|
}
|
|
)
|
|
|
|
func newWorkflowTaskHandlerCallback(historyEngine *historyEngineImpl) *workflowTaskHandlerCallbacksImpl {
|
|
return &workflowTaskHandlerCallbacksImpl{
|
|
currentClusterName: historyEngine.currentClusterName,
|
|
config: historyEngine.config,
|
|
shardContext: historyEngine.shardContext,
|
|
workflowConsistencyChecker: historyEngine.workflowConsistencyChecker,
|
|
timeSource: historyEngine.shardContext.GetTimeSource(),
|
|
namespaceRegistry: historyEngine.shardContext.GetNamespaceRegistry(),
|
|
eventNotifier: historyEngine.eventNotifier,
|
|
tokenSerializer: historyEngine.tokenSerializer,
|
|
metricsHandler: historyEngine.metricsHandler,
|
|
logger: historyEngine.logger,
|
|
throttledLogger: historyEngine.throttledLogger,
|
|
commandAttrValidator: newCommandAttrValidator(
|
|
historyEngine.shardContext.GetNamespaceRegistry(),
|
|
historyEngine.config,
|
|
historyEngine.searchAttributesValidator,
|
|
),
|
|
searchAttributesMapperProvider: historyEngine.shardContext.GetSearchAttributesMapperProvider(),
|
|
searchAttributesValidator: historyEngine.searchAttributesValidator,
|
|
persistenceVisibilityMgr: historyEngine.persistenceVisibilityMgr,
|
|
commandHandlerRegistry: historyEngine.commandHandlerRegistry,
|
|
}
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) handleWorkflowTaskScheduled(
|
|
ctx context.Context,
|
|
req *historyservice.ScheduleWorkflowTaskRequest,
|
|
) error {
|
|
|
|
_, err := api.GetActiveNamespace(handler.shardContext, namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return api.GetAndUpdateWorkflowWithNew(
|
|
ctx,
|
|
req.ChildClock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
req.NamespaceId,
|
|
req.WorkflowExecution.WorkflowId,
|
|
req.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowLease api.WorkflowLease) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowLease.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
if req.IsFirstWorkflowTask && mutableState.HadOrHasWorkflowTask() {
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
}, nil
|
|
}
|
|
|
|
startEvent, err := mutableState.GetStartEvent(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := mutableState.AddFirstWorkflowTaskScheduled(req.ParentClock, startEvent, false); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &api.UpdateWorkflowAction{}, nil
|
|
},
|
|
nil,
|
|
handler.shardContext,
|
|
handler.workflowConsistencyChecker,
|
|
)
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) handleWorkflowTaskStarted(
|
|
ctx context.Context,
|
|
req *historyservice.RecordWorkflowTaskStartedRequest,
|
|
) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
|
|
namespaceEntry, err := api.GetActiveNamespace(handler.shardContext, namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scheduledEventID := req.GetScheduledEventId()
|
|
requestID := req.GetRequestId()
|
|
|
|
var workflowKey definition.WorkflowKey
|
|
var resp *historyservice.RecordWorkflowTaskStartedResponse
|
|
|
|
err = api.GetAndUpdateWorkflowWithNew(
|
|
ctx,
|
|
req.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
req.NamespaceId,
|
|
req.WorkflowExecution.WorkflowId,
|
|
req.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowLease api.WorkflowLease) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowLease.GetMutableState()
|
|
updateRegistry := workflowLease.GetContext().UpdateRegistry(ctx, nil)
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
workflowTask := mutableState.GetWorkflowTaskByID(scheduledEventID)
|
|
metricsScope := handler.metricsHandler.WithTags(metrics.OperationTag(metrics.HistoryRecordWorkflowTaskStartedScope))
|
|
|
|
// First check to see if cache needs to be refreshed as we could potentially have stale workflow execution in
|
|
// some extreme cassandra failure cases.
|
|
if workflowTask == nil && scheduledEventID >= mutableState.GetNextEventID() {
|
|
metrics.StaleMutableStateCounter.With(metricsScope).Record(1)
|
|
// Reload workflow execution history
|
|
// ErrStaleState will trigger updateWorkflow function to reload the mutable state
|
|
return nil, consts.ErrStaleState
|
|
}
|
|
|
|
// Check execution state to make sure task is in the list of outstanding tasks and it is not yet started. If
|
|
// task is not outstanding than it is most probably a duplicate and complete the task.
|
|
if workflowTask == nil {
|
|
// Looks like WorkflowTask already completed as a result of another call.
|
|
// It is OK to drop the task at this point.
|
|
return nil, serviceerror.NewNotFound("Workflow task not found.")
|
|
}
|
|
|
|
workflowKey = mutableState.GetWorkflowKey()
|
|
updateAction := &api.UpdateWorkflowAction{}
|
|
|
|
if workflowTask.StartedEventID != common.EmptyEventID {
|
|
// If workflow task is started as part of the current request scope then return a positive response
|
|
if workflowTask.RequestID == requestID {
|
|
resp, err = handler.createRecordWorkflowTaskStartedResponse(ctx, mutableState, updateRegistry, workflowTask, req.PollRequest.GetIdentity(), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updateAction.Noop = true
|
|
return updateAction, nil
|
|
}
|
|
|
|
// Looks like WorkflowTask already started as a result of another call.
|
|
// It is OK to drop the task at this point.
|
|
return nil, serviceerrors.NewTaskAlreadyStarted("Workflow")
|
|
}
|
|
|
|
// Assuming a workflow is running on a sticky task queue by a workerA.
|
|
// After workerA is dead for more than 10s, matching will return StickyWorkerUnavailable error when history
|
|
// tries to push a new workflow task. When history sees that error, it will fall back to push the task to
|
|
// its original normal task queue without clear its stickiness to avoid an extra persistence write.
|
|
// We will clear the stickiness here when that task is delivered to another worker polling from normal queue.
|
|
// The stickiness info is used by frontend to decide if it should send down partial history or full history.
|
|
// Sending down partial history will cost the worker an extra fetch to server for the full history.
|
|
currentTaskQueue := mutableState.CurrentTaskQueue()
|
|
if currentTaskQueue.Kind == enumspb.TASK_QUEUE_KIND_STICKY &&
|
|
currentTaskQueue.GetName() != req.PollRequest.TaskQueue.GetName() {
|
|
// req.PollRequest.TaskQueue.GetName() may include partition, but we only check when sticky is enabled,
|
|
// and sticky queue never has partition, so it does not matter.
|
|
mutableState.ClearStickyTaskQueue()
|
|
}
|
|
|
|
_, workflowTask, err = mutableState.AddWorkflowTaskStartedEvent(
|
|
scheduledEventID,
|
|
requestID,
|
|
req.PollRequest.TaskQueue,
|
|
req.PollRequest.Identity,
|
|
worker_versioning.StampFromCapabilities(req.PollRequest.WorkerVersionCapabilities),
|
|
)
|
|
if err != nil {
|
|
// Unable to add WorkflowTaskStarted event to history
|
|
return nil, err
|
|
}
|
|
|
|
if workflowTask.Type == enumsspb.WORKFLOW_TASK_TYPE_SPECULATIVE {
|
|
updateAction.Noop = true
|
|
}
|
|
|
|
workflowScheduleToStartLatency := workflowTask.StartedTime.Sub(workflowTask.ScheduledTime)
|
|
namespaceName := namespaceEntry.Name()
|
|
taskQueue := workflowTask.TaskQueue
|
|
metrics.TaskScheduleToStartLatency.With(metrics.GetPerTaskQueueScope(
|
|
metricsScope,
|
|
namespaceName.String(),
|
|
taskQueue.GetName(),
|
|
taskQueue.GetKind(),
|
|
)).Record(workflowScheduleToStartLatency,
|
|
metrics.TaskQueueTypeTag(enumspb.TASK_QUEUE_TYPE_WORKFLOW),
|
|
)
|
|
|
|
resp, err = handler.createRecordWorkflowTaskStartedResponse(ctx, mutableState, updateRegistry, workflowTask, req.PollRequest.GetIdentity(), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return updateAction, nil
|
|
},
|
|
nil,
|
|
handler.shardContext,
|
|
handler.workflowConsistencyChecker,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dynamicconfig.AccessHistory(handler.config.FrontendAccessHistoryFraction, handler.metricsHandler.WithTags(metrics.OperationTag(metrics.HistoryHandleWorkflowTaskStartedTag))) {
|
|
maxHistoryPageSize := int32(handler.config.HistoryMaxPageSize(namespaceEntry.Name().String()))
|
|
err = handler.setHistoryForRecordWfTaskStartedResp(ctx, workflowKey, maxHistoryPageSize, resp)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) handleWorkflowTaskFailed(
|
|
ctx context.Context,
|
|
req *historyservice.RespondWorkflowTaskFailedRequest,
|
|
) (retError error) {
|
|
|
|
_, err := api.GetActiveNamespace(handler.shardContext, namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
request := req.FailedRequest
|
|
token, err := handler.tokenSerializer.Deserialize(request.TaskToken)
|
|
if err != nil {
|
|
return consts.ErrDeserializingToken
|
|
}
|
|
|
|
return api.GetAndUpdateWorkflowWithNew(
|
|
ctx,
|
|
token.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
token.NamespaceId,
|
|
token.WorkflowId,
|
|
token.RunId,
|
|
),
|
|
func(workflowLease api.WorkflowLease) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowLease.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
scheduledEventID := token.GetScheduledEventId()
|
|
workflowTask := mutableState.GetWorkflowTaskByID(scheduledEventID)
|
|
|
|
if workflowTask == nil ||
|
|
workflowTask.StartedEventID == common.EmptyEventID ||
|
|
(token.StartedEventId != common.EmptyEventID && token.StartedEventId != workflowTask.StartedEventID) ||
|
|
(token.StartedTime != nil && !workflowTask.StartedTime.IsZero() && !token.StartedTime.AsTime().Equal(workflowTask.StartedTime)) ||
|
|
workflowTask.Attempt != token.Attempt ||
|
|
(workflowTask.Version != common.EmptyVersion && token.Version != workflowTask.Version) {
|
|
// we have not alter mutable state yet, so release with it with nil to avoid clear MS.
|
|
workflowLease.GetReleaseFn()(nil)
|
|
return nil, serviceerror.NewNotFound("Workflow task not found.")
|
|
}
|
|
|
|
if _, err := mutableState.AddWorkflowTaskFailedEvent(
|
|
workflowTask,
|
|
request.GetCause(),
|
|
request.GetFailure(),
|
|
request.GetIdentity(),
|
|
request.GetWorkerVersion(),
|
|
request.GetBinaryChecksum(),
|
|
"",
|
|
"",
|
|
0); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO (alex-update): if it was speculative WT that failed, and there is nothing but pending updates,
|
|
// new WT also should be create as speculative (or not?). Currently, it will be recreated as normal WT.
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: true,
|
|
}, nil
|
|
},
|
|
nil,
|
|
handler.shardContext,
|
|
handler.workflowConsistencyChecker,
|
|
)
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) handleWorkflowTaskCompleted(
|
|
ctx context.Context,
|
|
req *historyservice.RespondWorkflowTaskCompletedRequest,
|
|
) (_ *historyservice.RespondWorkflowTaskCompletedResponse, retError error) {
|
|
namespaceEntry, err := api.GetActiveNamespace(handler.shardContext, namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request := req.CompleteRequest
|
|
token, err0 := handler.tokenSerializer.Deserialize(request.TaskToken)
|
|
if err0 != nil {
|
|
return nil, consts.ErrDeserializingToken
|
|
}
|
|
|
|
workflowLease, err := handler.workflowConsistencyChecker.GetWorkflowLease(
|
|
ctx,
|
|
token.Clock,
|
|
func(mutableState workflow.MutableState) bool {
|
|
workflowTask := mutableState.GetWorkflowTaskByID(token.GetScheduledEventId())
|
|
if workflowTask == nil && token.GetScheduledEventId() >= mutableState.GetNextEventID() {
|
|
metrics.StaleMutableStateCounter.With(handler.metricsHandler).Record(
|
|
1,
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope))
|
|
return false
|
|
}
|
|
return true
|
|
},
|
|
definition.NewWorkflowKey(
|
|
namespaceEntry.ID().String(),
|
|
token.WorkflowId,
|
|
token.RunId,
|
|
),
|
|
workflow.LockPriorityHigh,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
weContext := workflowLease.GetContext()
|
|
ms := workflowLease.GetMutableState()
|
|
|
|
currentWorkflowTask := ms.GetWorkflowTaskByID(token.GetScheduledEventId())
|
|
if !ms.IsWorkflowExecutionRunning() ||
|
|
currentWorkflowTask == nil ||
|
|
currentWorkflowTask.StartedEventID == common.EmptyEventID ||
|
|
(token.StartedEventId != common.EmptyEventID && token.StartedEventId != currentWorkflowTask.StartedEventID) ||
|
|
(token.StartedTime != nil && !currentWorkflowTask.StartedTime.IsZero() && !token.StartedTime.AsTime().Equal(currentWorkflowTask.StartedTime)) ||
|
|
currentWorkflowTask.Attempt != token.Attempt ||
|
|
(token.Version != common.EmptyVersion && token.Version != currentWorkflowTask.Version) {
|
|
// we have not alter mutable state yet, so release with it with nil to avoid clear MS.
|
|
workflowLease.GetReleaseFn()(nil)
|
|
return nil, serviceerror.NewNotFound("Workflow task not found.")
|
|
}
|
|
|
|
if assignedBuildId := ms.GetAssignedBuildId(); assignedBuildId != "" && !ms.IsStickyTaskQueueSet() {
|
|
// Worker versioning is used, make sure the task was completed by the right build ID, unless we're using a
|
|
// sticky queue in which case Matching will not send the build ID
|
|
wftStartedBuildId := ms.GetExecutionInfo().GetWorkflowTaskBuildId()
|
|
wftCompletedBuildId := request.GetWorkerVersionStamp().GetBuildId()
|
|
if wftCompletedBuildId != wftStartedBuildId {
|
|
workflowLease.GetReleaseFn()(nil)
|
|
return nil, serviceerror.NewNotFound("this workflow task was not dispatched to this Build ID")
|
|
}
|
|
}
|
|
|
|
defer func() { workflowLease.GetReleaseFn()(retError) }()
|
|
|
|
var effects effect.Buffer
|
|
defer func() {
|
|
// code in this file and workflowTaskHandler is inconsistent in the way
|
|
// errors are returned - some functions which appear to return error
|
|
// actually return nil in all cases and instead set a member variable
|
|
// that should be observed by other collaborating code (e.g.
|
|
// workflowtaskHandler.workflowTaskFailedCause). That made me paranoid
|
|
// about the way this function exits so while we have this defer here
|
|
// there is _also_ code to call effects.Cancel at key points.
|
|
if retError != nil {
|
|
handler.logger.Info("Cancel effects due to error.",
|
|
tag.Error(retError),
|
|
tag.WorkflowID(token.GetWorkflowId()),
|
|
tag.WorkflowRunID(token.GetRunId()),
|
|
tag.WorkflowNamespaceID(namespaceEntry.ID().String()))
|
|
effects.Cancel(ctx)
|
|
}
|
|
}()
|
|
|
|
// It's an error if the workflow has used versioning in the past but this task has no versioning info.
|
|
if ms.GetMostRecentWorkerVersionStamp().GetUseVersioning() && !request.GetWorkerVersionStamp().GetUseVersioning() {
|
|
return nil, serviceerror.NewInvalidArgument("Workflow using versioning must continue to use versioning.")
|
|
}
|
|
|
|
nsName := namespaceEntry.Name().String()
|
|
limits := workflow.WorkflowTaskCompletionLimits{
|
|
MaxResetPoints: handler.config.MaxAutoResetPoints(nsName),
|
|
MaxSearchAttributeValueSize: handler.config.SearchAttributesSizeOfValueLimit(nsName),
|
|
}
|
|
// TODO: this metric is inaccurate, it should only be emitted if a new binary checksum (or build ID) is added in this completion.
|
|
if ms.GetExecutionInfo().AutoResetPoints != nil && limits.MaxResetPoints == len(ms.GetExecutionInfo().AutoResetPoints.Points) {
|
|
metrics.AutoResetPointsLimitExceededCounter.With(handler.metricsHandler).Record(
|
|
1,
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope))
|
|
}
|
|
|
|
var wtHeartbeatTimedOut bool
|
|
var completedEvent *historypb.HistoryEvent
|
|
|
|
// SDKs set ForceCreateNewWorkflowTask flag to true when they are doing WT heartbeats.
|
|
// In a mean time, there might be pending commands and messages on the worker side.
|
|
// If those commands/messages are sent on the heartbeat WT it means that WF is making progress.
|
|
// WT heartbeat timeout is applicable only when WF doesn't make any progress and does heartbeats only.
|
|
checkWTHeartbeatTimeout := request.GetForceCreateNewWorkflowTask() && len(request.Commands) == 0 && len(request.Messages) == 0
|
|
|
|
if checkWTHeartbeatTimeout {
|
|
// WorkflowTaskHeartbeatTimeout is a total duration for which workflow is allowed to send continuous heartbeats.
|
|
// Default is 30 minutes.
|
|
// Heartbeat duration is computed between now and OriginalScheduledTime, which is set when WT is scheduled and
|
|
// carried over to the consequence WTs if they are heartbeat WTs.
|
|
// After this timeout is expired, WT is timed out (although this specific WT doesn't)
|
|
// and new WT will be scheduled on non-sticky task queue (see ClearStickyTaskQueue call bellow).
|
|
wtHeartbeatTimeoutDuration := handler.config.WorkflowTaskHeartbeatTimeout(nsName)
|
|
if currentWorkflowTask.OriginalScheduledTime.UnixNano() > 0 &&
|
|
handler.timeSource.Now().After(currentWorkflowTask.OriginalScheduledTime.Add(wtHeartbeatTimeoutDuration)) {
|
|
|
|
scope := handler.metricsHandler.WithTags(
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope),
|
|
metrics.NamespaceTag(nsName),
|
|
)
|
|
metrics.WorkflowTaskHeartbeatTimeoutCounter.With(scope).Record(1)
|
|
completedEvent, err = ms.AddWorkflowTaskTimedOutEvent(currentWorkflowTask)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ms.ClearStickyTaskQueue()
|
|
wtHeartbeatTimedOut = true
|
|
}
|
|
}
|
|
// WT wasn't timed out (due to too many heartbeats), therefore WTCompleted event should be created.
|
|
if !wtHeartbeatTimedOut {
|
|
completedEvent, err = ms.AddWorkflowTaskCompletedEvent(currentWorkflowTask, request, limits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// NOTE: completedEvent might be nil if WT was speculative and request has only `update.Rejection` messages.
|
|
// See workflowTaskStateMachine.skipWorkflowTaskCompletedEvent for more details.
|
|
|
|
if request.StickyAttributes == nil || request.StickyAttributes.WorkerTaskQueue == nil {
|
|
metrics.CompleteWorkflowTaskWithStickyDisabledCounter.With(handler.metricsHandler).Record(
|
|
1,
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope))
|
|
ms.ClearStickyTaskQueue()
|
|
} else {
|
|
metrics.CompleteWorkflowTaskWithStickyEnabledCounter.With(handler.metricsHandler).Record(
|
|
1,
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope))
|
|
ms.SetStickyTaskQueue(request.StickyAttributes.WorkerTaskQueue.GetName(), request.StickyAttributes.GetScheduleToStartTimeout())
|
|
}
|
|
|
|
var (
|
|
wtFailedCause *workflowTaskFailedCause
|
|
activityNotStartedCancelled bool
|
|
newMutableState workflow.MutableState
|
|
responseMutations []workflowTaskResponseMutation
|
|
)
|
|
updateRegistry := weContext.UpdateRegistry(ctx, nil)
|
|
// hasBufferedEventsOrMessages indicates if there are any buffered events
|
|
// or admitted updates which should generate a new workflow task.
|
|
|
|
// TODO: HasOutgoingMessages call (=check for admitted updates) is comment out
|
|
// because non-durable admitted updates can't block WF from closing,
|
|
// because everytime WFT is failing, WF context is cleared together with update registry
|
|
// and admitted updates are lost. Uncomment this check when durable admitted is implemented
|
|
// or updates stay in the registry after WFT is failed.
|
|
hasBufferedEventsOrMessages := ms.HasBufferedEvents() // || updateRegistry.HasOutgoingMessages(false)
|
|
if err := namespaceEntry.VerifyBinaryChecksum(request.GetBinaryChecksum()); err != nil {
|
|
wtFailedCause = newWorkflowTaskFailedCause(
|
|
enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_BINARY,
|
|
serviceerror.NewInvalidArgument(
|
|
fmt.Sprintf(
|
|
"binary %v is marked as bad deployment",
|
|
request.GetBinaryChecksum())),
|
|
nil)
|
|
} else {
|
|
namespace := namespaceEntry.Name()
|
|
workflowSizeChecker := newWorkflowSizeChecker(
|
|
workflowSizeLimits{
|
|
blobSizeLimitWarn: handler.config.BlobSizeLimitWarn(namespace.String()),
|
|
blobSizeLimitError: handler.config.BlobSizeLimitError(namespace.String()),
|
|
memoSizeLimitWarn: handler.config.MemoSizeLimitWarn(namespace.String()),
|
|
memoSizeLimitError: handler.config.MemoSizeLimitError(namespace.String()),
|
|
numPendingChildExecutionsLimit: handler.config.NumPendingChildExecutionsLimit(namespace.String()),
|
|
numPendingActivitiesLimit: handler.config.NumPendingActivitiesLimit(namespace.String()),
|
|
numPendingSignalsLimit: handler.config.NumPendingSignalsLimit(namespace.String()),
|
|
numPendingCancelsRequestLimit: handler.config.NumPendingCancelsRequestLimit(namespace.String()),
|
|
},
|
|
ms,
|
|
handler.searchAttributesValidator,
|
|
handler.metricsHandler.WithTags(
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope),
|
|
metrics.NamespaceTag(namespace.String()),
|
|
),
|
|
handler.throttledLogger,
|
|
)
|
|
|
|
workflowTaskHandler := newWorkflowTaskHandler(
|
|
request.GetIdentity(),
|
|
completedEvent.GetEventId(), // If completedEvent is nil, then GetEventId() returns 0 and this value shouldn't be used in workflowTaskHandler.
|
|
ms,
|
|
updateRegistry,
|
|
&effects,
|
|
handler.commandAttrValidator,
|
|
workflowSizeChecker,
|
|
handler.logger,
|
|
handler.namespaceRegistry,
|
|
handler.metricsHandler,
|
|
handler.config,
|
|
handler.shardContext,
|
|
handler.searchAttributesMapperProvider,
|
|
hasBufferedEventsOrMessages,
|
|
handler.commandHandlerRegistry,
|
|
)
|
|
|
|
if responseMutations, err = workflowTaskHandler.handleCommands(
|
|
ctx,
|
|
request.Commands,
|
|
collection.NewIndexedTakeList(
|
|
request.Messages,
|
|
func(msg *protocolpb.Message) string { return msg.Id },
|
|
),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Worker must respond with Update Accepted or Update Rejected message on every Update Requested
|
|
// message that were delivered on specific WT, when completing this WT.
|
|
// If worker ignored the update request (old SDK or SDK bug), then server rejects this update.
|
|
// Otherwise, this update will be delivered (and new WT created) again and again.
|
|
if err = workflowTaskHandler.rejectUnprocessedUpdates(
|
|
ctx,
|
|
currentWorkflowTask.ScheduledEventID,
|
|
request.GetForceCreateNewWorkflowTask(),
|
|
weContext.GetWorkflowKey(),
|
|
request.GetIdentity(),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// set the vars used by following logic
|
|
// further refactor should also clean up the vars used below
|
|
wtFailedCause = workflowTaskHandler.workflowTaskFailedCause
|
|
|
|
// failMessage is not used by workflowTaskHandlerCallbacks
|
|
activityNotStartedCancelled = workflowTaskHandler.activityNotStartedCancelled
|
|
// continueAsNewTimerTasks is not used by workflowTaskHandlerCallbacks
|
|
|
|
newMutableState = workflowTaskHandler.newMutableState
|
|
|
|
hasBufferedEventsOrMessages = workflowTaskHandler.hasBufferedEventsOrMessages
|
|
}
|
|
|
|
wtFailedShouldCreateNewTask := false
|
|
if wtFailedCause != nil {
|
|
effects.Cancel(ctx)
|
|
metrics.FailedWorkflowTasksCounter.With(handler.metricsHandler).Record(
|
|
1,
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope))
|
|
handler.logger.Info("Failing the workflow task.",
|
|
tag.Value(wtFailedCause.Message()),
|
|
tag.WorkflowID(token.GetWorkflowId()),
|
|
tag.WorkflowRunID(token.GetRunId()),
|
|
tag.WorkflowNamespaceID(namespaceEntry.ID().String()))
|
|
if currentWorkflowTask.Attempt > 1 && wtFailedCause.failedCause != enumspb.WORKFLOW_TASK_FAILED_CAUSE_UNHANDLED_COMMAND {
|
|
// drop this workflow task if it keeps failing. This will cause the workflow task to timeout and get retried after timeout.
|
|
return nil, serviceerror.NewInvalidArgument(wtFailedCause.Message())
|
|
}
|
|
var wtFailedEventID int64
|
|
ms, wtFailedEventID, err = failWorkflowTask(ctx, handler.shardContext, weContext, currentWorkflowTask, wtFailedCause, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wtFailedShouldCreateNewTask = true
|
|
newMutableState = nil
|
|
|
|
if wtFailedCause.workflowFailure != nil {
|
|
// Flush buffer event before failing the workflow
|
|
ms.FlushBufferedEvents()
|
|
|
|
attributes := &commandpb.FailWorkflowExecutionCommandAttributes{
|
|
Failure: wtFailedCause.workflowFailure,
|
|
}
|
|
if _, err := ms.AddFailWorkflowEvent(wtFailedEventID, enumspb.RETRY_STATE_NON_RETRYABLE_FAILURE, attributes, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
wtFailedShouldCreateNewTask = false
|
|
}
|
|
}
|
|
|
|
bufferedEventShouldCreateNewTask := hasBufferedEventsOrMessages && ms.HasAnyBufferedEvent(eventShouldGenerateNewTaskFilter)
|
|
if hasBufferedEventsOrMessages && !bufferedEventShouldCreateNewTask {
|
|
// Make sure tasks that should not create a new event don't get stuck in ms forever
|
|
ms.FlushBufferedEvents()
|
|
}
|
|
newWorkflowTaskType := enumsspb.WORKFLOW_TASK_TYPE_UNSPECIFIED
|
|
if ms.IsWorkflowExecutionRunning() {
|
|
if request.GetForceCreateNewWorkflowTask() || // Heartbeat WT is always of Normal type.
|
|
wtFailedShouldCreateNewTask ||
|
|
bufferedEventShouldCreateNewTask ||
|
|
activityNotStartedCancelled {
|
|
newWorkflowTaskType = enumsspb.WORKFLOW_TASK_TYPE_NORMAL
|
|
|
|
// There shouldn't be any sent updates in the registry because
|
|
// all sent but not processed updates were rejected by server.
|
|
// Therefore, it doesn't matter if to includeAlreadySent or not.
|
|
} else if updateRegistry.HasOutgoingMessages(true) {
|
|
if completedEvent == nil || ms.GetNextEventID() == completedEvent.GetEventId()+1 {
|
|
newWorkflowTaskType = enumsspb.WORKFLOW_TASK_TYPE_SPECULATIVE
|
|
} else {
|
|
newWorkflowTaskType = enumsspb.WORKFLOW_TASK_TYPE_NORMAL
|
|
}
|
|
}
|
|
}
|
|
|
|
bypassTaskGeneration := request.GetReturnNewWorkflowTask() && wtFailedCause == nil
|
|
// TODO (alex-update): Need to support case when ReturnNewWorkflowTask=false and WT.Type=Speculative.
|
|
// In this case WT needs to be added directly to matching.
|
|
// Current implementation will create normal WT.
|
|
if newWorkflowTaskType == enumsspb.WORKFLOW_TASK_TYPE_SPECULATIVE && !bypassTaskGeneration {
|
|
// If task generation can't be bypassed workflow task must be of Normal type because Speculative workflow task always skip task generation.
|
|
newWorkflowTaskType = enumsspb.WORKFLOW_TASK_TYPE_NORMAL
|
|
}
|
|
|
|
var newWorkflowTask *workflow.WorkflowTaskInfo
|
|
// Speculative workflow task will be created after mutable state is persisted.
|
|
if newWorkflowTaskType == enumsspb.WORKFLOW_TASK_TYPE_NORMAL {
|
|
var newWTErr error
|
|
// If we checked WT heartbeat timeout before and WT wasn't timed out,
|
|
// then OriginalScheduledTime needs to be carried over to the new WT.
|
|
if checkWTHeartbeatTimeout && !wtHeartbeatTimedOut {
|
|
newWorkflowTask, newWTErr = ms.AddWorkflowTaskScheduledEventAsHeartbeat(
|
|
bypassTaskGeneration,
|
|
timestamppb.New(currentWorkflowTask.OriginalScheduledTime),
|
|
enumsspb.WORKFLOW_TASK_TYPE_NORMAL, // Heartbeat workflow task is always of Normal type.
|
|
)
|
|
} else {
|
|
newWorkflowTask, newWTErr = ms.AddWorkflowTaskScheduledEvent(bypassTaskGeneration, newWorkflowTaskType)
|
|
}
|
|
if newWTErr != nil {
|
|
return nil, newWTErr
|
|
}
|
|
|
|
// skip transfer task for workflow task if request asking to return new workflow task
|
|
if bypassTaskGeneration {
|
|
versioningStamp := request.WorkerVersionStamp
|
|
if versioningStamp.GetUseVersioning() && ms.GetAssignedBuildId() == "" {
|
|
// old versioning is used. making sure the versioning stamp does not go through otherwise the
|
|
// workflow will start using new versioning which may surprise users.
|
|
// TODO: remove this block when deleting old wv [cleanup-old-wv]
|
|
versioningStamp = nil
|
|
}
|
|
// start the new workflow task if request asked to do so
|
|
// TODO: replace the poll request
|
|
_, newWorkflowTask, err = ms.AddWorkflowTaskStartedEvent(
|
|
newWorkflowTask.ScheduledEventID,
|
|
"request-from-RespondWorkflowTaskCompleted",
|
|
newWorkflowTask.TaskQueue,
|
|
request.Identity,
|
|
versioningStamp,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
var updateErr error
|
|
if newMutableState != nil {
|
|
newWorkflowExecutionInfo := newMutableState.GetExecutionInfo()
|
|
newWorkflowExecutionState := newMutableState.GetExecutionState()
|
|
updateErr = weContext.UpdateWorkflowExecutionWithNewAsActive(
|
|
ctx,
|
|
handler.shardContext,
|
|
workflow.NewContext(
|
|
handler.shardContext.GetConfig(),
|
|
definition.NewWorkflowKey(
|
|
newWorkflowExecutionInfo.NamespaceId,
|
|
newWorkflowExecutionInfo.WorkflowId,
|
|
newWorkflowExecutionState.RunId,
|
|
),
|
|
handler.logger,
|
|
handler.shardContext.GetThrottledLogger(),
|
|
handler.shardContext.GetMetricsHandler(),
|
|
),
|
|
newMutableState,
|
|
)
|
|
} else {
|
|
// If completedEvent is not nil (which means that this WT wasn't speculative)
|
|
// OR new WT is normal, then mutable state needs to be persisted.
|
|
// Otherwise, (both current and new WT are speculative) mutable state is updated in memory only but not persisted.
|
|
if completedEvent != nil || newWorkflowTaskType == enumsspb.WORKFLOW_TASK_TYPE_NORMAL {
|
|
updateErr = weContext.UpdateWorkflowExecutionAsActive(ctx, handler.shardContext)
|
|
}
|
|
}
|
|
|
|
if updateErr != nil {
|
|
effects.Cancel(ctx)
|
|
if persistence.IsConflictErr(updateErr) {
|
|
metrics.ConcurrencyUpdateFailureCounter.With(handler.metricsHandler).Record(
|
|
1,
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope))
|
|
}
|
|
|
|
// if updateErr resulted in TransactionSizeLimitError then fail workflow
|
|
switch updateErr.(type) {
|
|
case *persistence.TransactionSizeLimitError:
|
|
// must reload mutable state because the first call to updateWorkflowExecutionWithContext or continueAsNewWorkflowExecution
|
|
// clears mutable state if error is returned
|
|
ms, err = weContext.LoadMutableState(ctx, handler.shardContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := workflow.TerminateWorkflow(
|
|
ms,
|
|
common.FailureReasonTransactionSizeExceedsLimit,
|
|
payloads.EncodeString(updateErr.Error()),
|
|
consts.IdentityHistoryService,
|
|
false,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := weContext.UpdateWorkflowExecutionAsActive(
|
|
ctx,
|
|
handler.shardContext,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
updateRegistry.Abort(update.AbortReasonWorkflowCompleted)
|
|
}
|
|
|
|
return nil, updateErr
|
|
}
|
|
|
|
// If mutable state was persisted successfully (or persistence was skipped),
|
|
// then effects needs to be applied immediately to keep registry and mutable state in sync.
|
|
effects.Apply(ctx)
|
|
|
|
if !ms.IsWorkflowExecutionRunning() {
|
|
// If the workflow completed itself with one of the completion commands,
|
|
// abort all waiters with "workflow completed" error.
|
|
// Because all unprocessed updates were already rejected, incomplete updates in the registry are:
|
|
// - updates that were received while this WFT was running,
|
|
// - updates that were accepted but not completed by this WFT.
|
|
// It is important to call this after applying effects to be sure there are no unprocessed effects.
|
|
updateRegistry.Abort(update.AbortReasonWorkflowCompleted)
|
|
}
|
|
|
|
// Create speculative workflow task after mutable state is persisted.
|
|
if newWorkflowTaskType == enumsspb.WORKFLOW_TASK_TYPE_SPECULATIVE {
|
|
newWorkflowTask, err = ms.AddWorkflowTaskScheduledEvent(bypassTaskGeneration, newWorkflowTaskType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
versioningStamp := request.WorkerVersionStamp
|
|
if versioningStamp.GetUseVersioning() && ms.GetAssignedBuildId() == "" {
|
|
// old versioning is used. making sure the versioning stamp does not go through otherwise the
|
|
// workflow will start using new versioning which may surprise users.
|
|
// TODO: remove this block when deleting old wv [cleanup-old-wv]
|
|
versioningStamp = nil
|
|
}
|
|
_, newWorkflowTask, err = ms.AddWorkflowTaskStartedEvent(
|
|
newWorkflowTask.ScheduledEventID,
|
|
"request-from-RespondWorkflowTaskCompleted",
|
|
newWorkflowTask.TaskQueue,
|
|
request.Identity,
|
|
versioningStamp,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
handler.handleBufferedQueries(ms, req.GetCompleteRequest().GetQueryResults(), newWorkflowTask != nil, namespaceEntry)
|
|
|
|
if wtHeartbeatTimedOut {
|
|
// at this point, update is successful, but we still return an error to client so that the worker will give up this workflow
|
|
// release workflow lock with nil error to prevent mutable state from being cleared and reloaded
|
|
workflowLease.GetReleaseFn()(nil)
|
|
return nil, serviceerror.NewNotFound("workflow task heartbeat timeout")
|
|
}
|
|
|
|
if wtFailedCause != nil {
|
|
// release workflow lock with nil error to prevent mutable state from being cleared and reloaded
|
|
workflowLease.GetReleaseFn()(nil)
|
|
return nil, serviceerror.NewInvalidArgument(wtFailedCause.Message())
|
|
}
|
|
|
|
resp := &historyservice.RespondWorkflowTaskCompletedResponse{}
|
|
//nolint:staticcheck
|
|
if request.GetReturnNewWorkflowTask() && newWorkflowTask != nil {
|
|
resp.StartedResponse, err = handler.createRecordWorkflowTaskStartedResponse(ctx, ms, updateRegistry, newWorkflowTask, request.GetIdentity(), request.GetForceCreateNewWorkflowTask())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// sticky is always enabled when worker request for new workflow task from RespondWorkflowTaskCompleted
|
|
resp.StartedResponse.StickyExecutionEnabled = true
|
|
|
|
resp.NewWorkflowTask, err = handler.withNewWorkflowTask(ctx, namespaceEntry.Name(), req, resp.StartedResponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// If completedEvent is nil then it means that WT was speculative and
|
|
// WT events (scheduled/started/completed) were not written to the history and were dropped.
|
|
// SDK needs to know where to roll back its history event pointer, i.e. after what event all other events needs to be dropped.
|
|
// SDK uses WorkflowTaskStartedEventID to do that.
|
|
if completedEvent == nil {
|
|
resp.ResetHistoryEventId = ms.GetExecutionInfo().LastWorkflowTaskStartedEventId
|
|
}
|
|
|
|
for _, mutation := range responseMutations {
|
|
if err := mutation(resp); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) verifyFirstWorkflowTaskScheduled(
|
|
ctx context.Context,
|
|
req *historyservice.VerifyFirstWorkflowTaskScheduledRequest,
|
|
) (retError error) {
|
|
namespaceID := namespace.ID(req.GetNamespaceId())
|
|
if err := api.ValidateNamespaceUUID(namespaceID); err != nil {
|
|
return err
|
|
}
|
|
|
|
workflowLease, err := handler.workflowConsistencyChecker.GetWorkflowLease(
|
|
ctx,
|
|
req.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
req.NamespaceId,
|
|
req.WorkflowExecution.WorkflowId,
|
|
req.WorkflowExecution.RunId,
|
|
),
|
|
workflow.LockPriorityLow,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { workflowLease.GetReleaseFn()(retError) }()
|
|
|
|
mutableState := workflowLease.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() &&
|
|
mutableState.GetExecutionState().State != enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE {
|
|
return nil
|
|
}
|
|
|
|
if !mutableState.HadOrHasWorkflowTask() {
|
|
return consts.ErrWorkflowNotReady
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) createRecordWorkflowTaskStartedResponse(
|
|
ctx context.Context,
|
|
ms workflow.MutableState,
|
|
updateRegistry update.Registry,
|
|
workflowTask *workflow.WorkflowTaskInfo,
|
|
identity string,
|
|
wtHeartbeat bool,
|
|
) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
|
|
|
|
response := &historyservice.RecordWorkflowTaskStartedResponse{}
|
|
response.WorkflowType = ms.GetWorkflowType()
|
|
executionInfo := ms.GetExecutionInfo()
|
|
if executionInfo.LastWorkflowTaskStartedEventId != common.EmptyEventID {
|
|
response.PreviousStartedEventId = executionInfo.LastWorkflowTaskStartedEventId
|
|
}
|
|
|
|
// Starting workflowTask could result in different scheduledEventID if workflowTask was transient and new events came in
|
|
// before it was started.
|
|
response.ScheduledEventId = workflowTask.ScheduledEventID
|
|
response.StartedEventId = workflowTask.StartedEventID
|
|
response.StickyExecutionEnabled = ms.IsStickyTaskQueueSet()
|
|
response.NextEventId = ms.GetNextEventID()
|
|
response.Attempt = workflowTask.Attempt
|
|
response.WorkflowExecutionTaskQueue = &taskqueuepb.TaskQueue{
|
|
Name: executionInfo.TaskQueue,
|
|
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
|
|
}
|
|
response.ScheduledTime = timestamppb.New(workflowTask.ScheduledTime)
|
|
response.StartedTime = timestamppb.New(workflowTask.StartedTime)
|
|
response.Version = workflowTask.Version
|
|
|
|
// TODO (alex-update): Transient needs to be renamed to "TransientOrSpeculative"
|
|
response.TransientWorkflowTask = ms.GetTransientWorkflowTaskInfo(workflowTask, identity)
|
|
|
|
currentBranchToken, err := ms.GetCurrentBranchToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response.BranchToken = currentBranchToken
|
|
|
|
qr := ms.GetQueryRegistry()
|
|
bufferedQueryIDs := qr.GetBufferedIDs()
|
|
if len(bufferedQueryIDs) > 0 {
|
|
response.Queries = make(map[string]*querypb.WorkflowQuery, len(bufferedQueryIDs))
|
|
for _, bufferedQueryID := range bufferedQueryIDs {
|
|
input, err := qr.GetQueryInput(bufferedQueryID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
response.Queries[bufferedQueryID] = input
|
|
}
|
|
}
|
|
|
|
// If there are updates in the registry which were already sent to worker but still
|
|
// are not processed and not rejected by server, it means that WT that was used to
|
|
// deliver those updates failed, got timed out, or got lost.
|
|
// Resend these updates if this is not a heartbeat WT (includeAlreadySent = !wtHeartbeat).
|
|
// Heartbeat WT delivers only new updates that come while this WT was running (similar to queries and buffered events).
|
|
response.Messages = updateRegistry.Send(ctx, !wtHeartbeat, workflowTask.StartedEventID)
|
|
|
|
if workflowTask.Type == enumsspb.WORKFLOW_TASK_TYPE_SPECULATIVE && len(response.GetMessages()) == 0 {
|
|
return nil, serviceerror.NewNotFound("No messages for speculative workflow task.")
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) setHistoryForRecordWfTaskStartedResp(
|
|
ctx context.Context,
|
|
workflowKey definition.WorkflowKey,
|
|
maximumPageSize int32,
|
|
response *historyservice.RecordWorkflowTaskStartedResponse,
|
|
) (retError error) {
|
|
|
|
firstEventID := common.FirstEventID
|
|
nextEventID := response.GetNextEventId()
|
|
if response.GetStickyExecutionEnabled() {
|
|
// sticky tasks only need partial history
|
|
firstEventID = response.GetPreviousStartedEventId() + 1
|
|
}
|
|
|
|
// TODO below is a temporal solution to guard against invalid event batch
|
|
// when data inconsistency occurs
|
|
// long term solution should check event batch pointing backwards within history store
|
|
defer func() {
|
|
if _, ok := retError.(*serviceerror.DataLoss); ok {
|
|
api.TrimHistoryNode(
|
|
ctx,
|
|
handler.shardContext,
|
|
handler.workflowConsistencyChecker,
|
|
handler.eventNotifier,
|
|
workflowKey.GetNamespaceID(),
|
|
workflowKey.GetWorkflowID(),
|
|
workflowKey.GetRunID(),
|
|
)
|
|
}
|
|
}()
|
|
|
|
history, persistenceToken, err := api.GetHistory(
|
|
ctx,
|
|
handler.shardContext,
|
|
namespace.ID(workflowKey.GetNamespaceID()),
|
|
&commonpb.WorkflowExecution{WorkflowId: workflowKey.GetWorkflowID(), RunId: workflowKey.GetRunID()},
|
|
firstEventID,
|
|
nextEventID,
|
|
maximumPageSize,
|
|
nil,
|
|
response.GetTransientWorkflowTask(),
|
|
response.GetBranchToken(),
|
|
handler.persistenceVisibilityMgr,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var continuation []byte
|
|
if len(persistenceToken) != 0 {
|
|
continuation, err = api.SerializeHistoryToken(&tokenspb.HistoryContinuation{
|
|
RunId: workflowKey.GetRunID(),
|
|
FirstEventId: firstEventID,
|
|
NextEventId: nextEventID,
|
|
PersistenceToken: persistenceToken,
|
|
TransientWorkflowTask: response.GetTransientWorkflowTask(),
|
|
BranchToken: response.GetBranchToken(),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
response.History = history
|
|
response.NextPageToken = continuation
|
|
return nil
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) createPollWorkflowTaskQueueResponse(
|
|
ctx context.Context,
|
|
namespaceID namespace.ID,
|
|
matchingResp *matchingservice.PollWorkflowTaskQueueResponse,
|
|
branchToken []byte,
|
|
maximumPageSize int32,
|
|
) (_ *workflowservice.PollWorkflowTaskQueueResponse, retError error) {
|
|
|
|
if matchingResp.WorkflowExecution == nil {
|
|
// this will happen if there is no workflow task to be send to worker / caller
|
|
return &workflowservice.PollWorkflowTaskQueueResponse{}, nil
|
|
}
|
|
|
|
var history *historypb.History
|
|
var continuation []byte
|
|
var err error
|
|
|
|
if matchingResp.GetStickyExecutionEnabled() && matchingResp.Query != nil {
|
|
// meaning sticky query, we should not return any events to worker
|
|
// since query task only check the current status
|
|
history = &historypb.History{
|
|
Events: []*historypb.HistoryEvent{},
|
|
}
|
|
} else {
|
|
// here we have 3 cases:
|
|
// 1. sticky && non query task
|
|
// 2. non sticky && non query task
|
|
// 3. non sticky && query task
|
|
// for 1, partial history have to be send back
|
|
// for 2 and 3, full history have to be send back
|
|
|
|
var persistenceToken []byte
|
|
|
|
firstEventID := common.FirstEventID
|
|
nextEventID := matchingResp.GetNextEventId()
|
|
if matchingResp.GetStickyExecutionEnabled() {
|
|
firstEventID = matchingResp.GetPreviousStartedEventId() + 1
|
|
}
|
|
|
|
// TODO below is a temporal solution to guard against invalid event batch
|
|
// when data inconsistency occurs
|
|
// long term solution should check event batch pointing backwards within history store
|
|
defer func() {
|
|
if _, ok := retError.(*serviceerror.DataLoss); ok {
|
|
api.TrimHistoryNode(
|
|
ctx,
|
|
handler.shardContext,
|
|
handler.workflowConsistencyChecker,
|
|
handler.eventNotifier,
|
|
namespaceID.String(),
|
|
matchingResp.WorkflowExecution.GetWorkflowId(),
|
|
matchingResp.WorkflowExecution.GetRunId(),
|
|
)
|
|
}
|
|
}()
|
|
history, persistenceToken, err = api.GetHistory(
|
|
ctx,
|
|
handler.shardContext,
|
|
namespaceID,
|
|
matchingResp.GetWorkflowExecution(),
|
|
firstEventID,
|
|
nextEventID,
|
|
maximumPageSize,
|
|
nil,
|
|
matchingResp.GetTransientWorkflowTask(),
|
|
branchToken,
|
|
handler.persistenceVisibilityMgr,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(persistenceToken) != 0 {
|
|
continuation, err = api.SerializeHistoryToken(&tokenspb.HistoryContinuation{
|
|
RunId: matchingResp.WorkflowExecution.GetRunId(),
|
|
FirstEventId: firstEventID,
|
|
NextEventId: nextEventID,
|
|
PersistenceToken: persistenceToken,
|
|
TransientWorkflowTask: matchingResp.GetTransientWorkflowTask(),
|
|
BranchToken: branchToken,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
resp := &workflowservice.PollWorkflowTaskQueueResponse{
|
|
TaskToken: matchingResp.TaskToken,
|
|
WorkflowExecution: matchingResp.WorkflowExecution,
|
|
WorkflowType: matchingResp.WorkflowType,
|
|
PreviousStartedEventId: matchingResp.PreviousStartedEventId,
|
|
StartedEventId: matchingResp.StartedEventId,
|
|
Query: matchingResp.Query,
|
|
BacklogCountHint: matchingResp.BacklogCountHint,
|
|
Attempt: matchingResp.Attempt,
|
|
History: history,
|
|
NextPageToken: continuation,
|
|
WorkflowExecutionTaskQueue: matchingResp.WorkflowExecutionTaskQueue,
|
|
ScheduledTime: matchingResp.ScheduledTime,
|
|
StartedTime: matchingResp.StartedTime,
|
|
Queries: matchingResp.Queries,
|
|
Messages: matchingResp.Messages,
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) withNewWorkflowTask(
|
|
ctx context.Context,
|
|
namespaceName namespace.Name,
|
|
request *historyservice.RespondWorkflowTaskCompletedRequest,
|
|
response *historyservice.RecordWorkflowTaskStartedResponse,
|
|
) (*workflowservice.PollWorkflowTaskQueueResponse, error) {
|
|
taskToken, err := handler.tokenSerializer.Deserialize(request.CompleteRequest.TaskToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
taskToken = tasktoken.NewWorkflowTaskToken(
|
|
taskToken.GetNamespaceId(),
|
|
taskToken.GetWorkflowId(),
|
|
taskToken.GetRunId(),
|
|
response.GetScheduledEventId(),
|
|
response.GetStartedEventId(),
|
|
response.GetStartedTime(),
|
|
response.GetAttempt(),
|
|
response.GetClock(),
|
|
response.GetVersion(),
|
|
)
|
|
token, err := handler.tokenSerializer.Serialize(taskToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
workflowExecution := &commonpb.WorkflowExecution{
|
|
WorkflowId: taskToken.GetWorkflowId(),
|
|
RunId: taskToken.GetRunId(),
|
|
}
|
|
matchingResp := common.CreateMatchingPollWorkflowTaskQueueResponse(response, workflowExecution, token)
|
|
|
|
return handler.createPollWorkflowTaskQueueResponse(
|
|
ctx,
|
|
namespace.ID(taskToken.NamespaceId),
|
|
matchingResp,
|
|
matchingResp.GetBranchToken(),
|
|
int32(handler.config.HistoryMaxPageSize(namespaceName.String())),
|
|
)
|
|
}
|
|
|
|
func (handler *workflowTaskHandlerCallbacksImpl) handleBufferedQueries(ms workflow.MutableState, queryResults map[string]*querypb.WorkflowQueryResult, createNewWorkflowTask bool, namespaceEntry *namespace.Namespace) {
|
|
queryRegistry := ms.GetQueryRegistry()
|
|
if !queryRegistry.HasBufferedQuery() {
|
|
return
|
|
}
|
|
|
|
namespaceName := namespaceEntry.Name()
|
|
workflowID := ms.GetExecutionInfo().WorkflowId
|
|
runID := ms.GetExecutionState().GetRunId()
|
|
|
|
scope := handler.metricsHandler.WithTags(
|
|
metrics.OperationTag(metrics.HistoryRespondWorkflowTaskCompletedScope),
|
|
metrics.NamespaceTag(namespaceEntry.Name().String()),
|
|
metrics.CommandTypeTag("ConsistentQuery"))
|
|
|
|
sizeLimitError := handler.config.BlobSizeLimitError(namespaceName.String())
|
|
sizeLimitWarn := handler.config.BlobSizeLimitWarn(namespaceName.String())
|
|
|
|
// Complete or fail all queries we have results for
|
|
for id, result := range queryResults {
|
|
if err := common.CheckEventBlobSizeLimit(
|
|
result.GetAnswer().Size(),
|
|
sizeLimitWarn,
|
|
sizeLimitError,
|
|
namespaceName.String(),
|
|
workflowID,
|
|
runID,
|
|
scope,
|
|
handler.throttledLogger,
|
|
tag.BlobSizeViolationOperation("ConsistentQuery"),
|
|
); err != nil {
|
|
handler.logger.Info("failing query because query result size is too large",
|
|
tag.WorkflowNamespace(namespaceName.String()),
|
|
tag.WorkflowID(workflowID),
|
|
tag.WorkflowRunID(runID),
|
|
tag.QueryID(id),
|
|
tag.Error(err))
|
|
failedCompletionState := &workflow.QueryCompletionState{
|
|
Type: workflow.QueryCompletionTypeFailed,
|
|
Err: err,
|
|
}
|
|
if err := queryRegistry.SetCompletionState(id, failedCompletionState); err != nil {
|
|
handler.logger.Error(
|
|
"failed to set query completion state to failed",
|
|
tag.WorkflowNamespace(namespaceName.String()),
|
|
tag.WorkflowID(workflowID),
|
|
tag.WorkflowRunID(runID),
|
|
tag.QueryID(id),
|
|
tag.Error(err))
|
|
metrics.QueryRegistryInvalidStateCount.With(scope).Record(1)
|
|
}
|
|
} else {
|
|
succeededCompletionState := &workflow.QueryCompletionState{
|
|
Type: workflow.QueryCompletionTypeSucceeded,
|
|
Result: result,
|
|
}
|
|
if err := queryRegistry.SetCompletionState(id, succeededCompletionState); err != nil {
|
|
handler.logger.Error(
|
|
"failed to set query completion state to succeeded",
|
|
tag.WorkflowNamespace(namespaceName.String()),
|
|
tag.WorkflowID(workflowID),
|
|
tag.WorkflowRunID(runID),
|
|
tag.QueryID(id),
|
|
tag.Error(err))
|
|
metrics.QueryRegistryInvalidStateCount.With(scope).Record(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no workflow task was created then it means no buffered events came in during this workflow task's handling.
|
|
// This means all unanswered buffered queries can be dispatched directly through matching at this point.
|
|
if !createNewWorkflowTask {
|
|
buffered := queryRegistry.GetBufferedIDs()
|
|
for _, id := range buffered {
|
|
unblockCompletionState := &workflow.QueryCompletionState{
|
|
Type: workflow.QueryCompletionTypeUnblocked,
|
|
}
|
|
if err := queryRegistry.SetCompletionState(id, unblockCompletionState); err != nil {
|
|
handler.logger.Error(
|
|
"failed to set query completion state to unblocked",
|
|
tag.WorkflowNamespace(namespaceName.String()),
|
|
tag.WorkflowID(workflowID),
|
|
tag.WorkflowRunID(runID),
|
|
tag.QueryID(id),
|
|
tag.Error(err))
|
|
metrics.QueryRegistryInvalidStateCount.With(scope).Record(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func failWorkflowTask(
|
|
ctx context.Context,
|
|
shardContext shard.Context,
|
|
wfContext workflow.Context,
|
|
workflowTask *workflow.WorkflowTaskInfo,
|
|
wtFailedCause *workflowTaskFailedCause,
|
|
request *workflowservice.RespondWorkflowTaskCompletedRequest,
|
|
) (workflow.MutableState, int64, error) {
|
|
|
|
// clear any updates we have accumulated so far
|
|
wfContext.Clear()
|
|
|
|
// Reload workflow execution so we can apply the workflow task failure event
|
|
mutableState, err := wfContext.LoadMutableState(ctx, shardContext)
|
|
if err != nil {
|
|
return nil, common.EmptyEventID, err
|
|
}
|
|
wtFailedEvent, err := mutableState.AddWorkflowTaskFailedEvent(
|
|
workflowTask,
|
|
wtFailedCause.failedCause,
|
|
failure.NewServerFailure(wtFailedCause.Message(), false),
|
|
request.GetIdentity(),
|
|
nil,
|
|
request.GetBinaryChecksum(),
|
|
"",
|
|
"",
|
|
0,
|
|
)
|
|
if err != nil {
|
|
return nil, common.EmptyEventID, err
|
|
}
|
|
|
|
var wtFailedEventID int64
|
|
if wtFailedEvent != nil {
|
|
// If WTFailed event was added to the history then use its Id as wtFailedEventID.
|
|
wtFailedEventID = wtFailedEvent.GetEventId()
|
|
} else {
|
|
// Otherwise, if it was transient WT, last event should be WTFailed event from the 1st attempt.
|
|
wtFailedEventID = mutableState.GetNextEventID() - 1
|
|
}
|
|
|
|
// Return reloaded mutable state back to the caller for further updates.
|
|
return mutableState, wtFailedEventID, nil
|
|
}
|
|
|
|
// Filter function to be passed to mutable_state.HasAnyBufferedEvent
|
|
// Returns true if the event should generate a new workflow task
|
|
// Currently only signal events with SkipGenerateWorkflowTask=true flag set do not generate tasks
|
|
func eventShouldGenerateNewTaskFilter(event *historypb.HistoryEvent) bool {
|
|
if event.GetEventType() != enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED {
|
|
return true
|
|
}
|
|
return !event.GetWorkflowExecutionSignaledEventAttributes().GetSkipGenerateWorkflowTask()
|
|
}
|