Files
tasks/service/history/historyEngine.go

3674 lines
121 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"
"sync/atomic"
"time"
"github.com/pborman/uuid"
commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/api/enums/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"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/persistence/serialization"
"go.temporal.io/server/common/persistence/visibility/manager"
"go.temporal.io/server/common/sdk"
"go.temporal.io/server/service/history/queues"
"go.temporal.io/server/service/history/tasks"
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"
persistencespb "go.temporal.io/server/api/persistence/v1"
replicationspb "go.temporal.io/server/api/replication/v1"
workflowspb "go.temporal.io/server/api/workflow/v1"
"go.temporal.io/server/client/admin"
"go.temporal.io/server/client/history"
"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/versionhistory"
"go.temporal.io/server/common/primitives/timestamp"
"go.temporal.io/server/common/rpc/interceptor"
"go.temporal.io/server/common/searchattribute"
serviceerrors "go.temporal.io/server/common/serviceerror"
"go.temporal.io/server/common/xdc"
"go.temporal.io/server/service/history/configs"
"go.temporal.io/server/service/history/consts"
"go.temporal.io/server/service/history/events"
"go.temporal.io/server/service/history/shard"
"go.temporal.io/server/service/history/workflow"
"go.temporal.io/server/service/worker/archiver"
)
const (
conditionalRetryCount = 5
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
replicatorProcessor *replicatorQueueProcessorImpl
nDCReplicator nDCHistoryReplicator
nDCActivityReplicator nDCActivityReplicator
eventNotifier events.Notifier
tokenSerializer common.TaskTokenSerializer
historyCache workflow.Cache
metricsClient metrics.Client
logger log.Logger
throttledLogger log.Logger
config *configs.Config
workflowRebuilder workflowRebuilder
workflowResetter workflowResetter
replicationTaskFetchers ReplicationTaskFetchers
replicationTaskProcessorsLock sync.Mutex
replicationTaskProcessors map[string]ReplicationTaskProcessor
sdkClientFactory sdk.ClientFactory
eventsReapplier nDCEventsReapplier
matchingClient matchingservice.MatchingServiceClient
rawMatchingClient matchingservice.MatchingServiceClient
replicationDLQHandler replicationDLQHandler
searchAttributesValidator *searchattribute.Validator
workflowDeleteManager workflow.DeleteManager
eventSerializer serialization.Serializer
}
)
// NewEngineWithShardContext creates an instance of history engine
func NewEngineWithShardContext(
shard shard.Context,
visibilityMgr manager.VisibilityManager,
matchingClient matchingservice.MatchingServiceClient,
historyClient historyservice.HistoryServiceClient,
sdkClientFactory sdk.ClientFactory,
eventNotifier events.Notifier,
config *configs.Config,
replicationTaskFetchers ReplicationTaskFetchers,
rawMatchingClient matchingservice.MatchingServiceClient,
newCacheFn workflow.NewCacheFn,
archivalClient archiver.Client,
eventSerializer serialization.Serializer,
queueProcessorFactories []queues.ProcessorFactory,
) 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(),
historyCache: historyCache,
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,
replicationTaskProcessors: make(map[string]ReplicationTaskProcessor),
replicationTaskFetchers: replicationTaskFetchers,
workflowDeleteManager: workflowDeleteManager,
eventSerializer: eventSerializer,
}
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.replicatorProcessor = newReplicatorQueueProcessor(
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 = newLazyReplicationDLQHandler(shard, workflowDeleteManager, historyCache)
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()
}
// 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.
e.registerNamespaceFailoverCallback()
// Listen to cluster metadata and dynamically update replication processor for remote clussters.
e.listenToClusterMetadataChange()
}
// 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.clusterMetadata.UnRegisterMetadataChangeCallback(e)
e.replicationTaskProcessorsLock.Lock()
for _, replicationTaskProcessor := range e.replicationTaskProcessors {
replicationTaskProcessor.Stop()
}
e.replicationTaskProcessorsLock.Unlock()
// 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()
if nextNamespace.IsGlobalNamespace() &&
namespaceFailoverNotificationVersion >= 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() {
e.shard.UpdateHandoverNamespaces(nextNamespaces, e.replicatorProcessor.GetMaxReplicationTaskID())
}
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
// failover notification version <= NotificationVersion,
// there's no notification version that can make
// newNotificationVersion < shardNotificationVersion and
// failoverNotificationVersion >= 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(category, now)}
}
e.NotifyNewTasks(e.currentClusterName, fakeTasks)
}
// nolint:errcheck
e.shard.UpdateNamespaceNotificationVersion(newNotificationVersion)
},
)
}
func (e *historyEngineImpl) listenToClusterMetadataChange() {
e.clusterMetadata.RegisterMetadataChangeCallback(
e,
e.handleClusterMetadataUpdate,
)
}
func (e *historyEngineImpl) handleClusterMetadataUpdate(
oldClusterMetadata map[string]*cluster.ClusterInformation,
newClusterMetadata map[string]*cluster.ClusterInformation,
) {
e.replicationTaskProcessorsLock.Lock()
defer e.replicationTaskProcessorsLock.Unlock()
for clusterName := range oldClusterMetadata {
if clusterName == e.currentClusterName {
continue
}
// The metadata triggers a update when the following fields update: 1. Enabled 2. Initial Failover Version 3. Cluster address
// The callback covers three cases:
// Case 1: Remove a cluster Case 2: Add a new cluster Case 3: Refresh cluster metadata.
if processor, ok := e.replicationTaskProcessors[clusterName]; ok {
// Case 1 and Case 3
processor.Stop()
delete(e.replicationTaskProcessors, clusterName)
}
if clusterInfo := newClusterMetadata[clusterName]; clusterInfo != nil && clusterInfo.Enabled {
// Case 2 and Case 3
fetcher := e.replicationTaskFetchers.GetOrCreateFetcher(clusterName)
adminClient := e.shard.GetRemoteAdminClient(clusterName)
adminRetryableClient := admin.NewRetryableClient(
adminClient,
common.CreateReplicationServiceBusyRetryPolicy(),
common.IsResourceExhausted,
)
// Intentionally use the raw client to create its own retry policy
historyClient := e.shard.GetHistoryClient()
historyRetryableClient := history.NewRetryableClient(
historyClient,
common.CreateReplicationServiceBusyRetryPolicy(),
common.IsResourceExhausted,
)
nDCHistoryResender := xdc.NewNDCHistoryResender(
e.shard.GetNamespaceRegistry(),
adminRetryableClient,
func(ctx context.Context, request *historyservice.ReplicateEventsV2Request) error {
_, err := historyRetryableClient.ReplicateEventsV2(ctx, request)
return err
},
e.shard.GetPayloadSerializer(),
e.shard.GetConfig().StandbyTaskReReplicationContextTimeout,
e.shard.GetLogger(),
)
replicationTaskExecutor := newReplicationTaskExecutor(
e.shard,
e.shard.GetNamespaceRegistry(),
nDCHistoryResender,
e,
e.workflowDeleteManager,
e.historyCache,
e.shard.GetMetricsClient(),
e.shard.GetLogger(),
)
replicationTaskProcessor := NewReplicationTaskProcessor(
e.shard,
e,
e.config,
e.shard.GetMetricsClient(),
fetcher,
replicationTaskExecutor,
e.eventSerializer,
)
replicationTaskProcessor.Start()
e.replicationTaskProcessors[clusterName] = replicationTaskProcessor
}
}
}
func createMutableState(
shard shard.Context,
namespaceEntry *namespace.Namespace,
runID string,
) (workflow.MutableState, error) {
var newMutableState workflow.MutableState
// version history applies to both local and global namespace
newMutableState = workflow.NewMutableState(
shard,
shard.GetEventsCache(),
shard.GetLogger(),
namespaceEntry,
shard.GetTimeSource().Now(),
)
if err := newMutableState.SetHistoryTree(runID); err != nil {
return nil, err
}
return newMutableState, nil
}
func (e *historyEngineImpl) generateFirstWorkflowTask(
mutableState workflow.MutableState,
parentInfo *workflowspb.ParentExecutionInfo,
startEvent *historypb.HistoryEvent,
) error {
if parentInfo == nil {
// WorkflowTask is only created when it is not a Child Workflow and no backoff is needed
if err := mutableState.AddFirstWorkflowTaskScheduled(
startEvent,
); err != nil {
return err
}
}
return nil
}
// StartWorkflowExecution starts a workflow execution
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
}
namespace := namespaceEntry.Name()
namespaceID := namespaceEntry.ID()
request := startRequest.StartRequest
e.overrideStartWorkflowExecutionRequest(request, metrics.HistoryStartWorkflowExecutionScope)
err = e.validateStartWorkflowExecutionRequest(ctx, request, namespace, "StartWorkflowExecution")
if err != nil {
return nil, err
}
workflowID := request.GetWorkflowId()
// grab the current context as a Lock, nothing more
_, currentRelease, err := e.historyCache.GetOrCreateCurrentWorkflowExecution(
ctx,
namespaceID,
workflowID,
)
if err != nil {
return nil, err
}
defer func() { currentRelease(retError) }()
execution := commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: uuid.New(),
}
weContext, mutableState, err := e.newWorkflowWithSignal(namespaceEntry, execution, startRequest, nil)
if err != nil {
return nil, err
}
now := e.timeSource.Now()
newWorkflow, newWorkflowEventsSeq, err := mutableState.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 = weContext.CreateWorkflowExecution(
ctx,
now,
createMode,
prevRunID,
prevLastWriteVersion,
mutableState,
newWorkflow,
newWorkflowEventsSeq,
)
if err == nil {
return &historyservice.StartWorkflowExecutionResponse{
RunId: execution.GetRunId(),
}, 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 mutableState.GetCurrentVersion() < prevLastWriteVersion {
clusterMetadata := e.shard.GetClusterMetadata()
return nil, serviceerror.NewNamespaceNotActive(
request.GetNamespace(),
clusterMetadata.GetCurrentClusterName(),
clusterMetadata.ClusterNameForFailoverVersion(namespaceEntry.IsGlobalNamespace(), prevLastWriteVersion),
)
}
prevExecutionUpdateAction, err := e.applyWorkflowIDReusePolicyHelper(
t.RequestID,
prevRunID,
t.State,
t.Status,
execution,
startRequest.StartRequest.GetWorkflowIdReusePolicy(),
)
if err != nil {
return nil, err
}
if prevExecutionUpdateAction != nil {
// update prev execution and create new execution in one transaction
err := e.updateWorkflowExecutionWithNew(
ctx,
namespaceID,
commonpb.WorkflowExecution{
WorkflowId: execution.WorkflowId,
RunId: prevRunID,
},
prevExecutionUpdateAction,
func() (workflow.Context, workflow.MutableState, error) {
return e.newWorkflowWithSignal(namespaceEntry, execution, startRequest, nil)
},
)
switch err {
case nil:
return &historyservice.StartWorkflowExecutionResponse{
RunId: execution.GetRunId(),
}, 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 = weContext.CreateWorkflowExecution(
ctx,
now,
persistence.CreateWorkflowModeWorkflowIDReuse,
prevRunID,
prevLastWriteVersion,
mutableState,
newWorkflow,
newWorkflowEventsSeq,
); err != nil {
return nil, err
}
return &historyservice.StartWorkflowExecutionResponse{
RunId: execution.GetRunId(),
}, nil
}
func (e *historyEngineImpl) newWorkflowWithSignal(
namespaceEntry *namespace.Namespace,
execution commonpb.WorkflowExecution,
startRequest *historyservice.StartWorkflowExecutionRequest,
signalWithStartRequest *workflowservice.SignalWithStartWorkflowExecutionRequest,
) (workflow.Context, workflow.MutableState, error) {
newMutableState, err := createMutableState(e.shard, namespaceEntry, execution.GetRunId())
if err != nil {
return nil, nil, err
}
startEvent, err := newMutableState.AddWorkflowExecutionStartedEvent(
execution,
startRequest,
)
if err != nil {
return nil, nil, err
}
if signalWithStartRequest != nil {
if signalWithStartRequest.GetRequestId() != "" {
newMutableState.AddSignalRequested(signalWithStartRequest.GetRequestId())
}
if _, err := newMutableState.AddWorkflowExecutionSignaled(
signalWithStartRequest.GetSignalName(),
signalWithStartRequest.GetSignalInput(),
signalWithStartRequest.GetIdentity(),
signalWithStartRequest.GetHeader(),
); err != nil {
return nil, nil, err
}
}
// Generate first workflow task event if not child WF and no first workflow task backoff
if err := e.generateFirstWorkflowTask(
newMutableState,
startRequest.ParentExecutionInfo,
startEvent,
); err != nil {
return nil, nil, err
}
newWorkflowContext := workflow.NewContext(
e.shard,
definition.NewWorkflowKey(
namespaceEntry.ID().String(),
execution.GetWorkflowId(),
execution.GetRunId(),
),
e.logger,
)
return newWorkflowContext, newMutableState, 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,
}, 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
}
execution := commonpb.WorkflowExecution{
WorkflowId: request.Execution.WorkflowId,
RunId: request.Execution.RunId,
}
response, err := e.getMutableState(ctx, namespaceID, execution)
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)
}
// set the run id in case query the current running workflow
execution.RunId = response.Execution.RunId
// 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(definition.NewWorkflowKey(namespaceID.String(), execution.GetWorkflowId(), execution.GetRunId()))
if err != nil {
return nil, err
}
defer e.eventNotifier.UnwatchHistoryEvent(definition.NewWorkflowKey(namespaceID.String(), execution.GetWorkflowId(), execution.GetRunId()), subscriberID) // nolint:errcheck
// check again in case the next event ID is updated
response, err = e.getMutableState(ctx, namespaceID, execution)
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())
mutableStateResp, err := e.getMutableState(ctx, namespaceID, *request.GetRequest().GetExecution())
if err != nil {
return nil, err
}
req := request.GetRequest()
if mutableStateResp.GetWorkflowStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING && req.QueryRejectCondition != enumspb.QUERY_REJECT_CONDITION_NONE {
notOpenReject := req.GetQueryRejectCondition() == enumspb.QUERY_REJECT_CONDITION_NOT_OPEN
status := mutableStateResp.GetWorkflowStatus()
notCompletedCleanlyReject := req.GetQueryRejectCondition() == enumspb.QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY && status != enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED
if notOpenReject || notCompletedCleanlyReject {
return &historyservice.QueryWorkflowResponse{
Response: &workflowservice.QueryWorkflowResponse{
QueryRejected: &querypb.QueryRejected{
Status: status,
},
},
}, nil
}
}
de, err := e.shard.GetNamespaceRegistry().GetNamespaceByID(namespaceID)
if err != nil {
return nil, err
}
context, release, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
*request.GetRequest().GetExecution(),
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
defer func() { release(retErr) }()
mutableState, err := context.LoadWorkflowExecution(ctx)
if err != nil {
return nil, err
}
// 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.
//
// 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 := !de.ActiveInCluster(e.clusterMetadata.GetCurrentClusterName()) ||
!mutableState.IsWorkflowExecutionRunning() ||
(!mutableState.HasPendingWorkflowTask() && !mutableState.HasInFlightWorkflowTask())
if safeToDispatchDirectly {
release(nil)
msResp, err := e.getMutableState(ctx, namespaceID, *request.GetRequest().GetExecution())
if err != nil {
return nil, err
}
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)
release(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, namespaceID, *request.GetRequest().GetExecution())
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) {
e.logger.Error("query directly though matching on sticky failed, will not attempt query on non-sticky",
tag.WorkflowNamespace(queryRequest.GetNamespace()),
tag.WorkflowID(queryRequest.Execution.GetWorkflowId()),
tag.WorkflowRunID(queryRequest.Execution.GetRunId()),
tag.WorkflowQueryType(queryRequest.Query.GetQueryType()),
tag.Error(err))
return nil, err
}
if msResp.GetWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
e.logger.Info("query direct through matching failed on sticky, clearing sticky before attempting on non-sticky",
tag.WorkflowNamespace(queryRequest.GetNamespace()),
tag.WorkflowID(queryRequest.Execution.GetWorkflowId()),
tag.WorkflowRunID(queryRequest.Execution.GetRunId()),
tag.WorkflowQueryType(queryRequest.Query.GetQueryType()))
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 {
e.logger.Info("query context timed out before query on non-sticky task queue could be attempted",
tag.WorkflowNamespace(queryRequest.GetNamespace()),
tag.WorkflowID(queryRequest.Execution.GetWorkflowId()),
tag.WorkflowRunID(queryRequest.Execution.GetRunId()),
tag.WorkflowQueryType(queryRequest.Query.GetQueryType()))
scope.IncCounter(metrics.DirectQueryDispatchTimeoutBeforeNonStickyCount)
return nil, err
}
e.logger.Info("query directly through matching on sticky timed out, attempting to query on non-sticky",
tag.WorkflowNamespace(queryRequest.GetNamespace()),
tag.WorkflowID(queryRequest.Execution.GetWorkflowId()),
tag.WorkflowRunID(queryRequest.Execution.GetRunId()),
tag.WorkflowQueryType(queryRequest.Query.GetQueryType()),
tag.WorkflowTaskQueueName(msResp.GetStickyTaskQueue().GetName()),
tag.WorkflowNextEventID(msResp.GetNextEventId()))
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 {
e.logger.Error("query directly though matching on non-sticky failed",
tag.WorkflowNamespace(queryRequest.GetNamespace()),
tag.WorkflowID(queryRequest.Execution.GetWorkflowId()),
tag.WorkflowRunID(queryRequest.Execution.GetRunId()),
tag.WorkflowQueryType(queryRequest.Query.GetQueryType()),
tag.Error(err))
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,
namespaceID namespace.ID,
execution commonpb.WorkflowExecution,
) (_ *historyservice.GetMutableStateResponse, retError error) {
context, release, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
defer func() { release(retError) }()
mutableState, err := context.LoadWorkflowExecution(ctx)
if err != nil {
return nil, err
}
currentBranchToken, err := mutableState.GetCurrentBranchToken()
if err != nil {
return nil, err
}
executionInfo := mutableState.GetExecutionInfo()
execution.RunId = context.GetRunID()
workflowState, workflowStatus := mutableState.GetWorkflowStateStatus()
lastFirstEventID, lastFirstEventTxnID := mutableState.GetLastFirstEventIDTxnID()
return &historyservice.GetMutableStateResponse{
Execution: &execution,
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(),
),
}, 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
}
execution := commonpb.WorkflowExecution{
WorkflowId: request.Execution.WorkflowId,
RunId: request.Execution.RunId,
}
context, release, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
defer func() { release(retError) }()
response = &historyservice.DescribeMutableStateResponse{}
if context.(*workflow.ContextImpl).MutableState != nil {
msb := context.(*workflow.ContextImpl).MutableState
response.CacheMutableState = msb.CloneToProto()
}
// clear mutable state to force reload from persistence. This API returns both cached and persisted version.
context.Clear()
mutableState, err := context.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.updateWorkflowExecution(
ctx,
namespaceID,
*resetRequest.Execution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
mutableState.ClearStickyness()
return &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
}
execution := *request.Request.Execution
context, release, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
defer func() { release(retError) }()
mutableState, err1 := context.LoadWorkflowExecution(ctx)
if err1 != nil {
return nil, err1
}
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
completionEvent, err := mutableState.GetCompletionEvent(ctx)
if err != nil {
return nil, err
}
result.WorkflowExecutionInfo.CloseTime = completionEvent.GetEventTime()
}
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.StartedId != 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.ScheduleId)
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.InitiatedId,
ParentClosePolicy: ch.ParentClosePolicy,
}
result.PendingChildren = append(result.PendingChildren, p)
}
}
if di, ok := mutableState.GetPendingWorkflowTask(); ok {
pendingWorkflowTask := &workflowpb.PendingWorkflowTaskInfo{
State: enumspb.PENDING_WORKFLOW_TASK_STATE_SCHEDULED,
ScheduledTime: di.ScheduledTime,
OriginalScheduledTime: di.OriginalScheduledTime,
Attempt: di.Attempt,
}
if di.StartedID != common.EmptyEventID {
pendingWorkflowTask.State = enumspb.PENDING_WORKFLOW_TASK_STATE_STARTED
pendingWorkflowTask.StartedTime = di.StartedTime
}
result.PendingWorkflowTask = pendingWorkflowTask
}
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
}
namespaceID := namespaceEntry.ID()
namespace := namespaceEntry.Name()
execution := commonpb.WorkflowExecution{
WorkflowId: request.WorkflowExecution.WorkflowId,
RunId: request.WorkflowExecution.RunId,
}
response := &historyservice.RecordActivityTaskStartedResponse{}
err = e.updateWorkflowExecution(
ctx,
namespaceID,
execution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
scheduleID := request.GetScheduleId()
requestID := request.GetRequestId()
ai, isRunning := mutableState.GetActivityInfo(scheduleID)
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 && scheduleID >= 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, scheduleID)
if err != nil {
return nil, err
}
response.ScheduledEvent = scheduledEvent
response.CurrentAttemptScheduledTime = ai.ScheduledTime
if ai.StartedId != 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 &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, scheduleID, 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 &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)
}
// 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
}
namespaceID := namespaceEntry.ID()
namespace := namespaceEntry.Name()
request := req.CompleteRequest
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
if err0 != nil {
return consts.ErrDeserializingToken
}
workflowExecution := commonpb.WorkflowExecution{
WorkflowId: token.GetWorkflowId(),
RunId: token.GetRunId(),
}
var activityStartedTime time.Time
var taskQueue string
var workflowTypeName string
err = e.updateWorkflowExecution(
ctx,
namespaceID,
workflowExecution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
workflowTypeName = mutableState.GetWorkflowType().GetName()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
scheduleID := token.GetScheduleId()
if scheduleID == common.EmptyEventID { // client call CompleteActivityById, so get scheduleID by activityID
scheduleID, err0 = getScheduleID(token.GetActivityId(), mutableState)
if err0 != nil {
return nil, err0
}
}
ai, isRunning := mutableState.GetActivityInfo(scheduleID)
// 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 && scheduleID >= mutableState.GetNextEventID() {
e.metricsClient.IncCounter(metrics.HistoryRespondActivityTaskCompletedScope, metrics.StaleMutableStateCounter)
return nil, consts.ErrStaleState
}
if !isRunning || ai.StartedId == common.EmptyEventID ||
(token.GetScheduleId() != common.EmptyEventID && token.ScheduleAttempt != ai.Attempt) {
return nil, consts.ErrActivityTaskNotFound
}
if _, err := mutableState.AddActivityTaskCompletedEvent(scheduleID, ai.StartedId, request); err != nil {
// Unable to add ActivityTaskCompleted event to history
return nil, err
}
activityStartedTime = *ai.StartedTime
taskQueue = ai.TaskQueue
return &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
}
namespaceID := namespaceEntry.ID()
namespace := namespaceEntry.Name()
request := req.FailedRequest
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
if err0 != nil {
return consts.ErrDeserializingToken
}
workflowExecution := commonpb.WorkflowExecution{
WorkflowId: token.GetWorkflowId(),
RunId: token.GetRunId(),
}
var activityStartedTime time.Time
var taskQueue string
var workflowTypeName string
err = e.updateWorkflowExecution(ctx, namespaceID, workflowExecution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
workflowTypeName = mutableState.GetWorkflowType().GetName()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
scheduleID := token.GetScheduleId()
if scheduleID == common.EmptyEventID { // client call CompleteActivityById, so get scheduleID by activityID
scheduleID, err0 = getScheduleID(token.GetActivityId(), mutableState)
if err0 != nil {
return nil, err0
}
}
ai, isRunning := mutableState.GetActivityInfo(scheduleID)
// 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 && scheduleID >= mutableState.GetNextEventID() {
e.metricsClient.IncCounter(metrics.HistoryRespondActivityTaskFailedScope, metrics.StaleMutableStateCounter)
return nil, consts.ErrStaleState
}
if !isRunning || ai.StartedId == common.EmptyEventID ||
(token.GetScheduleId() != common.EmptyEventID && token.ScheduleAttempt != ai.Attempt) {
return nil, consts.ErrActivityTaskNotFound
}
e.logger.Debug("RespondActivityTaskFailed", tag.WorkflowScheduleID(scheduleID), 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 := &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(scheduleID, ai.StartedId, 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
}
namespaceID := namespaceEntry.ID()
namespace := namespaceEntry.Name()
request := req.CancelRequest
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
if err0 != nil {
return consts.ErrDeserializingToken
}
workflowExecution := commonpb.WorkflowExecution{
WorkflowId: token.GetWorkflowId(),
RunId: token.GetRunId(),
}
var activityStartedTime time.Time
var taskQueue string
var workflowTypeName string
err = e.updateWorkflowExecution(
ctx,
namespaceID,
workflowExecution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
workflowTypeName = mutableState.GetWorkflowType().GetName()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
scheduleID := token.GetScheduleId()
if scheduleID == common.EmptyEventID { // client call CompleteActivityById, so get scheduleID by activityID
scheduleID, err0 = getScheduleID(token.GetActivityId(), mutableState)
if err0 != nil {
return nil, err0
}
}
ai, isRunning := mutableState.GetActivityInfo(scheduleID)
// 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 && scheduleID >= mutableState.GetNextEventID() {
e.metricsClient.IncCounter(metrics.HistoryRespondActivityTaskCanceledScope, metrics.StaleMutableStateCounter)
return nil, consts.ErrStaleState
}
if !isRunning || ai.StartedId == common.EmptyEventID ||
(token.GetScheduleId() != common.EmptyEventID && token.ScheduleAttempt != 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(
scheduleID,
ai.StartedId,
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 &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) {
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(req.GetNamespaceId()))
if err != nil {
return nil, err
}
namespaceID := namespaceEntry.ID()
request := req.HeartbeatRequest
token, err0 := e.tokenSerializer.Deserialize(request.TaskToken)
if err0 != nil {
return nil, consts.ErrDeserializingToken
}
workflowExecution := commonpb.WorkflowExecution{
WorkflowId: token.GetWorkflowId(),
RunId: token.GetRunId(),
}
var cancelRequested bool
err = e.updateWorkflowExecution(
ctx,
namespaceID,
workflowExecution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
if !mutableState.IsWorkflowExecutionRunning() {
e.logger.Debug("Heartbeat failed")
return nil, consts.ErrWorkflowCompleted
}
scheduleID := token.GetScheduleId()
if scheduleID == common.EmptyEventID { // client call RecordActivityHeartbeatByID, so get scheduleID by activityID
scheduleID, err0 = getScheduleID(token.GetActivityId(), mutableState)
if err0 != nil {
return nil, err0
}
}
ai, isRunning := mutableState.GetActivityInfo(scheduleID)
// 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 && scheduleID >= mutableState.GetNextEventID() {
e.metricsClient.IncCounter(metrics.HistoryRecordActivityTaskHeartbeatScope, metrics.StaleMutableStateCounter)
return nil, consts.ErrStaleState
}
if !isRunning || ai.StartedId == common.EmptyEventID ||
(token.GetScheduleId() != common.EmptyEventID && token.ScheduleAttempt != ai.Attempt) {
return nil, consts.ErrActivityTaskNotFound
}
cancelRequested = ai.CancelRequested
e.logger.Debug("Activity heartbeat", tag.WorkflowScheduleID(scheduleID), tag.ActivityInfo(ai), tag.Bool(cancelRequested))
// Save progress and last HB reported time.
mutableState.UpdateActivityProgress(ai, request)
return &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()
execution := commonpb.WorkflowExecution{
WorkflowId: request.WorkflowExecution.WorkflowId,
}
firstExecutionRunID := request.GetFirstExecutionRunId()
// If firstExecutionRunID is set on the request always try to cancel currently running execution
if len(firstExecutionRunID) == 0 {
execution.RunId = request.WorkflowExecution.RunId
}
return e.updateWorkflow(ctx, namespaceID, execution,
func(workflowContext workflowContext) (*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 &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 &updateWorkflowAction{
noop: true,
createWorkflowTask: false,
}, nil
}
if _, err := mutableState.AddWorkflowExecutionCancelRequestedEvent(req); err != nil {
return nil, err
}
return 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()
execution := commonpb.WorkflowExecution{
WorkflowId: request.WorkflowExecution.WorkflowId,
RunId: request.WorkflowExecution.RunId,
}
return e.updateWorkflow(
ctx,
namespaceID,
execution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
if request.GetRequestId() != "" && mutableState.IsSignalRequested(request.GetRequestId()) {
return &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
}
maxAllowedSignals := e.config.MaximumSignalsPerExecution(namespaceEntry.Name().String())
if maxAllowedSignals > 0 && int(executionInfo.SignalCount) >= maxAllowedSignals {
e.logger.Info("Execution limit reached for maximum signals", tag.WorkflowSignalCount(executionInfo.SignalCount),
tag.WorkflowID(execution.GetWorkflowId()),
tag.WorkflowRunID(execution.GetRunId()),
tag.WorkflowNamespaceID(namespaceID.String()))
return nil, consts.ErrSignalsLimitExceeded
}
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 &updateWorkflowAction{
noop: false,
createWorkflowTask: createWorkflowTask,
}, nil
})
}
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()
namespace := namespaceEntry.Name()
sRequest := signalWithStartRequest.SignalWithStartRequest
execution := commonpb.WorkflowExecution{
WorkflowId: sRequest.WorkflowId,
}
var prevContext workflow.Context
var prevMutableState workflow.MutableState
attempt := 1
context, release, err0 := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeAPI,
)
if err0 == nil {
defer func() { release(retError) }()
Just_Signal_Loop:
for ; attempt <= conditionalRetryCount; attempt++ {
// workflow not exist, will create workflow then signal
mutableState, err1 := context.LoadWorkflowExecution(ctx)
if err1 != nil {
if _, ok := err1.(*serviceerror.NotFound); ok {
break
}
return nil, err1
}
// workflow exist but not running, will restart workflow then signal
// or workflow running but we need to terminate and restart
if !mutableState.IsWorkflowExecutionRunning() ||
sRequest.WorkflowIdReusePolicy == enums.WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING {
prevContext = context
prevMutableState = mutableState
break
}
executionInfo := mutableState.GetExecutionInfo()
maxAllowedSignals := e.config.MaximumSignalsPerExecution(namespace.String())
if maxAllowedSignals > 0 && int(executionInfo.SignalCount) >= maxAllowedSignals {
e.logger.Info("Execution limit reached for maximum signals", tag.WorkflowSignalCount(executionInfo.SignalCount),
tag.WorkflowID(execution.GetWorkflowId()),
tag.WorkflowRunID(execution.GetRunId()),
tag.WorkflowNamespaceID(namespaceID.String()))
return nil, consts.ErrSignalsLimitExceeded
}
if sRequest.GetRequestId() != "" && mutableState.IsSignalRequested(sRequest.GetRequestId()) {
// duplicate signal
return &historyservice.SignalWithStartWorkflowExecutionResponse{RunId: context.GetRunID()}, nil
}
if sRequest.GetRequestId() != "" {
mutableState.AddSignalRequested(sRequest.GetRequestId())
}
if _, err := mutableState.AddWorkflowExecutionSignaled(
sRequest.GetSignalName(),
sRequest.GetSignalInput(),
sRequest.GetIdentity(),
sRequest.GetHeader()); err != nil {
return nil, err
}
// Create a transfer task to schedule a workflow task
if !mutableState.HasPendingWorkflowTask() {
_, err := mutableState.AddWorkflowTaskScheduledEvent(false)
if err != nil {
return nil, err
}
}
// We apply the update to execution using optimistic concurrency. If it fails due to a conflict then reload
// the history and try the operation again.
if err := context.UpdateWorkflowExecutionAsActive(ctx, e.shard.GetTimeSource().Now()); err != nil {
if err == consts.ErrConflict {
continue Just_Signal_Loop
}
return nil, err
}
return &historyservice.SignalWithStartWorkflowExecutionResponse{RunId: context.GetRunID()}, nil
} // end for Just_Signal_Loop
if attempt == conditionalRetryCount+1 {
return nil, consts.ErrMaxAttemptsExceeded
}
} else {
if _, ok := err0.(*serviceerror.NotFound); !ok {
return nil, err0
}
// workflow not exist, will create workflow then signal
}
// Start workflow and signal
startRequest := e.getStartRequest(namespaceID, sRequest)
request := startRequest.StartRequest
e.overrideStartWorkflowExecutionRequest(request, metrics.HistorySignalWithStartWorkflowExecutionScope)
err = e.validateStartWorkflowExecutionRequest(ctx, request, namespace, "SignalWithStartWorkflowExecution")
if err != nil {
return nil, err
}
if err := common.CheckEventBlobSizeLimit(
sRequest.GetSignalInput().Size(),
e.config.BlobSizeLimitWarn(namespace.String()),
e.config.BlobSizeLimitError(namespace.String()),
namespace.String(),
sRequest.GetWorkflowId(),
"",
e.metricsScope(ctx).Tagged(metrics.CommandTypeTag(enumspb.COMMAND_TYPE_UNSPECIFIED.String())),
e.throttledLogger,
tag.BlobSizeViolationOperation("SignalWithStartWorkflowExecution"),
); err != nil {
return nil, err
}
workflowID := request.GetWorkflowId()
// grab the current context as a Lock, nothing more
_, currentRelease, err := e.historyCache.GetOrCreateCurrentWorkflowExecution(
ctx,
namespaceID,
workflowID,
)
if err != nil {
return nil, err
}
defer func() { currentRelease(retError) }()
execution = commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: uuid.New(),
}
if prevMutableState != nil {
prevExecutionUpdateAction, err := e.applyWorkflowIDReusePolicyForSignalWithStart(prevMutableState.GetExecutionState(), execution, request.WorkflowIdReusePolicy)
if err != nil {
return nil, err
}
if prevExecutionUpdateAction != nil {
err := e.updateWorkflowWithNewHelper(
ctx,
newWorkflowContext(prevContext, release, prevMutableState),
prevExecutionUpdateAction,
func() (workflow.Context, workflow.MutableState, error) {
return e.newWorkflowWithSignal(namespaceEntry, execution, startRequest, sRequest)
},
)
switch err {
case nil:
return &historyservice.SignalWithStartWorkflowExecutionResponse{
RunId: execution.GetRunId(),
}, nil
case consts.ErrWorkflowCompleted:
// previous workflow already closed
// fallthough to the logic for only creating the new workflow below
default:
return nil, err
}
}
}
now := e.timeSource.Now()
context, mutableState, err := e.newWorkflowWithSignal(namespaceEntry, execution, startRequest, sRequest)
if err != nil {
return nil, err
}
if err := e.newWorkflowVersionCheck(prevMutableState, mutableState); err != nil {
return nil, err
}
newWorkflow, newWorkflowEventsSeq, err := mutableState.CloseTransactionAsSnapshot(
now,
workflow.TransactionPolicyActive,
)
if err != nil {
return nil, err
}
if len(newWorkflowEventsSeq) != 1 {
return nil, serviceerror.NewInternal("unable to create 1st event batch")
}
createMode := persistence.CreateWorkflowModeBrandNew
prevRunID := ""
prevLastWriteVersion := int64(0)
if prevMutableState != nil {
createMode = persistence.CreateWorkflowModeWorkflowIDReuse
prevRunID = prevMutableState.GetExecutionState().GetRunId()
prevLastWriteVersion, err = prevMutableState.GetLastWriteVersion()
if err != nil {
return nil, err
}
}
err = context.CreateWorkflowExecution(
ctx,
now,
createMode,
prevRunID,
prevLastWriteVersion,
mutableState,
newWorkflow,
newWorkflowEventsSeq,
)
if t, ok := err.(*persistence.CurrentWorkflowConditionFailedError); ok {
if t.RequestID == request.GetRequestId() {
return &historyservice.SignalWithStartWorkflowExecutionResponse{
RunId: t.RunID,
}, nil
// delete history is expected here because duplicate start request will create history with different rid
}
return nil, err
}
if err != nil {
return nil, err
}
return &historyservice.SignalWithStartWorkflowExecutionResponse{
RunId: execution.RunId,
}, nil
}
// RemoveSignalMutableState remove the signal request id in signal_requested for deduplicate
func (e *historyEngineImpl) RemoveSignalMutableState(
ctx context.Context,
request *historyservice.RemoveSignalMutableStateRequest,
) error {
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(request.GetNamespaceId()))
if err != nil {
return err
}
namespaceID := namespaceEntry.ID()
execution := commonpb.WorkflowExecution{
WorkflowId: request.WorkflowExecution.WorkflowId,
RunId: request.WorkflowExecution.RunId,
}
return e.updateWorkflowExecution(
ctx,
namespaceID,
execution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
mutableState.DeleteSignalRequested(request.GetRequestId())
return &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
execution := commonpb.WorkflowExecution{
WorkflowId: request.WorkflowExecution.WorkflowId,
}
firstExecutionRunID := request.GetFirstExecutionRunId()
// If firstExecutionRunID is set on the request always try to terminate currently running execution
if len(firstExecutionRunID) == 0 {
execution.RunId = request.WorkflowExecution.RunId
}
return e.updateWorkflow(
ctx,
namespaceID,
execution,
func(workflowContext workflowContext) (*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 updateWorkflowWithoutWorkflowTask, workflow.TerminateWorkflow(
mutableState,
eventBatchFirstEventID,
request.GetReason(),
request.GetDetails(),
request.GetIdentity(),
)
})
}
func (e *historyEngineImpl) DeleteWorkflowExecution(
ctx context.Context,
request *historyservice.DeleteWorkflowExecutionRequest,
) (retError error) {
nsID := namespace.ID(request.GetNamespaceId())
wfCtx, err := e.loadWorkflow(ctx, nsID, request.GetWorkflowExecution().GetWorkflowId(), request.GetWorkflowExecution().GetRunId())
if err != nil {
return err
}
defer func() { wfCtx.getReleaseFn()(retError) }()
return e.workflowDeleteManager.AddDeleteWorkflowExecutionTask(
ctx,
nsID,
commonpb.WorkflowExecution{
WorkflowId: request.GetWorkflowExecution().GetWorkflowId(),
RunId: request.GetWorkflowExecution().GetRunId(),
},
wfCtx.getMutableState())
}
// RecordChildExecutionCompleted records the completion of child execution into parent execution history
func (e *historyEngineImpl) RecordChildExecutionCompleted(
ctx context.Context,
completionRequest *historyservice.RecordChildExecutionCompletedRequest,
) error {
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(completionRequest.GetNamespaceId()))
if err != nil {
return err
}
namespaceID := namespaceEntry.ID()
execution := commonpb.WorkflowExecution{
WorkflowId: completionRequest.WorkflowExecution.WorkflowId,
RunId: completionRequest.WorkflowExecution.RunId,
}
return e.updateWorkflowExecution(
ctx,
namespaceID,
execution,
func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
initiatedID := completionRequest.InitiatedId
completedExecution := completionRequest.CompletedExecution
completionEvent := completionRequest.CompletionEvent
// Check mutable state to make sure child execution is in pending child executions
ci, isRunning := mutableState.GetChildExecutionInfo(initiatedID)
if (!isRunning && initiatedID >= mutableState.GetNextEventID()) ||
(isRunning && ci.StartedId == common.EmptyEventID) {
// possible stale mutable state, try reload mutable state
//
// TODO: use initiate event ID and version to verify if the child exists or not
//
// NOTE: do not return ErrStaleState here, as in xdc there's no guarantee that parent
// will have the child information and its next eventID will larger than the initiatedID
// in the request after forced failover.
// If ErrStaleState is returned, the logic for this handler and processing of CloseWorkflowExecution
// task will keep retrying infinitely.
workflowContext.getContext().Clear()
mutableState, err = workflowContext.reloadMutableState(ctx)
if err != nil {
return nil, err
}
ci, isRunning = mutableState.GetChildExecutionInfo(initiatedID)
}
if !isRunning || ci.StartedId == common.EmptyEventID {
return nil, serviceerror.NewNotFound("Pending child execution not found.")
}
if ci.GetStartedWorkflowId() != completedExecution.GetWorkflowId() {
return nil, serviceerror.NewNotFound("Pending child execution not found.")
}
switch completionEvent.GetEventType() {
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED:
attributes := completionEvent.GetWorkflowExecutionCompletedEventAttributes()
_, err = mutableState.AddChildWorkflowExecutionCompletedEvent(initiatedID, completedExecution, attributes)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_FAILED:
attributes := completionEvent.GetWorkflowExecutionFailedEventAttributes()
_, err = mutableState.AddChildWorkflowExecutionFailedEvent(initiatedID, completedExecution, attributes)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED:
attributes := completionEvent.GetWorkflowExecutionCanceledEventAttributes()
_, err = mutableState.AddChildWorkflowExecutionCanceledEvent(initiatedID, completedExecution, attributes)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED:
attributes := completionEvent.GetWorkflowExecutionTerminatedEventAttributes()
_, err = mutableState.AddChildWorkflowExecutionTerminatedEvent(initiatedID, completedExecution, attributes)
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT:
attributes := completionEvent.GetWorkflowExecutionTimedOutEventAttributes()
_, err = mutableState.AddChildWorkflowExecutionTimedOutEvent(initiatedID, completedExecution, attributes)
}
if err != nil {
return nil, err
}
return &updateWorkflowAction{
noop: false,
createWorkflowTask: true,
}, err
})
}
func (e *historyEngineImpl) ReplicateEventsV2(
ctx context.Context,
replicateRequest *historyservice.ReplicateEventsV2Request,
) error {
return e.nDCReplicator.ApplyEvents(ctx, replicateRequest)
}
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)
}
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()
baseContext, baseReleaseFn, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: baseRunID,
},
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
defer func() { baseReleaseFn(retError) }()
baseMutableState, err := baseContext.LoadWorkflowExecution(ctx)
if err != nil {
return nil, err
}
if request.GetWorkflowTaskFinishEventId() <= common.FirstEventID ||
request.GetWorkflowTaskFinishEventId() >= baseMutableState.GetNextEventID() {
return nil, serviceerror.NewInvalidArgument("Workflow task finish ID must be > 1 && <= workflow last event ID.")
}
switch request.GetResetReapplyType() {
case enumspb.RESET_REAPPLY_TYPE_UNSPECIFIED:
return nil, serviceerror.NewInvalidArgument("reset type not set")
case enumspb.RESET_REAPPLY_TYPE_SIGNAL:
// noop
case enumspb.RESET_REAPPLY_TYPE_NONE:
// noop
default:
return nil, serviceerror.NewInternal("unknown reset type")
}
// also load the current run of the workflow, it can be different from the base runID
resp, err := e.shard.GetCurrentExecution(ctx, &persistence.GetCurrentExecutionRequest{
ShardID: e.shard.GetShardID(),
NamespaceID: namespaceID.String(),
WorkflowID: request.WorkflowExecution.GetWorkflowId(),
})
if err != nil {
return nil, err
}
currentRunID := resp.RunID
if baseRunID == "" {
baseRunID = currentRunID
}
var currentContext workflow.Context
var currentMutableState workflow.MutableState
var currentReleaseFn workflow.ReleaseCacheFunc
if currentRunID == baseRunID {
currentContext = baseContext
currentMutableState = baseMutableState
} else {
currentContext, currentReleaseFn, err = e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: currentRunID,
},
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
defer func() { currentReleaseFn(retError) }()
currentMutableState, err = currentContext.LoadWorkflowExecution(ctx)
if err != nil {
return nil, err
}
}
// dedup by requestID
if currentMutableState.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(),
currentContext,
currentMutableState,
currentReleaseFn,
),
request.GetReason(),
nil,
request.GetResetReapplyType(),
); err != nil {
return nil, err
}
return &historyservice.ResetWorkflowExecutionResponse{
RunId: resetRunID,
}, nil
}
func (e *historyEngineImpl) updateWorkflow(
ctx context.Context,
namespaceID namespace.ID,
execution commonpb.WorkflowExecution,
action updateWorkflowActionFunc,
) (retError error) {
workflowContext, err := e.loadWorkflow(ctx, namespaceID, execution.GetWorkflowId(), execution.GetRunId())
if err != nil {
return err
}
defer func() { workflowContext.getReleaseFn()(retError) }()
return e.updateWorkflowWithNewHelper(ctx, workflowContext, action, nil)
}
func (e *historyEngineImpl) updateWorkflowExecution(
ctx context.Context,
namespaceID namespace.ID,
execution commonpb.WorkflowExecution,
action updateWorkflowActionFunc,
) (retError error) {
workflowContext, err := e.loadWorkflowOnce(ctx, namespaceID, execution.GetWorkflowId(), execution.GetRunId())
if err != nil {
return err
}
defer func() { workflowContext.getReleaseFn()(retError) }()
return e.updateWorkflowWithNewHelper(ctx, workflowContext, action, nil)
}
func (e *historyEngineImpl) updateWorkflowExecutionWithNew(
ctx context.Context,
namespaceID namespace.ID,
execution commonpb.WorkflowExecution,
action updateWorkflowActionFunc,
newWorkflowFn func() (workflow.Context, workflow.MutableState, error),
) (retError error) {
workflowContext, err := e.loadWorkflowOnce(
ctx,
namespaceID,
execution.GetWorkflowId(),
execution.GetRunId(),
)
if err != nil {
return err
}
defer func() { workflowContext.getReleaseFn()(retError) }()
return e.updateWorkflowWithNewHelper(ctx, workflowContext, action, newWorkflowFn)
}
func (e *historyEngineImpl) updateWorkflowWithNewHelper(
ctx context.Context,
workflowContext workflowContext,
action updateWorkflowActionFunc,
newWorkflowFn func() (workflow.Context, workflow.MutableState, error),
) (retError error) {
UpdateHistoryLoop:
for attempt := 1; attempt <= conditionalRetryCount; attempt++ {
// conduct caller action
postActions, err := action(workflowContext)
if err != nil {
if err == consts.ErrStaleState {
// Handler detected that cached workflow mutable could potentially be stale
// Reload workflow execution history
workflowContext.getContext().Clear()
if attempt != conditionalRetryCount {
_, err = workflowContext.reloadMutableState(ctx)
if err != nil {
return err
}
}
continue UpdateHistoryLoop
}
// Returned error back to the caller
return err
}
if postActions.noop {
return nil
}
mutableState := workflowContext.getMutableState()
if postActions.createWorkflowTask {
// Create a transfer task to schedule a workflow task
if !mutableState.HasPendingWorkflowTask() {
if _, err := mutableState.AddWorkflowTaskScheduledEvent(
false,
); err != nil {
return err
}
}
}
if newWorkflowFn != nil {
var newContext workflow.Context
var newMutableState workflow.MutableState
newContext, newMutableState, err = newWorkflowFn()
if err != nil {
return err
}
if err = e.newWorkflowVersionCheck(mutableState, newMutableState); err != nil {
return err
}
err = workflowContext.getContext().UpdateWorkflowExecutionWithNewAsActive(
ctx,
e.shard.GetTimeSource().Now(),
newContext,
newMutableState,
)
} else {
err = workflowContext.getContext().UpdateWorkflowExecutionAsActive(
ctx,
e.shard.GetTimeSource().Now(),
)
}
if err == consts.ErrConflict {
if attempt != conditionalRetryCount {
_, err = workflowContext.reloadMutableState(ctx)
if err != nil {
return err
}
}
continue UpdateHistoryLoop
}
return err
}
return consts.ErrMaxAttemptsExceeded
}
func (e *historyEngineImpl) newWorkflowVersionCheck(
prevMutableState workflow.MutableState,
newMutableState workflow.MutableState,
) error {
if prevMutableState == nil {
return nil
}
prevLastWriteVersion, err := prevMutableState.GetLastWriteVersion()
if err != nil {
return err
}
if prevLastWriteVersion > newMutableState.GetCurrentVersion() {
clusterMetadata := e.shard.GetClusterMetadata()
namespaceEntry := newMutableState.GetNamespaceEntry()
return serviceerror.NewNamespaceNotActive(
namespaceEntry.Name().String(),
clusterMetadata.GetCurrentClusterName(),
clusterMetadata.ClusterNameForFailoverVersion(namespaceEntry.IsGlobalNamespace(), prevLastWriteVersion),
)
}
return nil
}
func (e *historyEngineImpl) failWorkflowTask(
ctx context.Context,
wfContext workflow.Context,
scheduleID int64,
startedID 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(
scheduleID,
startedID,
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.replicatorProcessor != nil {
e.replicatorProcessor.NotifyNewTasks(tasksByCategory)
}
continue
}
if len(tasksByCategory) > 0 {
e.queueProcessors[category].NotifyNewTasks(clusterName, tasksByCategory)
}
}
}
func (e *historyEngineImpl) validateStartWorkflowExecutionRequest(
ctx context.Context,
request *workflowservice.StartWorkflowExecutionRequest,
namespace namespace.Name,
operation string,
) error {
maxIDLengthLimit := e.config.MaxIDLengthLimit()
blobSizeLimitError := e.config.BlobSizeLimitError(namespace.String())
blobSizeLimitWarn := e.config.BlobSizeLimitWarn(namespace.String())
memoSizeLimitError := e.config.MemoSizeLimitError(namespace.String())
memoSizeLimitWarn := e.config.MemoSizeLimitWarn(namespace.String())
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 := common.CheckEventBlobSizeLimit(
request.GetInput().Size(),
blobSizeLimitWarn,
blobSizeLimitError,
namespace.String(),
request.GetWorkflowId(),
"",
e.metricsScope(ctx).Tagged(metrics.CommandTypeTag(enumspb.COMMAND_TYPE_UNSPECIFIED.String())),
e.throttledLogger,
tag.BlobSizeViolationOperation(operation),
); err != nil {
return err
}
if err := common.CheckEventBlobSizeLimit(
request.GetMemo().Size(),
memoSizeLimitWarn,
memoSizeLimitError,
namespace.String(),
request.GetWorkflowId(),
"",
e.metricsScope(ctx).Tagged(metrics.CommandTypeTag(enumspb.COMMAND_TYPE_UNSPECIFIED.String())),
e.throttledLogger,
tag.BlobSizeViolationOperation(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 getScheduleID(
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.ScheduleId, 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())
}
// for startWorkflowExecution & signalWithStart to handle workflow reuse policy
func (e *historyEngineImpl) applyWorkflowIDReusePolicyForSignalWithStart(
prevExecutionState *persistencespb.WorkflowExecutionState,
execution commonpb.WorkflowExecution,
wfIDReusePolicy enumspb.WorkflowIdReusePolicy,
) (updateWorkflowActionFunc, error) {
prevStartRequestID := prevExecutionState.CreateRequestId
prevRunID := prevExecutionState.RunId
prevState := prevExecutionState.State
prevStatus := prevExecutionState.Status
return e.applyWorkflowIDReusePolicyHelper(
prevStartRequestID,
prevRunID,
prevState,
prevStatus,
execution,
wfIDReusePolicy,
)
}
// applyWorkflowIDReusePolicyHelper returns updateWorkflowActionFunc
// for updating the previous execution and an error if the situation is
// not allowed by the workflowIDReusePolicy.
// Both result may be nil, if the case is allow and no update is needed
// for the previous execution.
func (e *historyEngineImpl) applyWorkflowIDReusePolicyHelper(
prevStartRequestID,
prevRunID string,
prevState enumsspb.WorkflowExecutionState,
prevStatus enumspb.WorkflowExecutionStatus,
newExecution commonpb.WorkflowExecution,
wfIDReusePolicy enumspb.WorkflowIdReusePolicy,
) (updateWorkflowActionFunc, error) {
// here we know there is some information about the prev workflow, i.e. either running right now
// or has history check if the this workflow is finished
switch prevState {
case enumsspb.WORKFLOW_EXECUTION_STATE_CREATED,
enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING:
if wfIDReusePolicy == enumspb.WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING {
return func(workflowContext workflowContext) (*updateWorkflowAction, error) {
mutableState := workflowContext.getMutableState()
if !mutableState.IsWorkflowExecutionRunning() {
return nil, consts.ErrWorkflowCompleted
}
return updateWorkflowWithoutWorkflowTask, workflow.TerminateWorkflow(
mutableState,
mutableState.GetNextEventID(),
"TerminateIfRunning WorkflowIdReusePolicy Policy",
payloads.EncodeString(
fmt.Sprintf("terminated by new runID: %s", newExecution.RunId),
),
consts.IdentityHistoryService,
)
}, nil
}
msg := "Workflow execution is already running. WorkflowId: %v, RunId: %v."
return nil, getWorkflowAlreadyStartedError(msg, prevStartRequestID, newExecution.GetWorkflowId(), prevRunID)
case enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED:
// previous workflow completed, proceed
default:
// persistence.WorkflowStateZombie or unknown type
return nil, serviceerror.NewInternal(fmt.Sprintf("Failed to process workflow, workflow has invalid state: %v.", prevState))
}
switch wfIDReusePolicy {
case enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY:
if _, ok := consts.FailedWorkflowStatuses[prevStatus]; !ok {
msg := "Workflow execution already finished successfully. WorkflowId: %v, RunId: %v. Workflow Id reuse policy: allow duplicate workflow Id if last run failed."
return nil, getWorkflowAlreadyStartedError(msg, prevStartRequestID, newExecution.GetWorkflowId(), prevRunID)
}
case enumspb.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE,
enumspb.WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING:
// as long as workflow not running, so this case has no check
case enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE:
msg := "Workflow execution already finished. WorkflowId: %v, RunId: %v. Workflow Id reuse policy: reject duplicate workflow Id."
return nil, getWorkflowAlreadyStartedError(msg, prevStartRequestID, newExecution.GetWorkflowId(), prevRunID)
default:
return nil, serviceerror.NewInternal(fmt.Sprintf("Failed to process start workflow reuse policy: %v.", wfIDReusePolicy))
}
return nil, nil
}
func getWorkflowAlreadyStartedError(errMsg string, createRequestID string, workflowID string, runID string) error {
return serviceerror.NewWorkflowExecutionAlreadyStarted(
fmt.Sprintf(errMsg, workflowID, runID),
createRequestID,
runID,
)
}
func (e *historyEngineImpl) GetReplicationMessages(
ctx context.Context,
pollingCluster string,
ackMessageID int64,
ackTimestampe time.Time,
queryMessageID int64,
) (*replicationspb.ReplicationMessages, error) {
if ackMessageID != persistence.EmptyQueueMessageID {
if err := e.shard.UpdateQueueClusterAckLevel(
tasks.CategoryReplication,
pollingCluster,
tasks.Key{
TaskID: ackMessageID,
},
); err != nil {
e.logger.Error("error updating replication level for shard", tag.Error(err), tag.OperationFailed)
}
e.shard.UpdateRemoteClusterInfo(pollingCluster, ackMessageID, ackTimestampe)
}
replicationMessages, err := e.replicatorProcessor.paginateTasks(
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.replicatorProcessor.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()
// remove run id from the execution so that reapply events to the current run
currentExecution := commonpb.WorkflowExecution{
WorkflowId: workflowID,
}
return e.updateWorkflowExecution(
ctx,
namespaceID,
currentExecution,
func(workflowContext workflowContext) (action *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.loadWorkflowOnce(ctx, namespaceID, 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 &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(currentExecution.GetWorkflowId()),
)
e.metricsClient.IncCounter(metrics.HistoryReapplyEventsScope, metrics.EventReapplySkippedCount)
return &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 &updateWorkflowAction{
noop: true,
createWorkflowTask: false,
}, nil
}
postActions := &updateWorkflowAction{
noop: false,
createWorkflowTask: true,
}
// Do not create workflow task when the workflow is cron and the cron has not been started yet
if mutableState.GetExecutionInfo().CronSchedule != "" && !mutableState.HasProcessedOrPendingWorkflowTask() {
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 &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) {
namespaceEntry, err := e.getActiveNamespaceEntry(namespaceUUID)
if err != nil {
return err
}
namespaceID := namespaceEntry.ID()
context, release, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeAPI,
)
if err != nil {
return err
}
defer func() { release(retError) }()
mutableState, err := context.LoadWorkflowExecution(ctx)
if err != nil {
return err
}
if !mutableState.IsWorkflowExecutionRunning() {
return nil
}
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
}
err = context.UpdateWorkflowExecutionAsActive(ctx, now)
if err != nil {
return err
}
return nil
}
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()
context, release, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
*request.GetExecution(),
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
defer func() { release(retError) }()
mutableState, err := context.LoadWorkflowExecution(ctx)
if err != nil {
return nil, err
}
now := e.shard.GetTimeSource().Now()
task, err := mutableState.GenerateLastHistoryReplicationTasks(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()),
}
if e.replicatorProcessor.maxTaskID != nil {
resp.MaxReplicationTaskId = *e.replicatorProcessor.maxTaskID
}
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) loadWorkflowOnce(
ctx context.Context,
namespaceID namespace.ID,
workflowID string,
runID string,
) (workflowContext, error) {
context, release, err := e.historyCache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: runID,
},
workflow.CallerTypeAPI,
)
if err != nil {
return nil, err
}
mutableState, err := context.LoadWorkflowExecution(ctx)
if err != nil {
release(err)
return nil, err
}
return newWorkflowContext(context, release, mutableState), nil
}
func (e *historyEngineImpl) loadWorkflow(
ctx context.Context,
namespaceID namespace.ID,
workflowID string,
runID string,
) (workflowContext, error) {
if runID != "" {
return e.loadWorkflowOnce(ctx, namespaceID, workflowID, runID)
}
for attempt := 1; attempt <= conditionalRetryCount; attempt++ {
workflowContext, err := e.loadWorkflowOnce(ctx, namespaceID, workflowID, "")
if err != nil {
return nil, err
}
if workflowContext.getMutableState().IsWorkflowExecutionRunning() {
return workflowContext, nil
}
// workflow not running, need to check current record
resp, err := e.shard.GetCurrentExecution(
ctx,
&persistence.GetCurrentExecutionRequest{
ShardID: e.shard.GetShardID(),
NamespaceID: namespaceID.String(),
WorkflowID: workflowID,
},
)
if err != nil {
workflowContext.getReleaseFn()(err)
return nil, err
}
if resp.RunID == workflowContext.getRunID() {
return workflowContext, nil
}
workflowContext.getReleaseFn()(nil)
}
return nil, serviceerror.NewUnavailable("unable to locate current workflow execution")
}
func (e *historyEngineImpl) metricsScope(ctx context.Context) metrics.Scope {
return interceptor.MetricsScope(ctx, e.logger)
}
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
}