mirror of
https://github.com/hanzoai/tasks.git
synced 2026-11-02 12:22:18 +00:00
3256 lines
108 KiB
Go
3256 lines
108 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 (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"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"
|
|
workflowpb "go.temporal.io/api/workflow/v1"
|
|
"go.temporal.io/api/workflowservice/v1"
|
|
|
|
clockspb "go.temporal.io/server/api/clock/v1"
|
|
enumsspb "go.temporal.io/server/api/enums/v1"
|
|
historyspb "go.temporal.io/server/api/history/v1"
|
|
"go.temporal.io/server/api/historyservice/v1"
|
|
"go.temporal.io/server/api/matchingservice/v1"
|
|
replicationspb "go.temporal.io/server/api/replication/v1"
|
|
tokenspb "go.temporal.io/server/api/token/v1"
|
|
"go.temporal.io/server/client"
|
|
"go.temporal.io/server/common"
|
|
"go.temporal.io/server/common/clock"
|
|
"go.temporal.io/server/common/cluster"
|
|
"go.temporal.io/server/common/definition"
|
|
"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/persistence"
|
|
"go.temporal.io/server/common/persistence/serialization"
|
|
"go.temporal.io/server/common/persistence/versionhistory"
|
|
"go.temporal.io/server/common/primitives/timestamp"
|
|
"go.temporal.io/server/common/sdk"
|
|
"go.temporal.io/server/common/searchattribute"
|
|
serviceerrors "go.temporal.io/server/common/serviceerror"
|
|
"go.temporal.io/server/service/history/api"
|
|
"go.temporal.io/server/service/history/api/signalwithstart"
|
|
"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/queues"
|
|
"go.temporal.io/server/service/history/replication"
|
|
"go.temporal.io/server/service/history/shard"
|
|
"go.temporal.io/server/service/history/tasks"
|
|
"go.temporal.io/server/service/history/workflow"
|
|
"go.temporal.io/server/service/worker/archiver"
|
|
)
|
|
|
|
const (
|
|
activityCancellationMsgActivityNotStarted = "ACTIVITY_ID_NOT_STARTED"
|
|
)
|
|
|
|
type (
|
|
historyEngineImpl struct {
|
|
status int32
|
|
currentClusterName string
|
|
shard shard.Context
|
|
timeSource clock.TimeSource
|
|
workflowTaskHandler workflowTaskHandlerCallbacks
|
|
clusterMetadata cluster.Metadata
|
|
executionManager persistence.ExecutionManager
|
|
queueProcessors map[tasks.Category]queues.Processor
|
|
replicationAckMgr replication.AckManager
|
|
nDCReplicator nDCHistoryReplicator
|
|
nDCActivityReplicator nDCActivityReplicator
|
|
replicationProcessorMgr common.Daemon
|
|
eventNotifier events.Notifier
|
|
tokenSerializer common.TaskTokenSerializer
|
|
metricsClient metrics.Client
|
|
logger log.Logger
|
|
throttledLogger log.Logger
|
|
config *configs.Config
|
|
workflowRebuilder workflowRebuilder
|
|
workflowResetter workflowResetter
|
|
sdkClientFactory sdk.ClientFactory
|
|
eventsReapplier nDCEventsReapplier
|
|
matchingClient matchingservice.MatchingServiceClient
|
|
rawMatchingClient matchingservice.MatchingServiceClient
|
|
replicationDLQHandler replication.DLQHandler
|
|
searchAttributesValidator *searchattribute.Validator
|
|
workflowDeleteManager workflow.DeleteManager
|
|
eventSerializer serialization.Serializer
|
|
workflowConsistencyChecker api.WorkflowConsistencyChecker
|
|
}
|
|
)
|
|
|
|
// NewEngineWithShardContext creates an instance of history engine
|
|
func NewEngineWithShardContext(
|
|
shard shard.Context,
|
|
clientBean client.Bean,
|
|
matchingClient matchingservice.MatchingServiceClient,
|
|
sdkClientFactory sdk.ClientFactory,
|
|
eventNotifier events.Notifier,
|
|
config *configs.Config,
|
|
rawMatchingClient matchingservice.MatchingServiceClient,
|
|
newCacheFn workflow.NewCacheFn,
|
|
archivalClient archiver.Client,
|
|
eventSerializer serialization.Serializer,
|
|
queueProcessorFactories []queues.ProcessorFactory,
|
|
replicationTaskFetcherFactory replication.TaskFetcherFactory,
|
|
replicationTaskExecutorProvider replication.TaskExecutorProvider,
|
|
) shard.Engine {
|
|
currentClusterName := shard.GetClusterMetadata().GetCurrentClusterName()
|
|
|
|
logger := shard.GetLogger()
|
|
executionManager := shard.GetExecutionManager()
|
|
historyCache := newCacheFn(shard)
|
|
|
|
workflowDeleteManager := workflow.NewDeleteManager(
|
|
shard,
|
|
historyCache,
|
|
config,
|
|
archivalClient,
|
|
shard.GetTimeSource(),
|
|
)
|
|
|
|
historyEngImpl := &historyEngineImpl{
|
|
status: common.DaemonStatusInitialized,
|
|
currentClusterName: currentClusterName,
|
|
shard: shard,
|
|
clusterMetadata: shard.GetClusterMetadata(),
|
|
timeSource: shard.GetTimeSource(),
|
|
executionManager: executionManager,
|
|
tokenSerializer: common.NewProtoTaskTokenSerializer(),
|
|
logger: log.With(logger, tag.ComponentHistoryEngine),
|
|
throttledLogger: log.With(shard.GetThrottledLogger(), tag.ComponentHistoryEngine),
|
|
metricsClient: shard.GetMetricsClient(),
|
|
eventNotifier: eventNotifier,
|
|
config: config,
|
|
sdkClientFactory: sdkClientFactory,
|
|
matchingClient: matchingClient,
|
|
rawMatchingClient: rawMatchingClient,
|
|
workflowDeleteManager: workflowDeleteManager,
|
|
eventSerializer: eventSerializer,
|
|
workflowConsistencyChecker: api.NewWorkflowConsistencyChecker(shard, historyCache),
|
|
}
|
|
|
|
historyEngImpl.queueProcessors = make(map[tasks.Category]queues.Processor)
|
|
for _, factory := range queueProcessorFactories {
|
|
processor := factory.CreateProcessor(shard, historyEngImpl, historyCache)
|
|
historyEngImpl.queueProcessors[processor.Category()] = processor
|
|
}
|
|
|
|
historyEngImpl.eventsReapplier = newNDCEventsReapplier(shard.GetMetricsClient(), logger)
|
|
|
|
if shard.GetClusterMetadata().IsGlobalNamespaceEnabled() {
|
|
historyEngImpl.replicationAckMgr = replication.NewAckManager(
|
|
shard,
|
|
historyCache,
|
|
executionManager,
|
|
logger,
|
|
)
|
|
historyEngImpl.nDCReplicator = newNDCHistoryReplicator(
|
|
shard,
|
|
historyCache,
|
|
historyEngImpl.eventsReapplier,
|
|
logger,
|
|
eventSerializer,
|
|
)
|
|
historyEngImpl.nDCActivityReplicator = newNDCActivityReplicator(
|
|
shard,
|
|
historyCache,
|
|
logger,
|
|
)
|
|
}
|
|
historyEngImpl.workflowRebuilder = NewWorkflowRebuilder(
|
|
shard,
|
|
historyCache,
|
|
logger,
|
|
)
|
|
historyEngImpl.workflowResetter = newWorkflowResetter(
|
|
shard,
|
|
historyCache,
|
|
logger,
|
|
)
|
|
|
|
historyEngImpl.searchAttributesValidator = searchattribute.NewValidator(
|
|
shard.GetSearchAttributesProvider(),
|
|
shard.GetSearchAttributesMapper(),
|
|
config.SearchAttributesNumberOfKeysLimit,
|
|
config.SearchAttributesSizeOfValueLimit,
|
|
config.SearchAttributesTotalSizeLimit,
|
|
)
|
|
|
|
historyEngImpl.workflowTaskHandler = newWorkflowTaskHandlerCallback(historyEngImpl)
|
|
historyEngImpl.replicationDLQHandler = replication.NewLazyDLQHandler(
|
|
shard,
|
|
workflowDeleteManager,
|
|
historyCache,
|
|
clientBean,
|
|
replicationTaskExecutorProvider,
|
|
)
|
|
historyEngImpl.replicationProcessorMgr = replication.NewTaskProcessorManager(
|
|
config,
|
|
shard,
|
|
historyEngImpl,
|
|
historyCache,
|
|
workflowDeleteManager,
|
|
clientBean,
|
|
eventSerializer,
|
|
replicationTaskFetcherFactory,
|
|
replicationTaskExecutorProvider,
|
|
)
|
|
return historyEngImpl
|
|
}
|
|
|
|
// Start will spin up all the components needed to start serving this shard.
|
|
// Make sure all the components are loaded lazily so start can return immediately. This is important because
|
|
// ShardController calls start sequentially for all the shards for a given host during startup.
|
|
func (e *historyEngineImpl) Start() {
|
|
if !atomic.CompareAndSwapInt32(
|
|
&e.status,
|
|
common.DaemonStatusInitialized,
|
|
common.DaemonStatusStarted,
|
|
) {
|
|
return
|
|
}
|
|
|
|
e.logger.Info("", tag.LifeCycleStarting)
|
|
defer e.logger.Info("", tag.LifeCycleStarted)
|
|
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.Start()
|
|
}
|
|
e.replicationProcessorMgr.Start()
|
|
|
|
// failover callback will try to create a failover queue processor to scan all inflight tasks
|
|
// if domain needs to be failovered. However, in the multicursor queue logic, the scan range
|
|
// can't be retrieved before the processor is started. If failover callback is registered
|
|
// before queue processor is started, it may result in a deadline as to create the failover queue,
|
|
// queue processor need to be started.
|
|
//
|
|
// Ideally, when both timer and transfer queues enabled single cursor mode, we don't have to register
|
|
// the callback. However, currently namespace migration is relying on the callback to UpdateHandoverNamespaces
|
|
e.registerNamespaceFailoverCallback()
|
|
}
|
|
|
|
// Stop the service.
|
|
func (e *historyEngineImpl) Stop() {
|
|
if !atomic.CompareAndSwapInt32(
|
|
&e.status,
|
|
common.DaemonStatusStarted,
|
|
common.DaemonStatusStopped,
|
|
) {
|
|
return
|
|
}
|
|
|
|
e.logger.Info("", tag.LifeCycleStopping)
|
|
defer e.logger.Info("", tag.LifeCycleStopped)
|
|
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.Stop()
|
|
}
|
|
e.replicationProcessorMgr.Stop()
|
|
// unset the failover callback
|
|
e.shard.GetNamespaceRegistry().UnregisterNamespaceChangeCallback(e)
|
|
}
|
|
|
|
func (e *historyEngineImpl) registerNamespaceFailoverCallback() {
|
|
|
|
// NOTE: READ BEFORE MODIFICATION
|
|
//
|
|
// Tasks, e.g. transfer tasks and timer tasks, are created when holding the shard lock
|
|
// meaning tasks -> release of shard lock
|
|
//
|
|
// Namespace change notification follows the following steps, order matters
|
|
// 1. lock all task processing.
|
|
// 2. namespace changes visible to everyone (Note: lock of task processing prevents task processing logic seeing the namespace changes).
|
|
// 3. failover min and max task levels are calculated, then update to shard.
|
|
// 4. failover start & task processing unlock & shard namespace version notification update. (order does not matter for this discussion)
|
|
//
|
|
// The above guarantees that task created during the failover will be processed.
|
|
// If the task is created after namespace change:
|
|
// then active processor will handle it. (simple case)
|
|
// If the task is created before namespace change:
|
|
// task -> release of shard lock
|
|
// failover min / max task levels calculated & updated to shard (using shard lock) -> failover start
|
|
// above 2 guarantees that failover start is after persistence of the task.
|
|
|
|
failoverPredicate := func(shardNotificationVersion int64, nextNamespace *namespace.Namespace, action func()) {
|
|
namespaceFailoverNotificationVersion := nextNamespace.FailoverNotificationVersion()
|
|
namespaceActiveCluster := nextNamespace.ActiveClusterName()
|
|
|
|
// +1 in the following check as the version in shard is max notification version +1.
|
|
// Need to run action() when namespaceFailoverNotificationVersion+1 == shardNotificationVersion
|
|
// as we don't know if the failover queue execution for that notification version is
|
|
// completed or not.
|
|
//
|
|
// NOTE: theoretically we need to get rid of the check on shardNotificationVersion, as
|
|
// we have no idea if the failover queue for any notification version below that is completed
|
|
// or not. However, removing that will cause more load upon shard reload.
|
|
// So here assume failover queue processor for notification version < X-1 is completed if
|
|
// shard notification version is X.
|
|
|
|
if nextNamespace.IsGlobalNamespace() &&
|
|
nextNamespace.ReplicationPolicy() == namespace.ReplicationPolicyMultiCluster &&
|
|
namespaceFailoverNotificationVersion+1 >= shardNotificationVersion &&
|
|
namespaceActiveCluster == e.currentClusterName {
|
|
action()
|
|
}
|
|
}
|
|
|
|
// first set the failover callback
|
|
e.shard.GetNamespaceRegistry().RegisterNamespaceChangeCallback(
|
|
e,
|
|
0, /* always want callback so UpdateHandoverNamespaces() can be called after shard reload */
|
|
func() {
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.LockTaskProcessing()
|
|
}
|
|
},
|
|
func(prevNamespaces []*namespace.Namespace, nextNamespaces []*namespace.Namespace) {
|
|
defer func() {
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.UnlockTaskProcessing()
|
|
}
|
|
}()
|
|
|
|
if len(nextNamespaces) == 0 {
|
|
return
|
|
}
|
|
|
|
if e.shard.GetClusterMetadata().IsGlobalNamespaceEnabled() {
|
|
maxTaskID, _ := e.replicationAckMgr.GetMaxTaskInfo()
|
|
e.shard.UpdateHandoverNamespaces(nextNamespaces, maxTaskID)
|
|
}
|
|
|
|
newNotificationVersion := nextNamespaces[len(nextNamespaces)-1].NotificationVersion() + 1
|
|
shardNotificationVersion := e.shard.GetNamespaceNotificationVersion()
|
|
|
|
// 1. We can't return when newNotificationVersion == shardNotificationVersion
|
|
// since we don't know if the previous failover queue processing has finished or not
|
|
// 2. We can return when newNotificationVersion < shardNotificationVersion. But the check
|
|
// is basically the same as the check in failover predicate. Because
|
|
// failoverNotificationVersion + 1 <= NotificationVersion + 1 = newNotificationVersion,
|
|
// there's no notification version can make
|
|
// newNotificationVersion < shardNotificationVersion and
|
|
// failoverNotificationVersion + 1 >= shardNotificationVersion are true at the same time
|
|
// Meaning if the check decides to return, no namespace will pass the failover predicate.
|
|
|
|
failoverNamespaceIDs := map[string]struct{}{}
|
|
for _, nextNamespace := range nextNamespaces {
|
|
failoverPredicate(shardNotificationVersion, nextNamespace, func() {
|
|
failoverNamespaceIDs[nextNamespace.ID().String()] = struct{}{}
|
|
})
|
|
}
|
|
|
|
if len(failoverNamespaceIDs) > 0 {
|
|
e.logger.Info("Namespace Failover Start.", tag.WorkflowNamespaceIDs(failoverNamespaceIDs))
|
|
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.FailoverNamespace(failoverNamespaceIDs)
|
|
}
|
|
|
|
// the fake tasks will not be actually used, we just need to make sure
|
|
// its length > 0 and has correct timestamp, to trigger a db scan
|
|
now := e.shard.GetTimeSource().Now()
|
|
fakeTasks := make(map[tasks.Category][]tasks.Task)
|
|
for category := range e.queueProcessors {
|
|
fakeTasks[category] = []tasks.Task{tasks.NewFakeTask(definition.WorkflowKey{}, category, now)}
|
|
}
|
|
e.NotifyNewTasks(e.currentClusterName, fakeTasks)
|
|
}
|
|
|
|
// nolint:errcheck
|
|
e.shard.UpdateNamespaceNotificationVersion(newNotificationVersion)
|
|
},
|
|
)
|
|
}
|
|
|
|
// StartWorkflowExecution starts a workflow execution
|
|
// Consistency guarantee: always write
|
|
func (e *historyEngineImpl) StartWorkflowExecution(
|
|
ctx context.Context,
|
|
startRequest *historyservice.StartWorkflowExecutionRequest,
|
|
) (resp *historyservice.StartWorkflowExecutionResponse, retError error) {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(startRequest.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
request := startRequest.StartRequest
|
|
e.overrideStartWorkflowExecutionRequest(request, metrics.HistoryStartWorkflowExecutionScope)
|
|
err = e.validateStartWorkflowExecutionRequest(ctx, request, namespaceEntry, "StartWorkflowExecution")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
workflowID := request.GetWorkflowId()
|
|
runID := uuid.New()
|
|
workflowContext, err := api.NewWorkflowWithSignal(
|
|
e.shard,
|
|
namespaceEntry,
|
|
workflowID,
|
|
runID,
|
|
startRequest,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := e.timeSource.Now()
|
|
newWorkflow, newWorkflowEventsSeq, err := workflowContext.GetMutableState().CloseTransactionAsSnapshot(
|
|
now,
|
|
workflow.TransactionPolicyActive,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(newWorkflowEventsSeq) != 1 {
|
|
return nil, serviceerror.NewInternal("unable to create 1st event batch")
|
|
}
|
|
|
|
// create as brand new
|
|
createMode := persistence.CreateWorkflowModeBrandNew
|
|
prevRunID := ""
|
|
prevLastWriteVersion := int64(0)
|
|
err = workflowContext.GetContext().CreateWorkflowExecution(
|
|
ctx,
|
|
now,
|
|
createMode,
|
|
prevRunID,
|
|
prevLastWriteVersion,
|
|
workflowContext.GetMutableState(),
|
|
newWorkflow,
|
|
newWorkflowEventsSeq,
|
|
)
|
|
if err == nil {
|
|
return &historyservice.StartWorkflowExecutionResponse{
|
|
RunId: runID,
|
|
}, nil
|
|
}
|
|
|
|
t, ok := err.(*persistence.CurrentWorkflowConditionFailedError)
|
|
if !ok {
|
|
return nil, err
|
|
}
|
|
|
|
// handle CurrentWorkflowConditionFailedError
|
|
if t.RequestID == request.GetRequestId() {
|
|
return &historyservice.StartWorkflowExecutionResponse{
|
|
RunId: t.RunID,
|
|
}, nil
|
|
// delete history is expected here because duplicate start request will create history with different rid
|
|
}
|
|
|
|
// create as ID reuse
|
|
prevRunID = t.RunID
|
|
prevLastWriteVersion = t.LastWriteVersion
|
|
if workflowContext.GetMutableState().GetCurrentVersion() < prevLastWriteVersion {
|
|
clusterMetadata := e.shard.GetClusterMetadata()
|
|
return nil, serviceerror.NewNamespaceNotActive(
|
|
request.GetNamespace(),
|
|
clusterMetadata.GetCurrentClusterName(),
|
|
clusterMetadata.ClusterNameForFailoverVersion(namespaceEntry.IsGlobalNamespace(), prevLastWriteVersion),
|
|
)
|
|
}
|
|
|
|
prevExecutionUpdateAction, err := api.ApplyWorkflowIDReusePolicy(
|
|
t.RequestID,
|
|
prevRunID,
|
|
t.State,
|
|
t.Status,
|
|
workflowID,
|
|
runID,
|
|
startRequest.StartRequest.GetWorkflowIdReusePolicy(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if prevExecutionUpdateAction != nil {
|
|
// update prev execution and create new execution in one transaction
|
|
err := e.updateWorkflowWithNew(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
prevRunID,
|
|
),
|
|
prevExecutionUpdateAction,
|
|
func() (workflow.Context, workflow.MutableState, error) {
|
|
workflowContext, err := api.NewWorkflowWithSignal(e.shard, namespaceEntry, workflowID, runID, startRequest, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return workflowContext.GetContext(), workflowContext.GetMutableState(), nil
|
|
},
|
|
)
|
|
switch err {
|
|
case nil:
|
|
return &historyservice.StartWorkflowExecutionResponse{
|
|
RunId: runID,
|
|
}, nil
|
|
case consts.ErrWorkflowCompleted:
|
|
// previous workflow already closed
|
|
// fallthough to the logic for only creating the new workflow below
|
|
default:
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err = workflowContext.GetContext().CreateWorkflowExecution(
|
|
ctx,
|
|
now,
|
|
persistence.CreateWorkflowModeUpdateCurrent,
|
|
prevRunID,
|
|
prevLastWriteVersion,
|
|
workflowContext.GetMutableState(),
|
|
newWorkflow,
|
|
newWorkflowEventsSeq,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.StartWorkflowExecutionResponse{
|
|
RunId: runID,
|
|
}, nil
|
|
}
|
|
|
|
// GetMutableState retrieves the mutable state of the workflow execution
|
|
func (e *historyEngineImpl) GetMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.GetMutableStateRequest,
|
|
) (*historyservice.GetMutableStateResponse, error) {
|
|
|
|
return e.getMutableStateOrPolling(ctx, request)
|
|
}
|
|
|
|
// PollMutableState retrieves the mutable state of the workflow execution with long polling
|
|
func (e *historyEngineImpl) PollMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.PollMutableStateRequest,
|
|
) (*historyservice.PollMutableStateResponse, error) {
|
|
|
|
response, err := e.getMutableStateOrPolling(ctx, &historyservice.GetMutableStateRequest{
|
|
NamespaceId: request.GetNamespaceId(),
|
|
Execution: request.Execution,
|
|
ExpectedNextEventId: request.ExpectedNextEventId,
|
|
CurrentBranchToken: request.CurrentBranchToken,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.PollMutableStateResponse{
|
|
Execution: response.Execution,
|
|
WorkflowType: response.WorkflowType,
|
|
NextEventId: response.NextEventId,
|
|
PreviousStartedEventId: response.PreviousStartedEventId,
|
|
LastFirstEventId: response.LastFirstEventId,
|
|
LastFirstEventTxnId: response.LastFirstEventTxnId,
|
|
TaskQueue: response.TaskQueue,
|
|
StickyTaskQueue: response.StickyTaskQueue,
|
|
StickyTaskQueueScheduleToStartTimeout: response.StickyTaskQueueScheduleToStartTimeout,
|
|
CurrentBranchToken: response.CurrentBranchToken,
|
|
VersionHistories: response.VersionHistories,
|
|
WorkflowState: response.WorkflowState,
|
|
WorkflowStatus: response.WorkflowStatus,
|
|
FirstExecutionRunId: response.FirstExecutionRunId,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) getMutableStateOrPolling(
|
|
ctx context.Context,
|
|
request *historyservice.GetMutableStateRequest,
|
|
) (*historyservice.GetMutableStateResponse, error) {
|
|
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
err := validateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(request.Execution.RunId) == 0 {
|
|
request.Execution.RunId, err = e.workflowConsistencyChecker.GetCurrentRunID(
|
|
ctx,
|
|
request.NamespaceId,
|
|
request.Execution.WorkflowId,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
workflowKey := definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Execution.WorkflowId,
|
|
request.Execution.RunId,
|
|
)
|
|
response, err := e.getMutableState(ctx, workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if request.CurrentBranchToken == nil {
|
|
request.CurrentBranchToken = response.CurrentBranchToken
|
|
}
|
|
if !bytes.Equal(request.CurrentBranchToken, response.CurrentBranchToken) {
|
|
return nil, serviceerrors.NewCurrentBranchChanged(response.CurrentBranchToken, request.CurrentBranchToken)
|
|
}
|
|
|
|
// expectedNextEventID is 0 when caller want to get the current next event ID without blocking
|
|
expectedNextEventID := common.FirstEventID
|
|
if request.ExpectedNextEventId != common.EmptyEventID {
|
|
expectedNextEventID = request.GetExpectedNextEventId()
|
|
}
|
|
|
|
// if caller decide to long poll on workflow execution
|
|
// and the event ID we are looking for is smaller than current next event ID
|
|
if expectedNextEventID >= response.GetNextEventId() && response.GetWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
subscriberID, channel, err := e.eventNotifier.WatchHistoryEvent(workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer e.eventNotifier.UnwatchHistoryEvent(workflowKey, subscriberID) // nolint:errcheck
|
|
// check again in case the next event ID is updated
|
|
response, err = e.getMutableState(ctx, workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// check again if the current branch token changed
|
|
if !bytes.Equal(request.CurrentBranchToken, response.CurrentBranchToken) {
|
|
return nil, serviceerrors.NewCurrentBranchChanged(response.CurrentBranchToken, request.CurrentBranchToken)
|
|
}
|
|
if expectedNextEventID < response.GetNextEventId() || response.GetWorkflowStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
return response, nil
|
|
}
|
|
|
|
namespaceRegistry, err := e.shard.GetNamespaceRegistry().GetNamespaceByID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timer := time.NewTimer(e.shard.GetConfig().LongPollExpirationInterval(namespaceRegistry.Name().String()))
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case event := <-channel:
|
|
response.LastFirstEventId = event.LastFirstEventID
|
|
response.LastFirstEventTxnId = event.LastFirstEventTxnID
|
|
response.NextEventId = event.NextEventID
|
|
response.PreviousStartedEventId = event.PreviousStartedEventID
|
|
response.WorkflowState = event.WorkflowState
|
|
response.WorkflowStatus = event.WorkflowStatus
|
|
if !bytes.Equal(request.CurrentBranchToken, event.CurrentBranchToken) {
|
|
return nil, serviceerrors.NewCurrentBranchChanged(event.CurrentBranchToken, request.CurrentBranchToken)
|
|
}
|
|
if expectedNextEventID < response.GetNextEventId() || response.GetWorkflowStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
return response, nil
|
|
}
|
|
case <-timer.C:
|
|
return response, nil
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) QueryWorkflow(
|
|
ctx context.Context,
|
|
request *historyservice.QueryWorkflowRequest,
|
|
) (_ *historyservice.QueryWorkflowResponse, retErr error) {
|
|
|
|
scope := e.metricsClient.Scope(metrics.HistoryQueryWorkflowScope)
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
err := validateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nsEntry, err := e.shard.GetNamespaceRegistry().GetNamespaceByID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(request.Request.Execution.RunId) == 0 {
|
|
request.Request.Execution.RunId, err = e.workflowConsistencyChecker.GetCurrentRunID(
|
|
ctx,
|
|
request.NamespaceId,
|
|
request.Request.Execution.WorkflowId,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
workflowKey := definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Request.Execution.WorkflowId,
|
|
request.Request.Execution.RunId,
|
|
)
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
workflowKey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retErr) }()
|
|
|
|
req := request.GetRequest()
|
|
_, mutableStateStatus := weCtx.GetMutableState().GetWorkflowStateStatus()
|
|
if mutableStateStatus != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING && req.QueryRejectCondition != enumspb.QUERY_REJECT_CONDITION_NONE {
|
|
notOpenReject := req.GetQueryRejectCondition() == enumspb.QUERY_REJECT_CONDITION_NOT_OPEN
|
|
notCompletedCleanlyReject := req.GetQueryRejectCondition() == enumspb.QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY && mutableStateStatus != enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED
|
|
if notOpenReject || notCompletedCleanlyReject {
|
|
return &historyservice.QueryWorkflowResponse{
|
|
Response: &workflowservice.QueryWorkflowResponse{
|
|
QueryRejected: &querypb.QueryRejected{
|
|
Status: mutableStateStatus,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
mutableState := weCtx.GetMutableState()
|
|
if !mutableState.HasProcessedOrPendingWorkflowTask() {
|
|
// workflow has no workflow task ever scheduled, this usually is due to firstWorkflowTaskBackoff (cron / retry)
|
|
// in this case, don't buffer the query, because it is almost certain the query will time out.
|
|
return nil, consts.ErrWorkflowTaskNotScheduled
|
|
}
|
|
|
|
// There are two ways in which queries get dispatched to workflow worker. First, queries can be dispatched on workflow tasks.
|
|
// These workflow tasks potentially contain new events and queries. The events are treated as coming before the query in time.
|
|
// The second way in which queries are dispatched to workflow worker is directly through matching; in this approach queries can be
|
|
// dispatched to workflow worker immediately even if there are outstanding events that came before the query. The following logic
|
|
// is used to determine if a query can be safely dispatched directly through matching or must be dispatched on a workflow task.
|
|
//
|
|
// Precondition to dispatch query directly to matching is workflow has at least one WorkflowTaskStarted event. Otherwise, sdk would panic.
|
|
if mutableState.GetPreviousStartedEventID() != common.EmptyEventID {
|
|
// There are three cases in which a query can be dispatched directly through matching safely, without violating strong consistency level:
|
|
// 1. the namespace is not active, in this case history is immutable so a query dispatched at any time is consistent
|
|
// 2. the workflow is not running, whenever a workflow is not running dispatching query directly is consistent
|
|
// 3. if there is no pending or started workflow tasks it means no events came before query arrived, so its safe to dispatch directly
|
|
safeToDispatchDirectly := !nsEntry.ActiveInCluster(e.clusterMetadata.GetCurrentClusterName()) ||
|
|
!mutableState.IsWorkflowExecutionRunning() ||
|
|
(!mutableState.HasPendingWorkflowTask() && !mutableState.HasInFlightWorkflowTask())
|
|
if safeToDispatchDirectly {
|
|
msResp, err := e.mutableStateToGetResponse(mutableState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
weCtx.GetReleaseFn()(nil)
|
|
req.Execution.RunId = msResp.Execution.RunId
|
|
return e.queryDirectlyThroughMatching(ctx, msResp, request.GetNamespaceId(), req, scope)
|
|
}
|
|
}
|
|
|
|
// If we get here it means query could not be dispatched through matching directly, so it must block
|
|
// until either an result has been obtained on a workflow task response or until it is safe to dispatch directly through matching.
|
|
sw := scope.StartTimer(metrics.WorkflowTaskQueryLatency)
|
|
defer sw.Stop()
|
|
queryReg := mutableState.GetQueryRegistry()
|
|
if len(queryReg.GetBufferedIDs()) >= e.config.MaxBufferedQueryCount() {
|
|
scope.IncCounter(metrics.QueryBufferExceededCount)
|
|
return nil, consts.ErrConsistentQueryBufferExceeded
|
|
}
|
|
queryID, termCh := queryReg.BufferQuery(req.GetQuery())
|
|
defer queryReg.RemoveQuery(queryID)
|
|
weCtx.GetReleaseFn()(nil)
|
|
select {
|
|
case <-termCh:
|
|
state, err := queryReg.GetTerminationState(queryID)
|
|
if err != nil {
|
|
scope.IncCounter(metrics.QueryRegistryInvalidStateCount)
|
|
return nil, err
|
|
}
|
|
switch state.QueryTerminationType {
|
|
case workflow.QueryTerminationTypeCompleted:
|
|
result := state.QueryResult
|
|
switch result.GetResultType() {
|
|
case enumspb.QUERY_RESULT_TYPE_ANSWERED:
|
|
return &historyservice.QueryWorkflowResponse{
|
|
Response: &workflowservice.QueryWorkflowResponse{
|
|
QueryResult: result.GetAnswer(),
|
|
},
|
|
}, nil
|
|
case enumspb.QUERY_RESULT_TYPE_FAILED:
|
|
return nil, serviceerror.NewQueryFailed(result.GetErrorMessage())
|
|
default:
|
|
scope.IncCounter(metrics.QueryRegistryInvalidStateCount)
|
|
return nil, consts.ErrQueryEnteredInvalidState
|
|
}
|
|
case workflow.QueryTerminationTypeUnblocked:
|
|
msResp, err := e.getMutableState(ctx, workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Execution.RunId = msResp.Execution.RunId
|
|
return e.queryDirectlyThroughMatching(ctx, msResp, request.GetNamespaceId(), req, scope)
|
|
case workflow.QueryTerminationTypeFailed:
|
|
return nil, state.Failure
|
|
default:
|
|
scope.IncCounter(metrics.QueryRegistryInvalidStateCount)
|
|
return nil, consts.ErrQueryEnteredInvalidState
|
|
}
|
|
case <-ctx.Done():
|
|
scope.IncCounter(metrics.ConsistentQueryTimeoutCount)
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (e *historyEngineImpl) queryDirectlyThroughMatching(
|
|
ctx context.Context,
|
|
msResp *historyservice.GetMutableStateResponse,
|
|
namespaceID string,
|
|
queryRequest *workflowservice.QueryWorkflowRequest,
|
|
scope metrics.Scope,
|
|
) (*historyservice.QueryWorkflowResponse, error) {
|
|
|
|
sw := scope.StartTimer(metrics.DirectQueryDispatchLatency)
|
|
defer sw.Stop()
|
|
|
|
if msResp.GetIsStickyTaskQueueEnabled() &&
|
|
len(msResp.GetStickyTaskQueue().GetName()) != 0 &&
|
|
e.config.EnableStickyQuery(queryRequest.GetNamespace()) {
|
|
|
|
stickyMatchingRequest := &matchingservice.QueryWorkflowRequest{
|
|
NamespaceId: namespaceID,
|
|
QueryRequest: queryRequest,
|
|
TaskQueue: msResp.GetStickyTaskQueue(),
|
|
}
|
|
|
|
// using a clean new context in case customer provide a context which has
|
|
// a really short deadline, causing we clear the stickiness
|
|
stickyContext, cancel := context.WithTimeout(context.Background(), timestamp.DurationValue(msResp.GetStickyTaskQueueScheduleToStartTimeout()))
|
|
stickyStopWatch := scope.StartTimer(metrics.DirectQueryDispatchStickyLatency)
|
|
matchingResp, err := e.rawMatchingClient.QueryWorkflow(stickyContext, stickyMatchingRequest)
|
|
stickyStopWatch.Stop()
|
|
cancel()
|
|
if err == nil {
|
|
scope.IncCounter(metrics.DirectQueryDispatchStickySuccessCount)
|
|
return &historyservice.QueryWorkflowResponse{
|
|
Response: &workflowservice.QueryWorkflowResponse{
|
|
QueryResult: matchingResp.GetQueryResult(),
|
|
QueryRejected: matchingResp.GetQueryRejected(),
|
|
}}, nil
|
|
}
|
|
if !common.IsContextDeadlineExceededErr(err) && !common.IsContextCanceledErr(err) && !common.IsStickyWorkerUnavailable(err) {
|
|
return nil, err
|
|
}
|
|
if msResp.GetWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
resetContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
clearStickinessStopWatch := scope.StartTimer(metrics.DirectQueryDispatchClearStickinessLatency)
|
|
_, err := e.ResetStickyTaskQueue(resetContext, &historyservice.ResetStickyTaskQueueRequest{
|
|
NamespaceId: namespaceID,
|
|
Execution: queryRequest.GetExecution(),
|
|
})
|
|
clearStickinessStopWatch.Stop()
|
|
cancel()
|
|
if err != nil && err != consts.ErrWorkflowCompleted {
|
|
return nil, err
|
|
}
|
|
scope.IncCounter(metrics.DirectQueryDispatchClearStickinessSuccessCount)
|
|
}
|
|
}
|
|
|
|
if err := common.IsValidContext(ctx); err != nil {
|
|
scope.IncCounter(metrics.DirectQueryDispatchTimeoutBeforeNonStickyCount)
|
|
return nil, err
|
|
}
|
|
|
|
nonStickyMatchingRequest := &matchingservice.QueryWorkflowRequest{
|
|
NamespaceId: namespaceID,
|
|
QueryRequest: queryRequest,
|
|
TaskQueue: msResp.TaskQueue,
|
|
}
|
|
|
|
nonStickyStopWatch := scope.StartTimer(metrics.DirectQueryDispatchNonStickyLatency)
|
|
matchingResp, err := e.matchingClient.QueryWorkflow(ctx, nonStickyMatchingRequest)
|
|
nonStickyStopWatch.Stop()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scope.IncCounter(metrics.DirectQueryDispatchNonStickySuccessCount)
|
|
return &historyservice.QueryWorkflowResponse{
|
|
Response: &workflowservice.QueryWorkflowResponse{
|
|
QueryResult: matchingResp.GetQueryResult(),
|
|
QueryRejected: matchingResp.GetQueryRejected(),
|
|
}}, err
|
|
}
|
|
|
|
func (e *historyEngineImpl) getMutableState(
|
|
ctx context.Context,
|
|
workflowKey definition.WorkflowKey,
|
|
) (_ *historyservice.GetMutableStateResponse, retError error) {
|
|
|
|
if len(workflowKey.RunID) == 0 {
|
|
return nil, serviceerror.NewInternal(fmt.Sprintf(
|
|
"getMutableState encountered empty run ID: %v", workflowKey,
|
|
))
|
|
}
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
workflowKey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
mutableState, err := weCtx.GetContext().LoadWorkflowExecution(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return e.mutableStateToGetResponse(mutableState)
|
|
}
|
|
|
|
func (e *historyEngineImpl) mutableStateToGetResponse(
|
|
mutableState workflow.MutableState,
|
|
) (_ *historyservice.GetMutableStateResponse, retError error) {
|
|
currentBranchToken, err := mutableState.GetCurrentBranchToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
executionInfo := mutableState.GetExecutionInfo()
|
|
workflowState, workflowStatus := mutableState.GetWorkflowStateStatus()
|
|
lastFirstEventID, lastFirstEventTxnID := mutableState.GetLastFirstEventIDTxnID()
|
|
return &historyservice.GetMutableStateResponse{
|
|
Execution: &commonpb.WorkflowExecution{
|
|
WorkflowId: mutableState.GetExecutionInfo().WorkflowId,
|
|
RunId: mutableState.GetExecutionState().RunId,
|
|
},
|
|
WorkflowType: &commonpb.WorkflowType{Name: executionInfo.WorkflowTypeName},
|
|
LastFirstEventId: lastFirstEventID,
|
|
LastFirstEventTxnId: lastFirstEventTxnID,
|
|
NextEventId: mutableState.GetNextEventID(),
|
|
PreviousStartedEventId: mutableState.GetPreviousStartedEventID(),
|
|
TaskQueue: &taskqueuepb.TaskQueue{
|
|
Name: executionInfo.TaskQueue,
|
|
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
|
|
},
|
|
StickyTaskQueue: &taskqueuepb.TaskQueue{
|
|
Name: executionInfo.StickyTaskQueue,
|
|
Kind: enumspb.TASK_QUEUE_KIND_STICKY,
|
|
},
|
|
StickyTaskQueueScheduleToStartTimeout: executionInfo.StickyScheduleToStartTimeout,
|
|
CurrentBranchToken: currentBranchToken,
|
|
WorkflowState: workflowState,
|
|
WorkflowStatus: workflowStatus,
|
|
IsStickyTaskQueueEnabled: mutableState.IsStickyTaskQueueEnabled(),
|
|
VersionHistories: versionhistory.CopyVersionHistories(
|
|
mutableState.GetExecutionInfo().GetVersionHistories(),
|
|
),
|
|
FirstExecutionRunId: executionInfo.FirstExecutionRunId,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) DescribeMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.DescribeMutableStateRequest,
|
|
) (response *historyservice.DescribeMutableStateResponse, retError error) {
|
|
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
err := validateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Execution.WorkflowId,
|
|
request.Execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
response = &historyservice.DescribeMutableStateResponse{}
|
|
|
|
if weCtx.GetContext().(*workflow.ContextImpl).MutableState != nil {
|
|
msb := weCtx.GetContext().(*workflow.ContextImpl).MutableState
|
|
response.CacheMutableState = msb.CloneToProto()
|
|
}
|
|
|
|
// clear mutable state to force reload from persistence. This API returns both cached and persisted version.
|
|
weCtx.GetContext().Clear()
|
|
mutableState, err := weCtx.GetContext().LoadWorkflowExecution(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.DatabaseMutableState = mutableState.CloneToProto()
|
|
return response, nil
|
|
}
|
|
|
|
// ResetStickyTaskQueue reset the volatile information in mutable state of a given workflow.
|
|
// Volatile information are the information related to client, such as:
|
|
// 1. StickyTaskQueue
|
|
// 2. StickyScheduleToStartTimeout
|
|
func (e *historyEngineImpl) ResetStickyTaskQueue(
|
|
ctx context.Context,
|
|
resetRequest *historyservice.ResetStickyTaskQueueRequest,
|
|
) (*historyservice.ResetStickyTaskQueueResponse, error) {
|
|
|
|
namespaceID := namespace.ID(resetRequest.GetNamespaceId())
|
|
err := validateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = e.updateWorkflow(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
resetRequest.NamespaceId,
|
|
resetRequest.Execution.WorkflowId,
|
|
resetRequest.Execution.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
mutableState.ClearStickyness()
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.ResetStickyTaskQueueResponse{}, nil
|
|
}
|
|
|
|
// DescribeWorkflowExecution returns information about the specified workflow execution.
|
|
func (e *historyEngineImpl) DescribeWorkflowExecution(
|
|
ctx context.Context,
|
|
request *historyservice.DescribeWorkflowExecutionRequest,
|
|
) (_ *historyservice.DescribeWorkflowExecutionResponse, retError error) {
|
|
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
err := validateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Request.Execution.WorkflowId,
|
|
request.Request.Execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
mutableState := weCtx.GetMutableState()
|
|
executionInfo := mutableState.GetExecutionInfo()
|
|
executionState := mutableState.GetExecutionState()
|
|
result := &historyservice.DescribeWorkflowExecutionResponse{
|
|
ExecutionConfig: &workflowpb.WorkflowExecutionConfig{
|
|
TaskQueue: &taskqueuepb.TaskQueue{
|
|
Name: executionInfo.TaskQueue,
|
|
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
|
|
},
|
|
WorkflowExecutionTimeout: executionInfo.WorkflowExecutionTimeout,
|
|
WorkflowRunTimeout: executionInfo.WorkflowRunTimeout,
|
|
DefaultWorkflowTaskTimeout: executionInfo.DefaultWorkflowTaskTimeout,
|
|
},
|
|
WorkflowExecutionInfo: &workflowpb.WorkflowExecutionInfo{
|
|
Execution: &commonpb.WorkflowExecution{
|
|
WorkflowId: executionInfo.WorkflowId,
|
|
RunId: executionState.RunId,
|
|
},
|
|
Type: &commonpb.WorkflowType{Name: executionInfo.WorkflowTypeName},
|
|
StartTime: executionInfo.StartTime,
|
|
Status: executionState.Status,
|
|
HistoryLength: mutableState.GetNextEventID() - common.FirstEventID,
|
|
ExecutionTime: executionInfo.ExecutionTime,
|
|
Memo: &commonpb.Memo{Fields: executionInfo.Memo},
|
|
SearchAttributes: &commonpb.SearchAttributes{IndexedFields: executionInfo.SearchAttributes},
|
|
AutoResetPoints: executionInfo.AutoResetPoints,
|
|
TaskQueue: executionInfo.TaskQueue,
|
|
StateTransitionCount: executionInfo.StateTransitionCount,
|
|
},
|
|
}
|
|
|
|
if executionInfo.ParentRunId != "" {
|
|
result.WorkflowExecutionInfo.ParentExecution = &commonpb.WorkflowExecution{
|
|
WorkflowId: executionInfo.ParentWorkflowId,
|
|
RunId: executionInfo.ParentRunId,
|
|
}
|
|
result.WorkflowExecutionInfo.ParentNamespaceId = executionInfo.ParentNamespaceId
|
|
}
|
|
if executionState.State == enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED {
|
|
// for closed workflow
|
|
result.WorkflowExecutionInfo.Status = executionState.Status
|
|
closeTime, err := mutableState.GetWorkflowCloseTime(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.WorkflowExecutionInfo.CloseTime = closeTime
|
|
}
|
|
|
|
if len(mutableState.GetPendingActivityInfos()) > 0 {
|
|
for _, ai := range mutableState.GetPendingActivityInfos() {
|
|
p := &workflowpb.PendingActivityInfo{
|
|
ActivityId: ai.ActivityId,
|
|
}
|
|
if ai.CancelRequested {
|
|
p.State = enumspb.PENDING_ACTIVITY_STATE_CANCEL_REQUESTED
|
|
} else if ai.StartedEventId != common.EmptyEventID {
|
|
p.State = enumspb.PENDING_ACTIVITY_STATE_STARTED
|
|
} else {
|
|
p.State = enumspb.PENDING_ACTIVITY_STATE_SCHEDULED
|
|
}
|
|
if !timestamp.TimeValue(ai.LastHeartbeatUpdateTime).IsZero() {
|
|
p.LastHeartbeatTime = ai.LastHeartbeatUpdateTime
|
|
p.HeartbeatDetails = ai.LastHeartbeatDetails
|
|
}
|
|
// TODO: move to mutable state instead of loading it from event
|
|
scheduledEvent, err := mutableState.GetActivityScheduledEvent(ctx, ai.ScheduledEventId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.ActivityType = scheduledEvent.GetActivityTaskScheduledEventAttributes().ActivityType
|
|
if p.State == enumspb.PENDING_ACTIVITY_STATE_SCHEDULED {
|
|
p.ScheduledTime = ai.ScheduledTime
|
|
} else {
|
|
p.LastStartedTime = ai.StartedTime
|
|
}
|
|
p.LastWorkerIdentity = ai.StartedIdentity
|
|
if ai.HasRetryPolicy {
|
|
p.Attempt = ai.Attempt
|
|
p.ExpirationTime = ai.RetryExpirationTime
|
|
if ai.RetryMaximumAttempts != 0 {
|
|
p.MaximumAttempts = ai.RetryMaximumAttempts
|
|
}
|
|
if ai.RetryLastFailure != nil {
|
|
p.LastFailure = ai.RetryLastFailure
|
|
}
|
|
if p.LastWorkerIdentity == "" && ai.RetryLastWorkerIdentity != "" {
|
|
p.LastWorkerIdentity = ai.RetryLastWorkerIdentity
|
|
}
|
|
} else {
|
|
p.Attempt = 1
|
|
}
|
|
result.PendingActivities = append(result.PendingActivities, p)
|
|
}
|
|
}
|
|
|
|
if len(mutableState.GetPendingChildExecutionInfos()) > 0 {
|
|
for _, ch := range mutableState.GetPendingChildExecutionInfos() {
|
|
p := &workflowpb.PendingChildExecutionInfo{
|
|
WorkflowId: ch.StartedWorkflowId,
|
|
RunId: ch.StartedRunId,
|
|
WorkflowTypeName: ch.WorkflowTypeName,
|
|
InitiatedId: ch.InitiatedEventId,
|
|
ParentClosePolicy: ch.ParentClosePolicy,
|
|
}
|
|
result.PendingChildren = append(result.PendingChildren, p)
|
|
}
|
|
}
|
|
|
|
if pendingWorkflowTask, ok := mutableState.GetPendingWorkflowTask(); ok {
|
|
result.PendingWorkflowTask = &workflowpb.PendingWorkflowTaskInfo{
|
|
State: enumspb.PENDING_WORKFLOW_TASK_STATE_SCHEDULED,
|
|
ScheduledTime: pendingWorkflowTask.ScheduledTime,
|
|
OriginalScheduledTime: pendingWorkflowTask.OriginalScheduledTime,
|
|
Attempt: pendingWorkflowTask.Attempt,
|
|
}
|
|
if pendingWorkflowTask.StartedEventID != common.EmptyEventID {
|
|
result.PendingWorkflowTask.State = enumspb.PENDING_WORKFLOW_TASK_STATE_STARTED
|
|
result.PendingWorkflowTask.StartedTime = pendingWorkflowTask.StartedTime
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) RecordActivityTaskStarted(
|
|
ctx context.Context,
|
|
request *historyservice.RecordActivityTaskStartedRequest,
|
|
) (*historyservice.RecordActivityTaskStartedResponse, error) {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(request.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespace := namespaceEntry.Name()
|
|
|
|
response := &historyservice.RecordActivityTaskStartedResponse{}
|
|
err = e.updateWorkflow(
|
|
ctx,
|
|
request.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.WorkflowExecution.WorkflowId,
|
|
request.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
scheduledEventID := request.GetScheduledEventId()
|
|
requestID := request.GetRequestId()
|
|
ai, isRunning := mutableState.GetActivityInfo(scheduledEventID)
|
|
|
|
metricsScope := e.metricsClient.Scope(metrics.HistoryRecordActivityTaskStartedScope)
|
|
|
|
// 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 !isRunning && scheduledEventID >= mutableState.GetNextEventID() {
|
|
metricsScope.IncCounter(metrics.StaleMutableStateCounter)
|
|
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 !isRunning {
|
|
// Looks like ActivityTask already completed as a result of another call.
|
|
// It is OK to drop the task at this point.
|
|
return nil, consts.ErrActivityTaskNotFound
|
|
}
|
|
|
|
scheduledEvent, err := mutableState.GetActivityScheduledEvent(ctx, scheduledEventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response.ScheduledEvent = scheduledEvent
|
|
response.CurrentAttemptScheduledTime = ai.ScheduledTime
|
|
|
|
if ai.StartedEventId != common.EmptyEventID {
|
|
// If activity is started as part of the current request scope then return a positive response
|
|
if ai.RequestId == requestID {
|
|
response.StartedTime = ai.StartedTime
|
|
response.Attempt = ai.Attempt
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
// Looks like ActivityTask already started as a result of another call.
|
|
// It is OK to drop the task at this point.
|
|
return nil, serviceerrors.NewTaskAlreadyStarted("Activity")
|
|
}
|
|
|
|
if _, err := mutableState.AddActivityTaskStartedEvent(
|
|
ai, scheduledEventID, requestID, request.PollRequest.GetIdentity(),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scheduleToStartLatency := ai.GetStartedTime().Sub(*ai.GetScheduledTime())
|
|
namespaceName := namespaceEntry.Name()
|
|
taskQueueName := ai.GetTaskQueue()
|
|
|
|
metrics.GetPerTaskQueueScope(
|
|
metricsScope,
|
|
namespaceName.String(),
|
|
taskQueueName,
|
|
enumspb.TASK_QUEUE_KIND_NORMAL,
|
|
).Tagged(metrics.TaskQueueTypeTag(enumspb.TASK_QUEUE_TYPE_ACTIVITY)).
|
|
RecordTimer(metrics.TaskScheduleToStartLatency, scheduleToStartLatency)
|
|
|
|
response.StartedTime = ai.StartedTime
|
|
response.Attempt = ai.Attempt
|
|
response.HeartbeatDetails = ai.LastHeartbeatDetails
|
|
|
|
response.WorkflowType = mutableState.GetWorkflowType()
|
|
response.WorkflowNamespace = namespace.String()
|
|
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
|
|
// ScheduleWorkflowTask schedules a workflow task if no outstanding workflow task found
|
|
func (e *historyEngineImpl) ScheduleWorkflowTask(
|
|
ctx context.Context,
|
|
req *historyservice.ScheduleWorkflowTaskRequest,
|
|
) error {
|
|
return e.workflowTaskHandler.handleWorkflowTaskScheduled(ctx, req)
|
|
}
|
|
|
|
func (e *historyEngineImpl) VerifyFirstWorkflowTaskScheduled(
|
|
ctx context.Context,
|
|
request *historyservice.VerifyFirstWorkflowTaskScheduledRequest,
|
|
) (retError error) {
|
|
return e.workflowTaskHandler.verifyFirstWorkflowTaskScheduled(ctx, request)
|
|
}
|
|
|
|
// RecordWorkflowTaskStarted starts a workflow task
|
|
func (e *historyEngineImpl) RecordWorkflowTaskStarted(
|
|
ctx context.Context,
|
|
request *historyservice.RecordWorkflowTaskStartedRequest,
|
|
) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
|
|
return e.workflowTaskHandler.handleWorkflowTaskStarted(ctx, request)
|
|
}
|
|
|
|
// RespondWorkflowTaskCompleted completes a workflow task
|
|
func (e *historyEngineImpl) RespondWorkflowTaskCompleted(
|
|
ctx context.Context,
|
|
req *historyservice.RespondWorkflowTaskCompletedRequest,
|
|
) (*historyservice.RespondWorkflowTaskCompletedResponse, error) {
|
|
return e.workflowTaskHandler.handleWorkflowTaskCompleted(ctx, req)
|
|
}
|
|
|
|
// RespondWorkflowTaskFailed fails a workflow task
|
|
func (e *historyEngineImpl) RespondWorkflowTaskFailed(
|
|
ctx context.Context,
|
|
req *historyservice.RespondWorkflowTaskFailedRequest,
|
|
) error {
|
|
return e.workflowTaskHandler.handleWorkflowTaskFailed(ctx, req)
|
|
}
|
|
|
|
// RespondActivityTaskCompleted completes an activity task.
|
|
func (e *historyEngineImpl) RespondActivityTaskCompleted(
|
|
ctx context.Context,
|
|
req *historyservice.RespondActivityTaskCompletedRequest,
|
|
) error {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespace := namespaceEntry.Name()
|
|
|
|
request := req.CompleteRequest
|
|
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
|
|
if err0 != nil {
|
|
return consts.ErrDeserializingToken
|
|
}
|
|
if err := e.setActivityTaskRunID(ctx, token); err != nil {
|
|
return err
|
|
}
|
|
|
|
var activityStartedTime time.Time
|
|
var taskQueue string
|
|
var workflowTypeName string
|
|
err = e.updateWorkflow(
|
|
ctx,
|
|
token.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
token.NamespaceId,
|
|
token.WorkflowId,
|
|
token.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
workflowTypeName = mutableState.GetWorkflowType().GetName()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
scheduledEventID := token.GetScheduledEventId()
|
|
if scheduledEventID == common.EmptyEventID { // client call CompleteActivityById, so get scheduledEventID by activityID
|
|
scheduledEventID, err0 = getscheduledEventID(token.GetActivityId(), mutableState)
|
|
if err0 != nil {
|
|
return nil, err0
|
|
}
|
|
}
|
|
ai, isRunning := mutableState.GetActivityInfo(scheduledEventID)
|
|
|
|
// 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 !isRunning && scheduledEventID >= mutableState.GetNextEventID() {
|
|
e.metricsClient.IncCounter(metrics.HistoryRespondActivityTaskCompletedScope, metrics.StaleMutableStateCounter)
|
|
return nil, consts.ErrStaleState
|
|
}
|
|
|
|
if !isRunning || ai.StartedEventId == common.EmptyEventID ||
|
|
(token.GetScheduledEventId() != common.EmptyEventID && token.Attempt != ai.Attempt) {
|
|
return nil, consts.ErrActivityTaskNotFound
|
|
}
|
|
|
|
if _, err := mutableState.AddActivityTaskCompletedEvent(scheduledEventID, ai.StartedEventId, request); err != nil {
|
|
// Unable to add ActivityTaskCompleted event to history
|
|
return nil, err
|
|
}
|
|
activityStartedTime = *ai.StartedTime
|
|
taskQueue = ai.TaskQueue
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: true,
|
|
}, nil
|
|
})
|
|
|
|
if err == nil && !activityStartedTime.IsZero() {
|
|
scope := e.metricsClient.Scope(metrics.HistoryRespondActivityTaskCompletedScope).
|
|
Tagged(
|
|
metrics.NamespaceTag(namespace.String()),
|
|
metrics.WorkflowTypeTag(workflowTypeName),
|
|
metrics.ActivityTypeTag(token.ActivityType),
|
|
metrics.TaskQueueTag(taskQueue),
|
|
)
|
|
scope.RecordTimer(metrics.ActivityE2ELatency, time.Since(activityStartedTime))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RespondActivityTaskFailed completes an activity task failure.
|
|
func (e *historyEngineImpl) RespondActivityTaskFailed(
|
|
ctx context.Context,
|
|
req *historyservice.RespondActivityTaskFailedRequest,
|
|
) error {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespace := namespaceEntry.Name()
|
|
|
|
request := req.FailedRequest
|
|
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
|
|
if err0 != nil {
|
|
return consts.ErrDeserializingToken
|
|
}
|
|
if err := e.setActivityTaskRunID(ctx, token); err != nil {
|
|
return err
|
|
}
|
|
|
|
var activityStartedTime time.Time
|
|
var taskQueue string
|
|
var workflowTypeName string
|
|
err = e.updateWorkflow(
|
|
ctx,
|
|
token.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
token.NamespaceId,
|
|
token.WorkflowId,
|
|
token.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
workflowTypeName = mutableState.GetWorkflowType().GetName()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
scheduledEventID := token.GetScheduledEventId()
|
|
if scheduledEventID == common.EmptyEventID { // client call CompleteActivityById, so get scheduledEventID by activityID
|
|
scheduledEventID, err0 = getscheduledEventID(token.GetActivityId(), mutableState)
|
|
if err0 != nil {
|
|
return nil, err0
|
|
}
|
|
}
|
|
ai, isRunning := mutableState.GetActivityInfo(scheduledEventID)
|
|
|
|
// 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 !isRunning && scheduledEventID >= mutableState.GetNextEventID() {
|
|
e.metricsClient.IncCounter(metrics.HistoryRespondActivityTaskFailedScope, metrics.StaleMutableStateCounter)
|
|
return nil, consts.ErrStaleState
|
|
}
|
|
|
|
if !isRunning || ai.StartedEventId == common.EmptyEventID ||
|
|
(token.GetScheduledEventId() != common.EmptyEventID && token.Attempt != ai.Attempt) {
|
|
return nil, consts.ErrActivityTaskNotFound
|
|
}
|
|
|
|
e.logger.Debug("RespondActivityTaskFailed", tag.WorkflowScheduledEventID(scheduledEventID), tag.ActivityInfo(ai), tag.NewBoolTag("hasHeartbeatDetails", request.GetLastHeartbeatDetails() != nil))
|
|
|
|
if request.GetLastHeartbeatDetails() != nil {
|
|
// Save heartbeat details as progress
|
|
mutableState.UpdateActivityProgress(ai, &workflowservice.RecordActivityTaskHeartbeatRequest{
|
|
TaskToken: request.GetTaskToken(),
|
|
Details: request.GetLastHeartbeatDetails(),
|
|
Identity: request.GetIdentity(),
|
|
Namespace: request.GetNamespace(),
|
|
})
|
|
}
|
|
|
|
postActions := &api.UpdateWorkflowAction{}
|
|
failure := request.GetFailure()
|
|
retryState, err := mutableState.RetryActivity(ai, failure)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if retryState != enumspb.RETRY_STATE_IN_PROGRESS {
|
|
// no more retry, and we want to record the failure event
|
|
if _, err := mutableState.AddActivityTaskFailedEvent(scheduledEventID, ai.StartedEventId, failure, retryState, request.GetIdentity()); err != nil {
|
|
// Unable to add ActivityTaskFailed event to history
|
|
return nil, err
|
|
}
|
|
postActions.CreateWorkflowTask = true
|
|
}
|
|
|
|
activityStartedTime = *ai.StartedTime
|
|
taskQueue = ai.TaskQueue
|
|
return postActions, nil
|
|
})
|
|
if err == nil && !activityStartedTime.IsZero() {
|
|
scope := e.metricsClient.Scope(metrics.HistoryRespondActivityTaskFailedScope).
|
|
Tagged(
|
|
metrics.NamespaceTag(namespace.String()),
|
|
metrics.WorkflowTypeTag(workflowTypeName),
|
|
metrics.ActivityTypeTag(token.ActivityType),
|
|
metrics.TaskQueueTag(taskQueue),
|
|
)
|
|
scope.RecordTimer(metrics.ActivityE2ELatency, time.Since(activityStartedTime))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RespondActivityTaskCanceled completes an activity task failure.
|
|
func (e *historyEngineImpl) RespondActivityTaskCanceled(
|
|
ctx context.Context,
|
|
req *historyservice.RespondActivityTaskCanceledRequest,
|
|
) error {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespace := namespaceEntry.Name()
|
|
|
|
request := req.CancelRequest
|
|
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
|
|
if err0 != nil {
|
|
return consts.ErrDeserializingToken
|
|
}
|
|
if err := e.setActivityTaskRunID(ctx, token); err != nil {
|
|
return err
|
|
}
|
|
|
|
var activityStartedTime time.Time
|
|
var taskQueue string
|
|
var workflowTypeName string
|
|
err = e.updateWorkflow(
|
|
ctx,
|
|
token.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
token.NamespaceId,
|
|
token.WorkflowId,
|
|
token.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
workflowTypeName = mutableState.GetWorkflowType().GetName()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
scheduledEventID := token.GetScheduledEventId()
|
|
if scheduledEventID == common.EmptyEventID { // client call CompleteActivityById, so get scheduledEventID by activityID
|
|
scheduledEventID, err0 = getscheduledEventID(token.GetActivityId(), mutableState)
|
|
if err0 != nil {
|
|
return nil, err0
|
|
}
|
|
}
|
|
ai, isRunning := mutableState.GetActivityInfo(scheduledEventID)
|
|
|
|
// 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 !isRunning && scheduledEventID >= mutableState.GetNextEventID() {
|
|
e.metricsClient.IncCounter(metrics.HistoryRespondActivityTaskCanceledScope, metrics.StaleMutableStateCounter)
|
|
return nil, consts.ErrStaleState
|
|
}
|
|
|
|
if !isRunning || ai.StartedEventId == common.EmptyEventID ||
|
|
(token.GetScheduledEventId() != common.EmptyEventID && token.Attempt != ai.Attempt) {
|
|
return nil, consts.ErrActivityTaskNotFound
|
|
}
|
|
|
|
// sanity check if activity is requested to be cancelled
|
|
if !ai.CancelRequested {
|
|
return nil, consts.ErrActivityTaskNotCancelRequested
|
|
}
|
|
|
|
if _, err := mutableState.AddActivityTaskCanceledEvent(
|
|
scheduledEventID,
|
|
ai.StartedEventId,
|
|
ai.CancelRequestId,
|
|
request.Details,
|
|
request.Identity); err != nil {
|
|
// Unable to add ActivityTaskCanceled event to history
|
|
return nil, err
|
|
}
|
|
|
|
activityStartedTime = *ai.StartedTime
|
|
taskQueue = ai.TaskQueue
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: true,
|
|
}, nil
|
|
})
|
|
|
|
if err == nil && !activityStartedTime.IsZero() {
|
|
scope := e.metricsClient.Scope(metrics.HistoryRespondActivityTaskCanceledScope).
|
|
Tagged(
|
|
metrics.NamespaceTag(namespace.String()),
|
|
metrics.WorkflowTypeTag(workflowTypeName),
|
|
metrics.ActivityTypeTag(token.ActivityType),
|
|
metrics.TaskQueueTag(taskQueue),
|
|
)
|
|
scope.RecordTimer(metrics.ActivityE2ELatency, time.Since(activityStartedTime))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RecordActivityTaskHeartbeat records an hearbeat for a task.
|
|
// This method can be used for two purposes.
|
|
// - For reporting liveness of the activity.
|
|
// - For reporting progress of the activity, this can be done even if the liveness is not configured.
|
|
func (e *historyEngineImpl) RecordActivityTaskHeartbeat(
|
|
ctx context.Context,
|
|
req *historyservice.RecordActivityTaskHeartbeatRequest,
|
|
) (*historyservice.RecordActivityTaskHeartbeatResponse, error) {
|
|
|
|
_, err := e.getActiveNamespaceEntry(namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request := req.HeartbeatRequest
|
|
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
|
|
if err0 != nil {
|
|
return nil, consts.ErrDeserializingToken
|
|
}
|
|
if err := e.setActivityTaskRunID(ctx, token); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cancelRequested bool
|
|
err = e.updateWorkflow(
|
|
ctx,
|
|
token.Clock,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
token.NamespaceId,
|
|
token.WorkflowId,
|
|
token.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
e.logger.Debug("Heartbeat failed")
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
scheduledEventID := token.GetScheduledEventId()
|
|
if scheduledEventID == common.EmptyEventID { // client call RecordActivityHeartbeatByID, so get scheduledEventID by activityID
|
|
scheduledEventID, err0 = getscheduledEventID(token.GetActivityId(), mutableState)
|
|
if err0 != nil {
|
|
return nil, err0
|
|
}
|
|
}
|
|
ai, isRunning := mutableState.GetActivityInfo(scheduledEventID)
|
|
|
|
// 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 !isRunning && scheduledEventID >= mutableState.GetNextEventID() {
|
|
e.metricsClient.IncCounter(metrics.HistoryRecordActivityTaskHeartbeatScope, metrics.StaleMutableStateCounter)
|
|
return nil, consts.ErrStaleState
|
|
}
|
|
|
|
if !isRunning || ai.StartedEventId == common.EmptyEventID ||
|
|
(token.GetScheduledEventId() != common.EmptyEventID && token.Attempt != ai.Attempt) {
|
|
return nil, consts.ErrActivityTaskNotFound
|
|
}
|
|
|
|
cancelRequested = ai.CancelRequested
|
|
|
|
e.logger.Debug("Activity heartbeat", tag.WorkflowScheduledEventID(scheduledEventID), tag.ActivityInfo(ai), tag.Bool(cancelRequested))
|
|
|
|
// Save progress and last HB reported time.
|
|
mutableState.UpdateActivityProgress(ai, request)
|
|
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return &historyservice.RecordActivityTaskHeartbeatResponse{}, err
|
|
}
|
|
|
|
return &historyservice.RecordActivityTaskHeartbeatResponse{CancelRequested: cancelRequested}, nil
|
|
}
|
|
|
|
// RequestCancelWorkflowExecution records request cancellation event for workflow execution
|
|
func (e *historyEngineImpl) RequestCancelWorkflowExecution(
|
|
ctx context.Context,
|
|
req *historyservice.RequestCancelWorkflowExecutionRequest,
|
|
) error {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(req.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
request := req.CancelRequest
|
|
parentExecution := req.ExternalWorkflowExecution
|
|
childWorkflowOnly := req.GetChildWorkflowOnly()
|
|
workflowID := request.WorkflowExecution.WorkflowId
|
|
runID := request.WorkflowExecution.RunId
|
|
firstExecutionRunID := request.FirstExecutionRunId
|
|
if len(firstExecutionRunID) != 0 {
|
|
runID = ""
|
|
}
|
|
|
|
return e.updateWorkflow(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
runID,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
// the request to cancel this workflow is a success even
|
|
// if the target workflow has already finished
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
// There is a workflow execution currently running with the WorkflowID.
|
|
// If user passed in a FirstExecutionRunID with the request to allow cancel to work across runs then
|
|
// let's compare the FirstExecutionRunID on the request to make sure we cancel the correct workflow
|
|
// execution.
|
|
executionInfo := mutableState.GetExecutionInfo()
|
|
if len(firstExecutionRunID) > 0 && executionInfo.FirstExecutionRunId != firstExecutionRunID {
|
|
return nil, consts.ErrWorkflowExecutionNotFound
|
|
}
|
|
|
|
if childWorkflowOnly {
|
|
parentWorkflowID := executionInfo.ParentWorkflowId
|
|
parentRunID := executionInfo.ParentRunId
|
|
if parentExecution.GetWorkflowId() != parentWorkflowID ||
|
|
parentExecution.GetRunId() != parentRunID {
|
|
return nil, consts.ErrWorkflowParent
|
|
}
|
|
}
|
|
|
|
isCancelRequested := mutableState.IsCancelRequested()
|
|
if isCancelRequested {
|
|
// since cancellation is idempotent
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
if _, err := mutableState.AddWorkflowExecutionCancelRequestedEvent(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return api.UpdateWorkflowWithNewWorkflowTask, nil
|
|
})
|
|
}
|
|
|
|
func (e *historyEngineImpl) SignalWorkflowExecution(
|
|
ctx context.Context,
|
|
signalRequest *historyservice.SignalWorkflowExecutionRequest,
|
|
) error {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(signalRequest.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
request := signalRequest.SignalRequest
|
|
parentExecution := signalRequest.ExternalWorkflowExecution
|
|
childWorkflowOnly := signalRequest.GetChildWorkflowOnly()
|
|
|
|
return e.updateWorkflow(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
request.WorkflowExecution.WorkflowId,
|
|
request.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if request.GetRequestId() != "" && mutableState.IsSignalRequested(request.GetRequestId()) {
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
executionInfo := mutableState.GetExecutionInfo()
|
|
createWorkflowTask := true
|
|
if mutableState.IsWorkflowPendingOnWorkflowTaskBackoff() {
|
|
// Do not create workflow task when the workflow has first workflow task backoff and execution is not started yet
|
|
createWorkflowTask = false
|
|
}
|
|
|
|
if err := api.ValidateSignal(
|
|
ctx,
|
|
e.shard,
|
|
mutableState,
|
|
request.GetInput().Size(),
|
|
"SignalWorkflowExecution",
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if childWorkflowOnly {
|
|
parentWorkflowID := executionInfo.ParentWorkflowId
|
|
parentRunID := executionInfo.ParentRunId
|
|
if parentExecution.GetWorkflowId() != parentWorkflowID ||
|
|
parentExecution.GetRunId() != parentRunID {
|
|
return nil, consts.ErrWorkflowParent
|
|
}
|
|
}
|
|
|
|
if request.GetRequestId() != "" {
|
|
mutableState.AddSignalRequested(request.GetRequestId())
|
|
}
|
|
if _, err := mutableState.AddWorkflowExecutionSignaled(
|
|
request.GetSignalName(),
|
|
request.GetInput(),
|
|
request.GetIdentity(),
|
|
request.GetHeader()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: createWorkflowTask,
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
// SignalWithStartWorkflowExecution signals current workflow (if running) or creates & signals a new workflow
|
|
// Consistency guarantee: always write
|
|
func (e *historyEngineImpl) SignalWithStartWorkflowExecution(
|
|
ctx context.Context,
|
|
signalWithStartRequest *historyservice.SignalWithStartWorkflowExecutionRequest,
|
|
) (_ *historyservice.SignalWithStartWorkflowExecutionResponse, retError error) {
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(signalWithStartRequest.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
var currentWorkflowContext api.WorkflowContext
|
|
currentWorkflowContext, err = e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
string(namespaceID),
|
|
signalWithStartRequest.SignalWithStartRequest.WorkflowId,
|
|
"",
|
|
),
|
|
)
|
|
switch err.(type) {
|
|
case nil:
|
|
defer func() { currentWorkflowContext.GetReleaseFn()(retError) }()
|
|
case *serviceerror.NotFound:
|
|
currentWorkflowContext = nil
|
|
default:
|
|
return nil, err
|
|
}
|
|
|
|
// Start workflow and signal
|
|
startRequest := e.getStartRequest(namespaceID, signalWithStartRequest.SignalWithStartRequest)
|
|
request := startRequest.StartRequest
|
|
e.overrideStartWorkflowExecutionRequest(request, metrics.HistorySignalWithStartWorkflowExecutionScope)
|
|
err = e.validateStartWorkflowExecutionRequest(ctx, request, namespaceEntry, "SignalWithStartWorkflowExecution")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runID, err := signalwithstart.SignalWithStartWorkflow(
|
|
ctx,
|
|
e.shard,
|
|
namespaceEntry,
|
|
currentWorkflowContext,
|
|
startRequest,
|
|
signalWithStartRequest.SignalWithStartRequest,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.SignalWithStartWorkflowExecutionResponse{
|
|
RunId: runID,
|
|
}, nil
|
|
}
|
|
|
|
// RemoveSignalMutableState remove the signal request id in signal_requested for deduplicate
|
|
func (e *historyEngineImpl) RemoveSignalMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.RemoveSignalMutableStateRequest,
|
|
) error {
|
|
|
|
_, err := e.getActiveNamespaceEntry(namespace.ID(request.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.updateWorkflow(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.WorkflowExecution.WorkflowId,
|
|
request.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
mutableState.DeleteSignalRequested(request.GetRequestId())
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (e *historyEngineImpl) TerminateWorkflowExecution(
|
|
ctx context.Context,
|
|
terminateRequest *historyservice.TerminateWorkflowExecutionRequest,
|
|
) error {
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(terminateRequest.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
request := terminateRequest.TerminateRequest
|
|
parentExecution := terminateRequest.ExternalWorkflowExecution
|
|
childWorkflowOnly := terminateRequest.ChildWorkflowOnly
|
|
workflowID := request.WorkflowExecution.WorkflowId
|
|
runID := request.WorkflowExecution.RunId
|
|
firstExecutionRunID := request.FirstExecutionRunId
|
|
if len(request.FirstExecutionRunId) != 0 {
|
|
runID = ""
|
|
}
|
|
return e.updateWorkflow(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
runID,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
// There is a workflow execution currently running with the WorkflowID.
|
|
// If user passed in a FirstExecutionRunID with the request to allow terminate to work across runs then
|
|
// let's compare the FirstExecutionRunID on the request to make sure we terminate the correct workflow
|
|
// execution.
|
|
executionInfo := mutableState.GetExecutionInfo()
|
|
if len(firstExecutionRunID) > 0 && executionInfo.FirstExecutionRunId != firstExecutionRunID {
|
|
return nil, consts.ErrWorkflowExecutionNotFound
|
|
}
|
|
|
|
if childWorkflowOnly {
|
|
if parentExecution.GetWorkflowId() != executionInfo.ParentWorkflowId ||
|
|
parentExecution.GetRunId() != executionInfo.ParentRunId {
|
|
return nil, consts.ErrWorkflowParent
|
|
}
|
|
}
|
|
|
|
eventBatchFirstEventID := mutableState.GetNextEventID()
|
|
|
|
return api.UpdateWorkflowWithoutWorkflowTask, workflow.TerminateWorkflow(
|
|
mutableState,
|
|
eventBatchFirstEventID,
|
|
request.GetReason(),
|
|
request.GetDetails(),
|
|
request.GetIdentity(),
|
|
false,
|
|
)
|
|
})
|
|
}
|
|
|
|
func (e *historyEngineImpl) DeleteWorkflowExecution(
|
|
ctx context.Context,
|
|
request *historyservice.DeleteWorkflowExecutionRequest,
|
|
) (retError error) {
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.WorkflowExecution.WorkflowId,
|
|
request.WorkflowExecution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
// Open and Close workflow executions are deleted differently.
|
|
// Open workflow execution is deleted by terminating with special flag `deleteAfterTerminate` set to true.
|
|
// This flag will be carried over with CloseExecutionTask and workflow will be deleted as the last step while processing the task.
|
|
//
|
|
// Close workflow execution is deleted using DeleteExecutionTask.
|
|
//
|
|
// DeleteWorkflowExecution is not replicated automatically. Workflow executions must be deleted separately in each cluster.
|
|
// Although running workflows in active cluster are terminated first and the termination event might be replicated.
|
|
// In passive cluster, workflow executions are just deleted in regardless of its state.
|
|
|
|
if weCtx.GetMutableState().IsWorkflowExecutionRunning() {
|
|
if request.GetClosedWorkflowOnly() {
|
|
// skip delete open workflow
|
|
return nil
|
|
}
|
|
ns, err := e.shard.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(request.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ns.ActiveInCluster(e.shard.GetClusterMetadata().GetCurrentClusterName()) {
|
|
// If workflow execution is running and in active cluster.
|
|
return api.UpdateWorkflowWithNew(
|
|
e.shard,
|
|
ctx,
|
|
weCtx,
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
eventBatchFirstEventID := mutableState.GetNextEventID()
|
|
|
|
return api.UpdateWorkflowWithoutWorkflowTask, workflow.TerminateWorkflow(
|
|
mutableState,
|
|
eventBatchFirstEventID,
|
|
"Delete workflow execution",
|
|
nil,
|
|
consts.IdentityHistoryService,
|
|
true,
|
|
)
|
|
},
|
|
nil)
|
|
}
|
|
}
|
|
|
|
// If workflow execution is closed or in passive cluster.
|
|
return e.workflowDeleteManager.AddDeleteWorkflowExecutionTask(
|
|
ctx,
|
|
namespace.ID(request.GetNamespaceId()),
|
|
commonpb.WorkflowExecution{
|
|
WorkflowId: request.GetWorkflowExecution().GetWorkflowId(),
|
|
RunId: request.GetWorkflowExecution().GetRunId(),
|
|
},
|
|
weCtx.GetMutableState(),
|
|
// Use cluster ack level for transfer queue ack level because it gets updated more often.
|
|
e.shard.GetQueueClusterAckLevel(tasks.CategoryTransfer, e.shard.GetClusterMetadata().GetCurrentClusterName()).TaskID,
|
|
// Use global ack level visibility queue ack level because cluster level is not updated.
|
|
e.shard.GetQueueAckLevel(tasks.CategoryVisibility).TaskID,
|
|
request.GetWorkflowVersion(),
|
|
)
|
|
}
|
|
|
|
// RecordChildExecutionCompleted records the completion of child execution into parent execution history
|
|
func (e *historyEngineImpl) RecordChildExecutionCompleted(
|
|
ctx context.Context,
|
|
completionRequest *historyservice.RecordChildExecutionCompletedRequest,
|
|
) error {
|
|
|
|
_, err := e.getActiveNamespaceEntry(namespace.ID(completionRequest.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parentInitiatedID := completionRequest.ParentInitiatedId
|
|
parentInitiatedVersion := completionRequest.ParentInitiatedVersion
|
|
|
|
return e.updateWorkflow(
|
|
ctx,
|
|
completionRequest.Clock,
|
|
func(mutableState workflow.MutableState) bool {
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
// current branch already closed, we won't perform any operation, pass the check
|
|
return true
|
|
}
|
|
|
|
onCurrentBranch, err := historyEventOnCurrentBranch(mutableState, parentInitiatedID, parentInitiatedVersion)
|
|
if err != nil {
|
|
// can't find initiated event, potential stale mutable, fail the predicate check
|
|
return false
|
|
}
|
|
if !onCurrentBranch {
|
|
// found on different branch, since we don't record completion on a different branch, pass the check
|
|
return true
|
|
}
|
|
|
|
ci, isRunning := mutableState.GetChildExecutionInfo(parentInitiatedID)
|
|
return !(isRunning && ci.StartedEventId == common.EmptyEventID) // !(potential stale)
|
|
},
|
|
definition.NewWorkflowKey(
|
|
completionRequest.NamespaceId,
|
|
completionRequest.WorkflowExecution.WorkflowId,
|
|
completionRequest.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
onCurrentBranch, err := historyEventOnCurrentBranch(mutableState, parentInitiatedID, parentInitiatedVersion)
|
|
if err != nil || !onCurrentBranch {
|
|
return nil, consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
// Check mutable state to make sure child execution is in pending child executions
|
|
ci, isRunning := mutableState.GetChildExecutionInfo(parentInitiatedID)
|
|
if !isRunning || ci.StartedEventId == common.EmptyEventID {
|
|
// note we already checked if startedEventID is empty (in consistency predicate)
|
|
// and reloaded mutable state
|
|
return nil, consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
completedExecution := completionRequest.CompletedExecution
|
|
if ci.GetStartedWorkflowId() != completedExecution.GetWorkflowId() {
|
|
// this can only happen when we don't have the initiated version
|
|
return nil, consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
completionEvent := completionRequest.CompletionEvent
|
|
switch completionEvent.GetEventType() {
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED:
|
|
attributes := completionEvent.GetWorkflowExecutionCompletedEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionCompletedEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_FAILED:
|
|
attributes := completionEvent.GetWorkflowExecutionFailedEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionFailedEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED:
|
|
attributes := completionEvent.GetWorkflowExecutionCanceledEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionCanceledEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED:
|
|
attributes := completionEvent.GetWorkflowExecutionTerminatedEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionTerminatedEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT:
|
|
attributes := completionEvent.GetWorkflowExecutionTimedOutEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionTimedOutEvent(parentInitiatedID, completedExecution, attributes)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: true,
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (e *historyEngineImpl) VerifyChildExecutionCompletionRecorded(
|
|
ctx context.Context,
|
|
request *historyservice.VerifyChildExecutionCompletionRecordedRequest,
|
|
) (retError error) {
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
if err := validateNamespaceUUID(namespaceID); err != nil {
|
|
return err
|
|
}
|
|
|
|
workflowContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
request.Clock,
|
|
// it's ok we have stale state when doing verification,
|
|
// the logic will return WorkflowNotReady error and the caller will retry
|
|
// this can prevent keep reloading mutable state when there's a replication lag
|
|
// in parent shard.
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.ParentExecution.WorkflowId,
|
|
request.ParentExecution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
if _, ok := err.(*serviceerror.NotFound); ok {
|
|
// workflow not found error, verification logic need to keep waiting in this case
|
|
// if we return NotFound directly, caller can't tell if it's workflow not found or child not found
|
|
// standby logic will continue verification
|
|
return consts.ErrWorkflowNotReady
|
|
}
|
|
return err
|
|
}
|
|
defer func() { workflowContext.GetReleaseFn()(retError) }()
|
|
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() &&
|
|
mutableState.GetExecutionState().State != enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE {
|
|
// standby logic will stop verification as the parent has already completed
|
|
// and can't be blocked after failover.
|
|
return consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
onCurrentBranch, err := historyEventOnCurrentBranch(mutableState, request.ParentInitiatedId, request.ParentInitiatedVersion)
|
|
if err != nil {
|
|
// initiated event not found on any branch
|
|
return consts.ErrWorkflowNotReady
|
|
}
|
|
|
|
if !onCurrentBranch {
|
|
// due to conflict resolution, the initiated event may on a different branch of the workflow.
|
|
// we don't have to do anything and can simply return not found error. Standby logic
|
|
// after seeing this error will give up verification.
|
|
return consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
ci, isRunning := mutableState.GetChildExecutionInfo(request.ParentInitiatedId)
|
|
if isRunning {
|
|
if ci.StartedEventId != common.EmptyEventID &&
|
|
ci.GetStartedWorkflowId() != request.ChildExecution.GetWorkflowId() {
|
|
// this can happen since we may not have the initiated version
|
|
return consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
return consts.ErrWorkflowNotReady
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) ReplicateEventsV2(
|
|
ctx context.Context,
|
|
replicateRequest *historyservice.ReplicateEventsV2Request,
|
|
) error {
|
|
|
|
return e.nDCReplicator.ApplyEvents(ctx, replicateRequest)
|
|
}
|
|
|
|
// ReplicateWorkflowState is an experimental method to replicate workflow state. This should not expose outside of history service role.
|
|
func (e *historyEngineImpl) ReplicateWorkflowState(
|
|
ctx context.Context,
|
|
request *historyservice.ReplicateWorkflowStateRequest,
|
|
) error {
|
|
|
|
return e.nDCReplicator.ApplyWorkflowState(ctx, request)
|
|
}
|
|
|
|
func (e *historyEngineImpl) SyncShardStatus(
|
|
ctx context.Context,
|
|
request *historyservice.SyncShardStatusRequest,
|
|
) error {
|
|
|
|
clusterName := request.GetSourceCluster()
|
|
now := timestamp.TimeValue(request.GetStatusTime())
|
|
|
|
// here there are 3 main things
|
|
// 1. update the view of remote cluster's shard time
|
|
// 2. notify the timer gate in the timer queue standby processor
|
|
// 3, notify the transfer (essentially a no op, just put it here so it looks symmetric)
|
|
e.shard.SetCurrentTime(clusterName, now)
|
|
for _, processor := range e.queueProcessors {
|
|
processor.NotifyNewTasks(clusterName, []tasks.Task{})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) SyncActivity(
|
|
ctx context.Context,
|
|
request *historyservice.SyncActivityRequest,
|
|
) (retError error) {
|
|
|
|
return e.nDCActivityReplicator.SyncActivity(ctx, request)
|
|
}
|
|
|
|
// ResetWorkflowExecution terminates current workflow (if running) and replay & create new workflow
|
|
// Consistency guarantee: always write
|
|
func (e *historyEngineImpl) ResetWorkflowExecution(
|
|
ctx context.Context,
|
|
resetRequest *historyservice.ResetWorkflowExecutionRequest,
|
|
) (response *historyservice.ResetWorkflowExecutionResponse, retError error) {
|
|
|
|
request := resetRequest.ResetRequest
|
|
namespaceID := namespace.ID(resetRequest.GetNamespaceId())
|
|
workflowID := request.WorkflowExecution.GetWorkflowId()
|
|
baseRunID := request.WorkflowExecution.GetRunId()
|
|
|
|
baseWFContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
baseRunID,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { baseWFContext.GetReleaseFn()(retError) }()
|
|
|
|
baseMutableState := baseWFContext.GetMutableState()
|
|
if request.GetWorkflowTaskFinishEventId() <= common.FirstEventID ||
|
|
request.GetWorkflowTaskFinishEventId() >= baseMutableState.GetNextEventID() {
|
|
return nil, serviceerror.NewInvalidArgument("Workflow task finish ID must be > 1 && <= workflow last event ID.")
|
|
}
|
|
|
|
// also load the current run of the workflow, it can be different from the base runID
|
|
currentRunID, err := e.workflowConsistencyChecker.GetCurrentRunID(
|
|
ctx,
|
|
namespaceID.String(),
|
|
request.WorkflowExecution.GetWorkflowId(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if baseRunID == "" {
|
|
baseRunID = currentRunID
|
|
}
|
|
|
|
var currentWFContext api.WorkflowContext
|
|
if currentRunID == baseRunID {
|
|
currentWFContext = baseWFContext
|
|
} else {
|
|
currentWFContext, err = e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
currentRunID,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { currentWFContext.GetReleaseFn()(retError) }()
|
|
}
|
|
|
|
// dedup by requestID
|
|
if currentWFContext.GetMutableState().GetExecutionState().CreateRequestId == request.GetRequestId() {
|
|
e.logger.Info("Duplicated reset request",
|
|
tag.WorkflowID(workflowID),
|
|
tag.WorkflowRunID(currentRunID),
|
|
tag.WorkflowNamespaceID(namespaceID.String()))
|
|
return &historyservice.ResetWorkflowExecutionResponse{
|
|
RunId: currentRunID,
|
|
}, nil
|
|
}
|
|
|
|
resetRunID := uuid.New()
|
|
baseRebuildLastEventID := request.GetWorkflowTaskFinishEventId() - 1
|
|
baseVersionHistories := baseMutableState.GetExecutionInfo().GetVersionHistories()
|
|
baseCurrentVersionHistory, err := versionhistory.GetCurrentVersionHistory(baseVersionHistories)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseRebuildLastEventVersion, err := versionhistory.GetVersionHistoryEventVersion(baseCurrentVersionHistory, baseRebuildLastEventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseCurrentBranchToken := baseCurrentVersionHistory.GetBranchToken()
|
|
baseNextEventID := baseMutableState.GetNextEventID()
|
|
|
|
if err := e.workflowResetter.resetWorkflow(
|
|
ctx,
|
|
namespaceID,
|
|
workflowID,
|
|
baseRunID,
|
|
baseCurrentBranchToken,
|
|
baseRebuildLastEventID,
|
|
baseRebuildLastEventVersion,
|
|
baseNextEventID,
|
|
resetRunID,
|
|
request.GetRequestId(),
|
|
newNDCWorkflow(
|
|
ctx,
|
|
e.shard.GetNamespaceRegistry(),
|
|
e.shard.GetClusterMetadata(),
|
|
currentWFContext.GetContext(),
|
|
currentWFContext.GetMutableState(),
|
|
currentWFContext.GetReleaseFn(),
|
|
),
|
|
request.GetReason(),
|
|
nil,
|
|
request.GetResetReapplyType(),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.ResetWorkflowExecutionResponse{
|
|
RunId: resetRunID,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) updateWorkflow(
|
|
ctx context.Context,
|
|
reqClock *clockspb.VectorClock,
|
|
consistencyCheckFn api.MutableStateConsistencyPredicate,
|
|
workflowKey definition.WorkflowKey,
|
|
action api.UpdateWorkflowActionFunc,
|
|
) (retError error) {
|
|
workflowContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
reqClock,
|
|
consistencyCheckFn,
|
|
workflowKey,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { workflowContext.GetReleaseFn()(retError) }()
|
|
|
|
return api.UpdateWorkflowWithNew(e.shard, ctx, workflowContext, action, nil)
|
|
}
|
|
|
|
func (e *historyEngineImpl) updateWorkflowWithNew(
|
|
ctx context.Context,
|
|
reqClock *clockspb.VectorClock,
|
|
consistencyCheckFn api.MutableStateConsistencyPredicate,
|
|
workflowKey definition.WorkflowKey,
|
|
action api.UpdateWorkflowActionFunc,
|
|
newWorkflowFn func() (workflow.Context, workflow.MutableState, error),
|
|
) (retError error) {
|
|
workflowContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
reqClock,
|
|
consistencyCheckFn,
|
|
workflowKey,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { workflowContext.GetReleaseFn()(retError) }()
|
|
|
|
return api.UpdateWorkflowWithNew(e.shard, ctx, workflowContext, action, newWorkflowFn)
|
|
}
|
|
|
|
func (e *historyEngineImpl) failWorkflowTask(
|
|
ctx context.Context,
|
|
wfContext workflow.Context,
|
|
scheduledEventID int64,
|
|
startedEventID int64,
|
|
wtFailedCause *workflowTaskFailedCause,
|
|
request *workflowservice.RespondWorkflowTaskCompletedRequest,
|
|
) (workflow.MutableState, 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.LoadWorkflowExecution(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err = mutableState.AddWorkflowTaskFailedEvent(
|
|
scheduledEventID,
|
|
startedEventID,
|
|
wtFailedCause.failedCause,
|
|
failure.NewServerFailure(wtFailedCause.Message(), true),
|
|
request.GetIdentity(),
|
|
request.GetBinaryChecksum(),
|
|
"",
|
|
"",
|
|
0); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return new builder back to the caller for further updates
|
|
return mutableState, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) NotifyNewHistoryEvent(
|
|
notification *events.Notification,
|
|
) {
|
|
|
|
e.eventNotifier.NotifyNewHistoryEvent(notification)
|
|
}
|
|
|
|
func (e *historyEngineImpl) NotifyNewTasks(
|
|
clusterName string,
|
|
newTasks map[tasks.Category][]tasks.Task,
|
|
) {
|
|
for category, tasksByCategory := range newTasks {
|
|
// TODO: make replicatorProcessor part of queueProcessors list
|
|
// and get rid of the special case here.
|
|
if category == tasks.CategoryReplication {
|
|
if e.replicationAckMgr != nil {
|
|
e.replicationAckMgr.NotifyNewTasks(tasksByCategory)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if len(tasksByCategory) > 0 {
|
|
e.queueProcessors[category].NotifyNewTasks(clusterName, tasksByCategory)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *historyEngineImpl) validateStartWorkflowExecutionRequest(
|
|
ctx context.Context,
|
|
request *workflowservice.StartWorkflowExecutionRequest,
|
|
namespaceEntry *namespace.Namespace,
|
|
operation string,
|
|
) error {
|
|
|
|
workflowID := request.GetWorkflowId()
|
|
maxIDLengthLimit := e.config.MaxIDLengthLimit()
|
|
|
|
if len(request.GetRequestId()) == 0 {
|
|
return serviceerror.NewInvalidArgument("Missing request ID.")
|
|
}
|
|
if timestamp.DurationValue(request.GetWorkflowExecutionTimeout()) < 0 {
|
|
return serviceerror.NewInvalidArgument("Invalid WorkflowExecutionTimeoutSeconds.")
|
|
}
|
|
if timestamp.DurationValue(request.GetWorkflowRunTimeout()) < 0 {
|
|
return serviceerror.NewInvalidArgument("Invalid WorkflowRunTimeoutSeconds.")
|
|
}
|
|
if timestamp.DurationValue(request.GetWorkflowTaskTimeout()) < 0 {
|
|
return serviceerror.NewInvalidArgument("Invalid WorkflowTaskTimeoutSeconds.")
|
|
}
|
|
if request.TaskQueue == nil || request.TaskQueue.GetName() == "" {
|
|
return serviceerror.NewInvalidArgument("Missing Taskqueue.")
|
|
}
|
|
if request.WorkflowType == nil || request.WorkflowType.GetName() == "" {
|
|
return serviceerror.NewInvalidArgument("Missing WorkflowType.")
|
|
}
|
|
if len(request.GetNamespace()) > maxIDLengthLimit {
|
|
return serviceerror.NewInvalidArgument("Namespace exceeds length limit.")
|
|
}
|
|
if len(request.GetWorkflowId()) > maxIDLengthLimit {
|
|
return serviceerror.NewInvalidArgument("WorkflowId exceeds length limit.")
|
|
}
|
|
if len(request.TaskQueue.GetName()) > maxIDLengthLimit {
|
|
return serviceerror.NewInvalidArgument("TaskQueue exceeds length limit.")
|
|
}
|
|
if len(request.WorkflowType.GetName()) > maxIDLengthLimit {
|
|
return serviceerror.NewInvalidArgument("WorkflowType exceeds length limit.")
|
|
}
|
|
if err := common.ValidateRetryPolicy(request.RetryPolicy); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := api.ValidateStart(
|
|
ctx,
|
|
e.shard,
|
|
namespaceEntry,
|
|
workflowID,
|
|
request.GetInput().Size(),
|
|
request.GetMemo().Size(),
|
|
operation,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) overrideStartWorkflowExecutionRequest(
|
|
request *workflowservice.StartWorkflowExecutionRequest,
|
|
metricsScope int,
|
|
) {
|
|
// workflow execution timeout is left as is
|
|
// if workflow execution timeout == 0 -> infinity
|
|
|
|
namespace := request.GetNamespace()
|
|
|
|
workflowRunTimeout := common.OverrideWorkflowRunTimeout(
|
|
timestamp.DurationValue(request.GetWorkflowRunTimeout()),
|
|
timestamp.DurationValue(request.GetWorkflowExecutionTimeout()),
|
|
)
|
|
if workflowRunTimeout != timestamp.DurationValue(request.GetWorkflowRunTimeout()) {
|
|
request.WorkflowRunTimeout = timestamp.DurationPtr(workflowRunTimeout)
|
|
e.metricsClient.Scope(
|
|
metricsScope,
|
|
metrics.NamespaceTag(namespace),
|
|
).IncCounter(metrics.WorkflowRunTimeoutOverrideCount)
|
|
}
|
|
|
|
workflowTaskStartToCloseTimeout := common.OverrideWorkflowTaskTimeout(
|
|
namespace,
|
|
timestamp.DurationValue(request.GetWorkflowTaskTimeout()),
|
|
timestamp.DurationValue(request.GetWorkflowRunTimeout()),
|
|
e.config.DefaultWorkflowTaskTimeout,
|
|
)
|
|
if workflowTaskStartToCloseTimeout != timestamp.DurationValue(request.GetWorkflowTaskTimeout()) {
|
|
request.WorkflowTaskTimeout = timestamp.DurationPtr(workflowTaskStartToCloseTimeout)
|
|
e.metricsClient.Scope(
|
|
metricsScope,
|
|
metrics.NamespaceTag(namespace),
|
|
).IncCounter(metrics.WorkflowTaskTimeoutOverrideCount)
|
|
}
|
|
}
|
|
|
|
func validateNamespaceUUID(
|
|
namespaceUUID namespace.ID,
|
|
) error {
|
|
|
|
if namespaceUUID == "" {
|
|
return serviceerror.NewInvalidArgument("Missing namespace UUID.")
|
|
} else if uuid.Parse(namespaceUUID.String()) == nil {
|
|
return serviceerror.NewInvalidArgument("Invalid namespace UUID.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) getActiveNamespaceEntry(
|
|
namespaceUUID namespace.ID,
|
|
) (*namespace.Namespace, error) {
|
|
|
|
return getActiveNamespaceEntryFromShard(e.shard, namespaceUUID)
|
|
}
|
|
|
|
func getActiveNamespaceEntryFromShard(
|
|
shard shard.Context,
|
|
namespaceUUID namespace.ID,
|
|
) (*namespace.Namespace, error) {
|
|
|
|
err := validateNamespaceUUID(namespaceUUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
namespaceEntry, err := shard.GetNamespaceRegistry().GetNamespaceByID(namespaceUUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !namespaceEntry.ActiveInCluster(shard.GetClusterMetadata().GetCurrentClusterName()) {
|
|
return nil, serviceerror.NewNamespaceNotActive(
|
|
namespaceEntry.Name().String(),
|
|
shard.GetClusterMetadata().GetCurrentClusterName(),
|
|
namespaceEntry.ActiveClusterName())
|
|
}
|
|
return namespaceEntry, nil
|
|
}
|
|
|
|
func getscheduledEventID(
|
|
activityID string,
|
|
mutableState workflow.MutableState,
|
|
) (int64, error) {
|
|
|
|
if activityID == "" {
|
|
return 0, serviceerror.NewInvalidArgument("activityID cannot be empty")
|
|
}
|
|
activityInfo, ok := mutableState.GetActivityByActivityID(activityID)
|
|
if !ok {
|
|
return 0, serviceerror.NewNotFound(fmt.Sprintf("cannot find pending activity with ActivityID %s, check workflow execution history for more details", activityID))
|
|
}
|
|
return activityInfo.ScheduledEventId, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) getStartRequest(
|
|
namespaceID namespace.ID,
|
|
request *workflowservice.SignalWithStartWorkflowExecutionRequest,
|
|
) *historyservice.StartWorkflowExecutionRequest {
|
|
|
|
req := &workflowservice.StartWorkflowExecutionRequest{
|
|
Namespace: request.GetNamespace(),
|
|
WorkflowId: request.GetWorkflowId(),
|
|
WorkflowType: request.GetWorkflowType(),
|
|
TaskQueue: request.GetTaskQueue(),
|
|
Input: request.GetInput(),
|
|
WorkflowExecutionTimeout: request.GetWorkflowExecutionTimeout(),
|
|
WorkflowRunTimeout: request.GetWorkflowRunTimeout(),
|
|
WorkflowTaskTimeout: request.GetWorkflowTaskTimeout(),
|
|
Identity: request.GetIdentity(),
|
|
RequestId: request.GetRequestId(),
|
|
WorkflowIdReusePolicy: request.GetWorkflowIdReusePolicy(),
|
|
RetryPolicy: request.GetRetryPolicy(),
|
|
CronSchedule: request.GetCronSchedule(),
|
|
Memo: request.GetMemo(),
|
|
SearchAttributes: request.GetSearchAttributes(),
|
|
Header: request.GetHeader(),
|
|
}
|
|
|
|
return common.CreateHistoryStartWorkflowRequest(namespaceID.String(), req, nil, e.shard.GetTimeSource().Now())
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetReplicationMessages(
|
|
ctx context.Context,
|
|
pollingCluster string,
|
|
ackMessageID int64,
|
|
ackTimestamp time.Time,
|
|
queryMessageID int64,
|
|
) (*replicationspb.ReplicationMessages, error) {
|
|
|
|
if ackMessageID != persistence.EmptyQueueMessageID {
|
|
if err := e.shard.UpdateQueueClusterAckLevel(
|
|
tasks.CategoryReplication,
|
|
pollingCluster,
|
|
tasks.NewImmediateKey(ackMessageID),
|
|
); err != nil {
|
|
e.logger.Error("error updating replication level for shard", tag.Error(err), tag.OperationFailed)
|
|
}
|
|
e.shard.UpdateRemoteClusterInfo(pollingCluster, ackMessageID, ackTimestamp)
|
|
}
|
|
|
|
replicationMessages, err := e.replicationAckMgr.GetTasks(
|
|
ctx,
|
|
pollingCluster,
|
|
queryMessageID,
|
|
)
|
|
if err != nil {
|
|
e.logger.Error("Failed to retrieve replication messages.", tag.Error(err))
|
|
return nil, err
|
|
}
|
|
e.logger.Debug("Successfully fetched replication messages.", tag.Counter(len(replicationMessages.ReplicationTasks)))
|
|
|
|
return replicationMessages, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetDLQReplicationMessages(
|
|
ctx context.Context,
|
|
taskInfos []*replicationspb.ReplicationTaskInfo,
|
|
) ([]*replicationspb.ReplicationTask, error) {
|
|
|
|
tasks := make([]*replicationspb.ReplicationTask, 0, len(taskInfos))
|
|
for _, taskInfo := range taskInfos {
|
|
task, err := e.replicationAckMgr.GetTask(ctx, taskInfo)
|
|
if err != nil {
|
|
e.logger.Error("Failed to fetch DLQ replication messages.", tag.Error(err))
|
|
return nil, err
|
|
}
|
|
tasks = append(tasks, task)
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) ReapplyEvents(
|
|
ctx context.Context,
|
|
namespaceUUID namespace.ID,
|
|
workflowID string,
|
|
runID string,
|
|
reapplyEvents []*historypb.HistoryEvent,
|
|
) error {
|
|
|
|
if e.config.SkipReapplicationByNamespaceID(namespaceUUID.String()) {
|
|
return nil
|
|
}
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespaceUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
return e.updateWorkflow(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
"",
|
|
),
|
|
func(workflowContext api.WorkflowContext) (action *api.UpdateWorkflowAction, retErr error) {
|
|
context := workflowContext.GetContext()
|
|
mutableState := workflowContext.GetMutableState()
|
|
// Filter out reapply event from the same cluster
|
|
toReapplyEvents := make([]*historypb.HistoryEvent, 0, len(reapplyEvents))
|
|
lastWriteVersion, err := mutableState.GetLastWriteVersion()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceMutableState := mutableState
|
|
if sourceMutableState.GetWorkflowKey().RunID != runID {
|
|
originCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(namespaceID.String(), workflowID, runID),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { originCtx.GetReleaseFn()(retErr) }()
|
|
sourceMutableState = originCtx.GetMutableState()
|
|
}
|
|
|
|
for _, event := range reapplyEvents {
|
|
if event.GetVersion() == lastWriteVersion {
|
|
// The reapply is from the same cluster. Ignoring.
|
|
continue
|
|
}
|
|
dedupResource := definition.NewEventReappliedID(runID, event.GetEventId(), event.GetVersion())
|
|
if mutableState.IsResourceDuplicated(dedupResource) {
|
|
// already apply the signal
|
|
continue
|
|
}
|
|
versionHistories := sourceMutableState.GetExecutionInfo().GetVersionHistories()
|
|
if e.containsHistoryEvent(versionHistories, event.GetEventId(), event.GetVersion()) {
|
|
continue
|
|
}
|
|
|
|
toReapplyEvents = append(toReapplyEvents, event)
|
|
}
|
|
if len(toReapplyEvents) == 0 {
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
// need to reset target workflow (which is also the current workflow)
|
|
// to accept events to be reapplied
|
|
baseRunID := mutableState.GetExecutionState().GetRunId()
|
|
resetRunID := uuid.New()
|
|
baseRebuildLastEventID := mutableState.GetPreviousStartedEventID()
|
|
|
|
// TODO when https://github.com/uber/cadence/issues/2420 is finished, remove this block,
|
|
// since cannot reapply event to a finished workflow which had no workflow tasks started
|
|
if baseRebuildLastEventID == common.EmptyEventID {
|
|
e.logger.Warn("cannot reapply event to a finished workflow with no workflow task",
|
|
tag.WorkflowNamespaceID(namespaceID.String()),
|
|
tag.WorkflowID(workflowID),
|
|
)
|
|
e.metricsClient.IncCounter(metrics.HistoryReapplyEventsScope, metrics.EventReapplySkippedCount)
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
baseVersionHistories := mutableState.GetExecutionInfo().GetVersionHistories()
|
|
baseCurrentVersionHistory, err := versionhistory.GetCurrentVersionHistory(baseVersionHistories)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseRebuildLastEventVersion, err := versionhistory.GetVersionHistoryEventVersion(baseCurrentVersionHistory, baseRebuildLastEventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseCurrentBranchToken := baseCurrentVersionHistory.GetBranchToken()
|
|
baseNextEventID := mutableState.GetNextEventID()
|
|
|
|
err = e.workflowResetter.resetWorkflow(
|
|
ctx,
|
|
namespaceID,
|
|
workflowID,
|
|
baseRunID,
|
|
baseCurrentBranchToken,
|
|
baseRebuildLastEventID,
|
|
baseRebuildLastEventVersion,
|
|
baseNextEventID,
|
|
resetRunID,
|
|
uuid.New(),
|
|
newNDCWorkflow(
|
|
ctx,
|
|
e.shard.GetNamespaceRegistry(),
|
|
e.shard.GetClusterMetadata(),
|
|
context,
|
|
mutableState,
|
|
workflow.NoopReleaseFn,
|
|
),
|
|
eventsReapplicationResetWorkflowReason,
|
|
toReapplyEvents,
|
|
enumspb.RESET_REAPPLY_TYPE_SIGNAL,
|
|
)
|
|
switch err.(type) {
|
|
case *serviceerror.InvalidArgument:
|
|
// no-op. Usually this is due to reset workflow with pending child workflows
|
|
e.logger.Warn("Cannot reset workflow. Ignoring reapply events.", tag.Error(err))
|
|
case nil:
|
|
// no-op
|
|
default:
|
|
return nil, err
|
|
}
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
postActions := &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: true,
|
|
}
|
|
if mutableState.IsWorkflowPendingOnWorkflowTaskBackoff() {
|
|
// Do not create workflow task when the workflow has first workflow task backoff and execution is not started yet
|
|
postActions.CreateWorkflowTask = false
|
|
}
|
|
reappliedEvents, err := e.eventsReapplier.reapplyEvents(
|
|
ctx,
|
|
mutableState,
|
|
toReapplyEvents,
|
|
runID,
|
|
)
|
|
if err != nil {
|
|
e.logger.Error("failed to re-apply stale events", tag.Error(err))
|
|
return nil, err
|
|
}
|
|
if len(reappliedEvents) == 0 {
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
return postActions, nil
|
|
})
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetDLQMessages(
|
|
ctx context.Context,
|
|
request *historyservice.GetDLQMessagesRequest,
|
|
) (*historyservice.GetDLQMessagesResponse, error) {
|
|
|
|
_, ok := e.clusterMetadata.GetAllClusterInfo()[request.GetSourceCluster()]
|
|
if !ok {
|
|
return nil, consts.ErrUnknownCluster
|
|
}
|
|
|
|
tasks, token, err := e.replicationDLQHandler.GetMessages(
|
|
ctx,
|
|
request.GetSourceCluster(),
|
|
request.GetInclusiveEndMessageId(),
|
|
int(request.GetMaximumPageSize()),
|
|
request.GetNextPageToken(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.GetDLQMessagesResponse{
|
|
Type: request.GetType(),
|
|
ReplicationTasks: tasks,
|
|
NextPageToken: token,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) PurgeDLQMessages(
|
|
ctx context.Context,
|
|
request *historyservice.PurgeDLQMessagesRequest,
|
|
) error {
|
|
|
|
_, ok := e.clusterMetadata.GetAllClusterInfo()[request.GetSourceCluster()]
|
|
if !ok {
|
|
return consts.ErrUnknownCluster
|
|
}
|
|
|
|
return e.replicationDLQHandler.PurgeMessages(
|
|
ctx,
|
|
request.GetSourceCluster(),
|
|
request.GetInclusiveEndMessageId(),
|
|
)
|
|
}
|
|
|
|
func (e *historyEngineImpl) MergeDLQMessages(
|
|
ctx context.Context,
|
|
request *historyservice.MergeDLQMessagesRequest,
|
|
) (*historyservice.MergeDLQMessagesResponse, error) {
|
|
|
|
_, ok := e.clusterMetadata.GetAllClusterInfo()[request.GetSourceCluster()]
|
|
if !ok {
|
|
return nil, consts.ErrUnknownCluster
|
|
}
|
|
|
|
token, err := e.replicationDLQHandler.MergeMessages(
|
|
ctx,
|
|
request.GetSourceCluster(),
|
|
request.GetInclusiveEndMessageId(),
|
|
int(request.GetMaximumPageSize()),
|
|
request.GetNextPageToken(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.MergeDLQMessagesResponse{
|
|
NextPageToken: token,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) RebuildMutableState(
|
|
ctx context.Context,
|
|
namespaceUUID namespace.ID,
|
|
execution commonpb.WorkflowExecution,
|
|
) error {
|
|
return e.workflowRebuilder.rebuild(
|
|
ctx,
|
|
definition.NewWorkflowKey(
|
|
namespaceUUID.String(),
|
|
execution.GetWorkflowId(),
|
|
execution.GetRunId(),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (e *historyEngineImpl) RefreshWorkflowTasks(
|
|
ctx context.Context,
|
|
namespaceUUID namespace.ID,
|
|
execution commonpb.WorkflowExecution,
|
|
) (retError error) {
|
|
|
|
err := validateNamespaceUUID(namespaceUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wfContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceUUID.String(),
|
|
execution.WorkflowId,
|
|
execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { wfContext.GetReleaseFn()(retError) }()
|
|
|
|
mutableState := wfContext.GetMutableState()
|
|
mutableStateTaskRefresher := workflow.NewTaskRefresher(
|
|
e.shard,
|
|
e.shard.GetConfig(),
|
|
e.shard.GetNamespaceRegistry(),
|
|
e.shard.GetEventsCache(),
|
|
e.shard.GetLogger(),
|
|
)
|
|
|
|
now := e.shard.GetTimeSource().Now()
|
|
|
|
err = mutableStateTaskRefresher.RefreshTasks(ctx, now, mutableState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.shard.AddTasks(ctx, &persistence.AddHistoryTasksRequest{
|
|
ShardID: e.shard.GetShardID(),
|
|
// RangeID is set by shard
|
|
NamespaceID: namespaceUUID.String(),
|
|
WorkflowID: execution.WorkflowId,
|
|
RunID: execution.RunId,
|
|
Tasks: mutableState.PopTasks(),
|
|
})
|
|
}
|
|
|
|
func (e *historyEngineImpl) GenerateLastHistoryReplicationTasks(
|
|
ctx context.Context,
|
|
request *historyservice.GenerateLastHistoryReplicationTasksRequest,
|
|
) (_ *historyservice.GenerateLastHistoryReplicationTasksResponse, retError error) {
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(request.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
wfContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
request.Execution.WorkflowId,
|
|
request.Execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { wfContext.GetReleaseFn()(retError) }()
|
|
|
|
now := e.shard.GetTimeSource().Now()
|
|
task, err := wfContext.GetMutableState().GenerateMigrationTasks(now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = e.shard.AddTasks(ctx, &persistence.AddHistoryTasksRequest{
|
|
ShardID: e.shard.GetShardID(),
|
|
// RangeID is set by shard
|
|
NamespaceID: string(namespaceID),
|
|
WorkflowID: request.Execution.WorkflowId,
|
|
RunID: request.Execution.RunId,
|
|
Tasks: map[tasks.Category][]tasks.Task{
|
|
tasks.CategoryReplication: {task},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.GenerateLastHistoryReplicationTasksResponse{}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetReplicationStatus(
|
|
ctx context.Context,
|
|
request *historyservice.GetReplicationStatusRequest,
|
|
) (_ *historyservice.ShardReplicationStatus, retError error) {
|
|
|
|
resp := &historyservice.ShardReplicationStatus{
|
|
ShardId: e.shard.GetShardID(),
|
|
ShardLocalTime: timestamp.TimePtr(e.shard.GetTimeSource().Now()),
|
|
}
|
|
|
|
maxReplicationTaskId, maxTaskVisibilityTimeStamp := e.replicationAckMgr.GetMaxTaskInfo()
|
|
resp.MaxReplicationTaskId = maxReplicationTaskId
|
|
resp.MaxReplicationTaskVisibilityTime = timestamp.TimePtr(maxTaskVisibilityTimeStamp)
|
|
|
|
remoteClusters, handoverNamespaces, err := e.shard.GetReplicationStatus(request.RemoteClusters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.RemoteClusters = remoteClusters
|
|
resp.HandoverNamespaces = handoverNamespaces
|
|
return resp, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) containsHistoryEvent(
|
|
versionHistories *historyspb.VersionHistories,
|
|
reappliedEventID int64,
|
|
reappliedEventVersion int64,
|
|
) bool {
|
|
// Check if the source workflow contains the reapply event.
|
|
// If it does, it means the event is received in this cluster, no need to reapply.
|
|
_, err := versionhistory.FindFirstVersionHistoryIndexByVersionHistoryItem(
|
|
versionHistories,
|
|
versionhistory.NewVersionHistoryItem(reappliedEventID, reappliedEventVersion),
|
|
)
|
|
return err == nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) setActivityTaskRunID(
|
|
ctx context.Context,
|
|
token *tokenspb.Task,
|
|
) error {
|
|
// TODO when the following APIs are deprecated
|
|
// remove this function since run ID will always be set
|
|
// * RecordActivityTaskHeartbeatById
|
|
// * RespondActivityTaskCanceledById
|
|
// * RespondActivityTaskFailedById
|
|
// * RespondActivityTaskCompletedById
|
|
|
|
if len(token.RunId) != 0 {
|
|
return nil
|
|
}
|
|
|
|
runID, err := e.workflowConsistencyChecker.GetCurrentRunID(
|
|
ctx,
|
|
token.NamespaceId,
|
|
token.WorkflowId,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token.RunId = runID
|
|
return nil
|
|
}
|
|
|
|
func historyEventOnCurrentBranch(
|
|
mutableState workflow.MutableState,
|
|
eventID int64,
|
|
eventVersion int64,
|
|
) (bool, error) {
|
|
if eventVersion == 0 {
|
|
if eventID >= mutableState.GetNextEventID() {
|
|
return false, &serviceerror.NotFound{Message: "History event not found"}
|
|
}
|
|
|
|
// there's no version, assume the event is on the current branch
|
|
return true, nil
|
|
}
|
|
|
|
versionHistoryies := mutableState.GetExecutionInfo().GetVersionHistories()
|
|
versionHistoryItem := versionhistory.NewVersionHistoryItem(eventID, eventVersion)
|
|
if _, err := versionhistory.FindFirstVersionHistoryIndexByVersionHistoryItem(
|
|
versionHistoryies,
|
|
versionHistoryItem,
|
|
); err != nil {
|
|
return false, &serviceerror.NotFound{Message: "History event not found"}
|
|
}
|
|
|
|
// check if on current branch
|
|
currentVersionHistory, err := versionhistory.GetCurrentVersionHistory(versionHistoryies)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return versionhistory.ContainsVersionHistoryItem(
|
|
currentVersionHistory,
|
|
versionHistoryItem,
|
|
), nil
|
|
}
|