mirror of
https://github.com/hanzoai/tasks.git
synced 2026-11-02 12:22:18 +00:00
2137 lines
74 KiB
Go
2137 lines
74 KiB
Go
// The MIT License
|
|
//
|
|
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
|
|
//
|
|
// Copyright (c) 2020 Uber Technologies, Inc.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
package history
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/pborman/uuid"
|
|
"go.opentelemetry.io/otel/trace"
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
historypb "go.temporal.io/api/history/v1"
|
|
querypb "go.temporal.io/api/query/v1"
|
|
"go.temporal.io/api/serviceerror"
|
|
taskqueuepb "go.temporal.io/api/taskqueue/v1"
|
|
workflowpb "go.temporal.io/api/workflow/v1"
|
|
"go.temporal.io/api/workflowservice/v1"
|
|
|
|
enumsspb "go.temporal.io/server/api/enums/v1"
|
|
historyspb "go.temporal.io/server/api/history/v1"
|
|
"go.temporal.io/server/api/historyservice/v1"
|
|
"go.temporal.io/server/api/matchingservice/v1"
|
|
replicationspb "go.temporal.io/server/api/replication/v1"
|
|
"go.temporal.io/server/client"
|
|
"go.temporal.io/server/common"
|
|
"go.temporal.io/server/common/clock"
|
|
"go.temporal.io/server/common/cluster"
|
|
"go.temporal.io/server/common/definition"
|
|
"go.temporal.io/server/common/log"
|
|
"go.temporal.io/server/common/log/tag"
|
|
"go.temporal.io/server/common/metrics"
|
|
"go.temporal.io/server/common/namespace"
|
|
"go.temporal.io/server/common/persistence"
|
|
"go.temporal.io/server/common/persistence/serialization"
|
|
"go.temporal.io/server/common/persistence/versionhistory"
|
|
"go.temporal.io/server/common/primitives/timestamp"
|
|
"go.temporal.io/server/common/sdk"
|
|
"go.temporal.io/server/common/searchattribute"
|
|
serviceerrors "go.temporal.io/server/common/serviceerror"
|
|
"go.temporal.io/server/service/history/api"
|
|
"go.temporal.io/server/service/history/api/recordactivitytaskheartbeat"
|
|
"go.temporal.io/server/service/history/api/recordactivitytaskstarted"
|
|
"go.temporal.io/server/service/history/api/requestcancelworkflow"
|
|
"go.temporal.io/server/service/history/api/resetstickytaskqueue"
|
|
respondactivitytaskcandeled "go.temporal.io/server/service/history/api/respondactivitytaskcanceled"
|
|
"go.temporal.io/server/service/history/api/respondactivitytaskcompleted"
|
|
"go.temporal.io/server/service/history/api/respondactivitytaskfailed"
|
|
"go.temporal.io/server/service/history/api/signalwithstartworkflow"
|
|
"go.temporal.io/server/service/history/api/signalworkflow"
|
|
"go.temporal.io/server/service/history/api/startworkflow"
|
|
"go.temporal.io/server/service/history/api/terminateworkflow"
|
|
"go.temporal.io/server/service/history/configs"
|
|
"go.temporal.io/server/service/history/consts"
|
|
"go.temporal.io/server/service/history/events"
|
|
"go.temporal.io/server/service/history/queues"
|
|
"go.temporal.io/server/service/history/replication"
|
|
"go.temporal.io/server/service/history/shard"
|
|
"go.temporal.io/server/service/history/tasks"
|
|
"go.temporal.io/server/service/history/workflow"
|
|
"go.temporal.io/server/service/worker/archiver"
|
|
)
|
|
|
|
const (
|
|
activityCancellationMsgActivityNotStarted = "ACTIVITY_ID_NOT_STARTED"
|
|
)
|
|
|
|
type (
|
|
historyEngineImpl struct {
|
|
status int32
|
|
currentClusterName string
|
|
shard shard.Context
|
|
timeSource clock.TimeSource
|
|
workflowTaskHandler workflowTaskHandlerCallbacks
|
|
clusterMetadata cluster.Metadata
|
|
executionManager persistence.ExecutionManager
|
|
queueProcessors map[tasks.Category]queues.Queue
|
|
replicationAckMgr replication.AckManager
|
|
nDCReplicator nDCHistoryReplicator
|
|
nDCActivityReplicator nDCActivityReplicator
|
|
replicationProcessorMgr common.Daemon
|
|
eventNotifier events.Notifier
|
|
tokenSerializer common.TaskTokenSerializer
|
|
metricsClient metrics.Client
|
|
logger log.Logger
|
|
throttledLogger log.Logger
|
|
config *configs.Config
|
|
workflowRebuilder workflowRebuilder
|
|
workflowResetter workflowResetter
|
|
sdkClientFactory sdk.ClientFactory
|
|
eventsReapplier nDCEventsReapplier
|
|
matchingClient matchingservice.MatchingServiceClient
|
|
rawMatchingClient matchingservice.MatchingServiceClient
|
|
replicationDLQHandler replication.DLQHandler
|
|
searchAttributesValidator *searchattribute.Validator
|
|
workflowDeleteManager workflow.DeleteManager
|
|
eventSerializer serialization.Serializer
|
|
workflowConsistencyChecker api.WorkflowConsistencyChecker
|
|
tracer trace.Tracer
|
|
}
|
|
)
|
|
|
|
// NewEngineWithShardContext creates an instance of history engine
|
|
func NewEngineWithShardContext(
|
|
shard shard.Context,
|
|
clientBean client.Bean,
|
|
matchingClient matchingservice.MatchingServiceClient,
|
|
sdkClientFactory sdk.ClientFactory,
|
|
eventNotifier events.Notifier,
|
|
config *configs.Config,
|
|
rawMatchingClient matchingservice.MatchingServiceClient,
|
|
workflowCache workflow.Cache,
|
|
archivalClient archiver.Client,
|
|
eventSerializer serialization.Serializer,
|
|
queueProcessorFactories []QueueFactory,
|
|
replicationTaskFetcherFactory replication.TaskFetcherFactory,
|
|
replicationTaskExecutorProvider replication.TaskExecutorProvider,
|
|
workflowConsistencyChecker api.WorkflowConsistencyChecker,
|
|
tracerProvider trace.TracerProvider,
|
|
) shard.Engine {
|
|
currentClusterName := shard.GetClusterMetadata().GetCurrentClusterName()
|
|
|
|
logger := shard.GetLogger()
|
|
executionManager := shard.GetExecutionManager()
|
|
|
|
workflowDeleteManager := workflow.NewDeleteManager(
|
|
shard,
|
|
workflowCache,
|
|
config,
|
|
archivalClient,
|
|
shard.GetTimeSource(),
|
|
)
|
|
|
|
historyEngImpl := &historyEngineImpl{
|
|
status: common.DaemonStatusInitialized,
|
|
currentClusterName: currentClusterName,
|
|
shard: shard,
|
|
clusterMetadata: shard.GetClusterMetadata(),
|
|
timeSource: shard.GetTimeSource(),
|
|
executionManager: executionManager,
|
|
tokenSerializer: common.NewProtoTaskTokenSerializer(),
|
|
logger: log.With(logger, tag.ComponentHistoryEngine),
|
|
throttledLogger: log.With(shard.GetThrottledLogger(), tag.ComponentHistoryEngine),
|
|
metricsClient: shard.GetMetricsClient(),
|
|
eventNotifier: eventNotifier,
|
|
config: config,
|
|
sdkClientFactory: sdkClientFactory,
|
|
matchingClient: matchingClient,
|
|
rawMatchingClient: rawMatchingClient,
|
|
workflowDeleteManager: workflowDeleteManager,
|
|
eventSerializer: eventSerializer,
|
|
workflowConsistencyChecker: workflowConsistencyChecker,
|
|
tracer: tracerProvider.Tracer(consts.LibraryName),
|
|
}
|
|
|
|
historyEngImpl.queueProcessors = make(map[tasks.Category]queues.Queue)
|
|
for _, factory := range queueProcessorFactories {
|
|
processor := factory.CreateQueue(shard, historyEngImpl, workflowCache)
|
|
historyEngImpl.queueProcessors[processor.Category()] = processor
|
|
}
|
|
|
|
historyEngImpl.eventsReapplier = newNDCEventsReapplier(shard.GetMetricsClient(), logger)
|
|
|
|
if shard.GetClusterMetadata().IsGlobalNamespaceEnabled() {
|
|
historyEngImpl.replicationAckMgr = replication.NewAckManager(
|
|
shard,
|
|
workflowCache,
|
|
executionManager,
|
|
logger,
|
|
)
|
|
historyEngImpl.nDCReplicator = newNDCHistoryReplicator(
|
|
shard,
|
|
workflowCache,
|
|
historyEngImpl.eventsReapplier,
|
|
logger,
|
|
eventSerializer,
|
|
)
|
|
historyEngImpl.nDCActivityReplicator = newNDCActivityReplicator(
|
|
shard,
|
|
workflowCache,
|
|
logger,
|
|
)
|
|
}
|
|
historyEngImpl.workflowRebuilder = NewWorkflowRebuilder(
|
|
shard,
|
|
workflowCache,
|
|
logger,
|
|
)
|
|
historyEngImpl.workflowResetter = newWorkflowResetter(
|
|
shard,
|
|
workflowCache,
|
|
logger,
|
|
)
|
|
|
|
historyEngImpl.searchAttributesValidator = searchattribute.NewValidator(
|
|
shard.GetSearchAttributesProvider(),
|
|
shard.GetSearchAttributesMapper(),
|
|
config.SearchAttributesNumberOfKeysLimit,
|
|
config.SearchAttributesSizeOfValueLimit,
|
|
config.SearchAttributesTotalSizeLimit,
|
|
)
|
|
|
|
historyEngImpl.workflowTaskHandler = newWorkflowTaskHandlerCallback(historyEngImpl)
|
|
historyEngImpl.replicationDLQHandler = replication.NewLazyDLQHandler(
|
|
shard,
|
|
workflowDeleteManager,
|
|
workflowCache,
|
|
clientBean,
|
|
replicationTaskExecutorProvider,
|
|
)
|
|
historyEngImpl.replicationProcessorMgr = replication.NewTaskProcessorManager(
|
|
config,
|
|
shard,
|
|
historyEngImpl,
|
|
workflowCache,
|
|
workflowDeleteManager,
|
|
clientBean,
|
|
eventSerializer,
|
|
replicationTaskFetcherFactory,
|
|
replicationTaskExecutorProvider,
|
|
)
|
|
return historyEngImpl
|
|
}
|
|
|
|
// Start will spin up all the components needed to start serving this shard.
|
|
// Make sure all the components are loaded lazily so start can return immediately. This is important because
|
|
// ShardController calls start sequentially for all the shards for a given host during startup.
|
|
func (e *historyEngineImpl) Start() {
|
|
if !atomic.CompareAndSwapInt32(
|
|
&e.status,
|
|
common.DaemonStatusInitialized,
|
|
common.DaemonStatusStarted,
|
|
) {
|
|
return
|
|
}
|
|
|
|
e.logger.Info("", tag.LifeCycleStarting)
|
|
defer e.logger.Info("", tag.LifeCycleStarted)
|
|
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.Start()
|
|
}
|
|
e.replicationProcessorMgr.Start()
|
|
|
|
// failover callback will try to create a failover queue processor to scan all inflight tasks
|
|
// if domain needs to be failovered. However, in the multicursor queue logic, the scan range
|
|
// can't be retrieved before the processor is started. If failover callback is registered
|
|
// before queue processor is started, it may result in a deadline as to create the failover queue,
|
|
// queue processor need to be started.
|
|
//
|
|
// Ideally, when both timer and transfer queues enabled single cursor mode, we don't have to register
|
|
// the callback. However, currently namespace migration is relying on the callback to UpdateHandoverNamespaces
|
|
e.registerNamespaceFailoverCallback()
|
|
}
|
|
|
|
// Stop the service.
|
|
func (e *historyEngineImpl) Stop() {
|
|
if !atomic.CompareAndSwapInt32(
|
|
&e.status,
|
|
common.DaemonStatusStarted,
|
|
common.DaemonStatusStopped,
|
|
) {
|
|
return
|
|
}
|
|
|
|
e.logger.Info("", tag.LifeCycleStopping)
|
|
defer e.logger.Info("", tag.LifeCycleStopped)
|
|
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.Stop()
|
|
}
|
|
e.replicationProcessorMgr.Stop()
|
|
// unset the failover callback
|
|
e.shard.GetNamespaceRegistry().UnregisterNamespaceChangeCallback(e)
|
|
}
|
|
|
|
func (e *historyEngineImpl) registerNamespaceFailoverCallback() {
|
|
|
|
// NOTE: READ BEFORE MODIFICATION
|
|
//
|
|
// Tasks, e.g. transfer tasks and timer tasks, are created when holding the shard lock
|
|
// meaning tasks -> release of shard lock
|
|
//
|
|
// Namespace change notification follows the following steps, order matters
|
|
// 1. lock all task processing.
|
|
// 2. namespace changes visible to everyone (Note: lock of task processing prevents task processing logic seeing the namespace changes).
|
|
// 3. failover min and max task levels are calculated, then update to shard.
|
|
// 4. failover start & task processing unlock & shard namespace version notification update. (order does not matter for this discussion)
|
|
//
|
|
// The above guarantees that task created during the failover will be processed.
|
|
// If the task is created after namespace change:
|
|
// then active processor will handle it. (simple case)
|
|
// If the task is created before namespace change:
|
|
// task -> release of shard lock
|
|
// failover min / max task levels calculated & updated to shard (using shard lock) -> failover start
|
|
// above 2 guarantees that failover start is after persistence of the task.
|
|
|
|
failoverPredicate := func(shardNotificationVersion int64, nextNamespace *namespace.Namespace, action func()) {
|
|
namespaceFailoverNotificationVersion := nextNamespace.FailoverNotificationVersion()
|
|
namespaceActiveCluster := nextNamespace.ActiveClusterName()
|
|
|
|
// +1 in the following check as the version in shard is max notification version +1.
|
|
// Need to run action() when namespaceFailoverNotificationVersion+1 == shardNotificationVersion
|
|
// as we don't know if the failover queue execution for that notification version is
|
|
// completed or not.
|
|
//
|
|
// NOTE: theoretically we need to get rid of the check on shardNotificationVersion, as
|
|
// we have no idea if the failover queue for any notification version below that is completed
|
|
// or not. However, removing that will cause more load upon shard reload.
|
|
// So here assume failover queue processor for notification version < X-1 is completed if
|
|
// shard notification version is X.
|
|
|
|
if nextNamespace.IsGlobalNamespace() &&
|
|
nextNamespace.ReplicationPolicy() == namespace.ReplicationPolicyMultiCluster &&
|
|
namespaceFailoverNotificationVersion+1 >= shardNotificationVersion &&
|
|
namespaceActiveCluster == e.currentClusterName {
|
|
action()
|
|
}
|
|
}
|
|
|
|
// first set the failover callback
|
|
e.shard.GetNamespaceRegistry().RegisterNamespaceChangeCallback(
|
|
e,
|
|
0, /* always want callback so UpdateHandoverNamespaces() can be called after shard reload */
|
|
func() {
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.LockTaskProcessing()
|
|
}
|
|
},
|
|
func(prevNamespaces []*namespace.Namespace, nextNamespaces []*namespace.Namespace) {
|
|
defer func() {
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.UnlockTaskProcessing()
|
|
}
|
|
}()
|
|
|
|
if len(nextNamespaces) == 0 {
|
|
return
|
|
}
|
|
|
|
if e.shard.GetClusterMetadata().IsGlobalNamespaceEnabled() {
|
|
maxTaskID, _ := e.replicationAckMgr.GetMaxTaskInfo()
|
|
e.shard.UpdateHandoverNamespaces(nextNamespaces, maxTaskID)
|
|
}
|
|
|
|
newNotificationVersion := nextNamespaces[len(nextNamespaces)-1].NotificationVersion() + 1
|
|
shardNotificationVersion := e.shard.GetNamespaceNotificationVersion()
|
|
|
|
// 1. We can't return when newNotificationVersion == shardNotificationVersion
|
|
// since we don't know if the previous failover queue processing has finished or not
|
|
// 2. We can return when newNotificationVersion < shardNotificationVersion. But the check
|
|
// is basically the same as the check in failover predicate. Because
|
|
// failoverNotificationVersion + 1 <= NotificationVersion + 1 = newNotificationVersion,
|
|
// there's no notification version can make
|
|
// newNotificationVersion < shardNotificationVersion and
|
|
// failoverNotificationVersion + 1 >= shardNotificationVersion are true at the same time
|
|
// Meaning if the check decides to return, no namespace will pass the failover predicate.
|
|
|
|
failoverNamespaceIDs := map[string]struct{}{}
|
|
for _, nextNamespace := range nextNamespaces {
|
|
failoverPredicate(shardNotificationVersion, nextNamespace, func() {
|
|
failoverNamespaceIDs[nextNamespace.ID().String()] = struct{}{}
|
|
})
|
|
}
|
|
|
|
if len(failoverNamespaceIDs) > 0 {
|
|
e.logger.Info("Namespace Failover Start.", tag.WorkflowNamespaceIDs(failoverNamespaceIDs))
|
|
|
|
for _, queueProcessor := range e.queueProcessors {
|
|
queueProcessor.FailoverNamespace(failoverNamespaceIDs)
|
|
}
|
|
|
|
// the fake tasks will not be actually used, we just need to make sure
|
|
// its length > 0 and has correct timestamp, to trigger a db scan
|
|
now := e.shard.GetTimeSource().Now()
|
|
fakeTasks := make(map[tasks.Category][]tasks.Task)
|
|
for category := range e.queueProcessors {
|
|
fakeTasks[category] = []tasks.Task{tasks.NewFakeTask(definition.WorkflowKey{}, category, now)}
|
|
}
|
|
e.NotifyNewTasks(e.currentClusterName, fakeTasks)
|
|
}
|
|
|
|
_ = e.shard.UpdateNamespaceNotificationVersion(newNotificationVersion)
|
|
},
|
|
)
|
|
}
|
|
|
|
// StartWorkflowExecution starts a workflow execution
|
|
// Consistency guarantee: always write
|
|
func (e *historyEngineImpl) StartWorkflowExecution(
|
|
ctx context.Context,
|
|
startRequest *historyservice.StartWorkflowExecutionRequest,
|
|
) (resp *historyservice.StartWorkflowExecutionResponse, retError error) {
|
|
return startworkflow.Invoke(ctx, startRequest, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// GetMutableState retrieves the mutable state of the workflow execution
|
|
func (e *historyEngineImpl) GetMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.GetMutableStateRequest,
|
|
) (*historyservice.GetMutableStateResponse, error) {
|
|
|
|
return e.getMutableStateOrPolling(ctx, request)
|
|
}
|
|
|
|
// PollMutableState retrieves the mutable state of the workflow execution with long polling
|
|
func (e *historyEngineImpl) PollMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.PollMutableStateRequest,
|
|
) (*historyservice.PollMutableStateResponse, error) {
|
|
|
|
response, err := e.getMutableStateOrPolling(ctx, &historyservice.GetMutableStateRequest{
|
|
NamespaceId: request.GetNamespaceId(),
|
|
Execution: request.Execution,
|
|
ExpectedNextEventId: request.ExpectedNextEventId,
|
|
CurrentBranchToken: request.CurrentBranchToken,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.PollMutableStateResponse{
|
|
Execution: response.Execution,
|
|
WorkflowType: response.WorkflowType,
|
|
NextEventId: response.NextEventId,
|
|
PreviousStartedEventId: response.PreviousStartedEventId,
|
|
LastFirstEventId: response.LastFirstEventId,
|
|
LastFirstEventTxnId: response.LastFirstEventTxnId,
|
|
TaskQueue: response.TaskQueue,
|
|
StickyTaskQueue: response.StickyTaskQueue,
|
|
StickyTaskQueueScheduleToStartTimeout: response.StickyTaskQueueScheduleToStartTimeout,
|
|
CurrentBranchToken: response.CurrentBranchToken,
|
|
VersionHistories: response.VersionHistories,
|
|
WorkflowState: response.WorkflowState,
|
|
WorkflowStatus: response.WorkflowStatus,
|
|
FirstExecutionRunId: response.FirstExecutionRunId,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) getMutableStateOrPolling(
|
|
ctx context.Context,
|
|
request *historyservice.GetMutableStateRequest,
|
|
) (*historyservice.GetMutableStateResponse, error) {
|
|
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
err := api.ValidateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(request.Execution.RunId) == 0 {
|
|
request.Execution.RunId, err = e.workflowConsistencyChecker.GetCurrentRunID(
|
|
ctx,
|
|
request.NamespaceId,
|
|
request.Execution.WorkflowId,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
workflowKey := definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Execution.WorkflowId,
|
|
request.Execution.RunId,
|
|
)
|
|
response, err := e.getMutableState(ctx, workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if request.CurrentBranchToken == nil {
|
|
request.CurrentBranchToken = response.CurrentBranchToken
|
|
}
|
|
if !bytes.Equal(request.CurrentBranchToken, response.CurrentBranchToken) {
|
|
return nil, serviceerrors.NewCurrentBranchChanged(response.CurrentBranchToken, request.CurrentBranchToken)
|
|
}
|
|
|
|
// expectedNextEventID is 0 when caller want to get the current next event ID without blocking
|
|
expectedNextEventID := common.FirstEventID
|
|
if request.ExpectedNextEventId != common.EmptyEventID {
|
|
expectedNextEventID = request.GetExpectedNextEventId()
|
|
}
|
|
|
|
// if caller decide to long poll on workflow execution
|
|
// and the event ID we are looking for is smaller than current next event ID
|
|
if expectedNextEventID >= response.GetNextEventId() && response.GetWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
subscriberID, channel, err := e.eventNotifier.WatchHistoryEvent(workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = e.eventNotifier.UnwatchHistoryEvent(workflowKey, subscriberID) }()
|
|
// check again in case the next event ID is updated
|
|
response, err = e.getMutableState(ctx, workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// check again if the current branch token changed
|
|
if !bytes.Equal(request.CurrentBranchToken, response.CurrentBranchToken) {
|
|
return nil, serviceerrors.NewCurrentBranchChanged(response.CurrentBranchToken, request.CurrentBranchToken)
|
|
}
|
|
if expectedNextEventID < response.GetNextEventId() || response.GetWorkflowStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
return response, nil
|
|
}
|
|
|
|
namespaceRegistry, err := e.shard.GetNamespaceRegistry().GetNamespaceByID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timer := time.NewTimer(e.shard.GetConfig().LongPollExpirationInterval(namespaceRegistry.Name().String()))
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case event := <-channel:
|
|
response.LastFirstEventId = event.LastFirstEventID
|
|
response.LastFirstEventTxnId = event.LastFirstEventTxnID
|
|
response.NextEventId = event.NextEventID
|
|
response.PreviousStartedEventId = event.PreviousStartedEventID
|
|
response.WorkflowState = event.WorkflowState
|
|
response.WorkflowStatus = event.WorkflowStatus
|
|
if !bytes.Equal(request.CurrentBranchToken, event.CurrentBranchToken) {
|
|
return nil, serviceerrors.NewCurrentBranchChanged(event.CurrentBranchToken, request.CurrentBranchToken)
|
|
}
|
|
if expectedNextEventID < response.GetNextEventId() || response.GetWorkflowStatus() != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
return response, nil
|
|
}
|
|
case <-timer.C:
|
|
return response, nil
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) QueryWorkflow(
|
|
ctx context.Context,
|
|
request *historyservice.QueryWorkflowRequest,
|
|
) (_ *historyservice.QueryWorkflowResponse, retErr error) {
|
|
|
|
scope := e.metricsClient.Scope(metrics.HistoryQueryWorkflowScope)
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
err := api.ValidateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nsEntry, err := e.shard.GetNamespaceRegistry().GetNamespaceByID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(request.Request.Execution.RunId) == 0 {
|
|
request.Request.Execution.RunId, err = e.workflowConsistencyChecker.GetCurrentRunID(
|
|
ctx,
|
|
request.NamespaceId,
|
|
request.Request.Execution.WorkflowId,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
workflowKey := definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Request.Execution.WorkflowId,
|
|
request.Request.Execution.RunId,
|
|
)
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
workflowKey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retErr) }()
|
|
|
|
req := request.GetRequest()
|
|
_, mutableStateStatus := weCtx.GetMutableState().GetWorkflowStateStatus()
|
|
if mutableStateStatus != enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING && req.QueryRejectCondition != enumspb.QUERY_REJECT_CONDITION_NONE {
|
|
notOpenReject := req.GetQueryRejectCondition() == enumspb.QUERY_REJECT_CONDITION_NOT_OPEN
|
|
notCompletedCleanlyReject := req.GetQueryRejectCondition() == enumspb.QUERY_REJECT_CONDITION_NOT_COMPLETED_CLEANLY && mutableStateStatus != enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED
|
|
if notOpenReject || notCompletedCleanlyReject {
|
|
return &historyservice.QueryWorkflowResponse{
|
|
Response: &workflowservice.QueryWorkflowResponse{
|
|
QueryRejected: &querypb.QueryRejected{
|
|
Status: mutableStateStatus,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
mutableState := weCtx.GetMutableState()
|
|
if !mutableState.HasProcessedOrPendingWorkflowTask() {
|
|
// workflow has no workflow task ever scheduled, this usually is due to firstWorkflowTaskBackoff (cron / retry)
|
|
// in this case, don't buffer the query, because it is almost certain the query will time out.
|
|
return nil, consts.ErrWorkflowTaskNotScheduled
|
|
}
|
|
|
|
// There are two ways in which queries get dispatched to workflow worker. First, queries can be dispatched on workflow tasks.
|
|
// These workflow tasks potentially contain new events and queries. The events are treated as coming before the query in time.
|
|
// The second way in which queries are dispatched to workflow worker is directly through matching; in this approach queries can be
|
|
// dispatched to workflow worker immediately even if there are outstanding events that came before the query. The following logic
|
|
// is used to determine if a query can be safely dispatched directly through matching or must be dispatched on a workflow task.
|
|
//
|
|
// Precondition to dispatch query directly to matching is workflow has at least one WorkflowTaskStarted event. Otherwise, sdk would panic.
|
|
if mutableState.GetPreviousStartedEventID() != common.EmptyEventID {
|
|
// There are three cases in which a query can be dispatched directly through matching safely, without violating strong consistency level:
|
|
// 1. the namespace is not active, in this case history is immutable so a query dispatched at any time is consistent
|
|
// 2. the workflow is not running, whenever a workflow is not running dispatching query directly is consistent
|
|
// 3. if there is no pending or started workflow tasks it means no events came before query arrived, so its safe to dispatch directly
|
|
safeToDispatchDirectly := !nsEntry.ActiveInCluster(e.clusterMetadata.GetCurrentClusterName()) ||
|
|
!mutableState.IsWorkflowExecutionRunning() ||
|
|
(!mutableState.HasPendingWorkflowTask() && !mutableState.HasInFlightWorkflowTask())
|
|
if safeToDispatchDirectly {
|
|
msResp, err := e.mutableStateToGetResponse(mutableState)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
weCtx.GetReleaseFn()(nil)
|
|
req.Execution.RunId = msResp.Execution.RunId
|
|
return e.queryDirectlyThroughMatching(ctx, msResp, request.GetNamespaceId(), req, scope)
|
|
}
|
|
}
|
|
|
|
// If we get here it means query could not be dispatched through matching directly, so it must block
|
|
// until either an result has been obtained on a workflow task response or until it is safe to dispatch directly through matching.
|
|
sw := scope.StartTimer(metrics.WorkflowTaskQueryLatency)
|
|
defer sw.Stop()
|
|
queryReg := mutableState.GetQueryRegistry()
|
|
if len(queryReg.GetBufferedIDs()) >= e.config.MaxBufferedQueryCount() {
|
|
scope.IncCounter(metrics.QueryBufferExceededCount)
|
|
return nil, consts.ErrConsistentQueryBufferExceeded
|
|
}
|
|
queryID, completionCh := queryReg.BufferQuery(req.GetQuery())
|
|
defer queryReg.RemoveQuery(queryID)
|
|
weCtx.GetReleaseFn()(nil)
|
|
select {
|
|
case <-completionCh:
|
|
completionState, err := queryReg.GetCompletionState(queryID)
|
|
if err != nil {
|
|
scope.IncCounter(metrics.QueryRegistryInvalidStateCount)
|
|
return nil, err
|
|
}
|
|
switch completionState.Type {
|
|
case workflow.QueryCompletionTypeSucceeded:
|
|
result := completionState.Result
|
|
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.QueryCompletionTypeUnblocked:
|
|
msResp, err := e.getMutableState(ctx, workflowKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Execution.RunId = msResp.Execution.RunId
|
|
return e.queryDirectlyThroughMatching(ctx, msResp, request.GetNamespaceId(), req, scope)
|
|
case workflow.QueryCompletionTypeFailed:
|
|
return nil, completionState.Err
|
|
default:
|
|
scope.IncCounter(metrics.QueryRegistryInvalidStateCount)
|
|
return nil, consts.ErrQueryEnteredInvalidState
|
|
}
|
|
case <-ctx.Done():
|
|
scope.IncCounter(metrics.ConsistentQueryTimeoutCount)
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (e *historyEngineImpl) queryDirectlyThroughMatching(
|
|
ctx context.Context,
|
|
msResp *historyservice.GetMutableStateResponse,
|
|
namespaceID string,
|
|
queryRequest *workflowservice.QueryWorkflowRequest,
|
|
scope metrics.Scope,
|
|
) (*historyservice.QueryWorkflowResponse, error) {
|
|
|
|
sw := scope.StartTimer(metrics.DirectQueryDispatchLatency)
|
|
defer sw.Stop()
|
|
|
|
if msResp.GetIsStickyTaskQueueEnabled() &&
|
|
len(msResp.GetStickyTaskQueue().GetName()) != 0 &&
|
|
e.config.EnableStickyQuery(queryRequest.GetNamespace()) {
|
|
|
|
stickyMatchingRequest := &matchingservice.QueryWorkflowRequest{
|
|
NamespaceId: namespaceID,
|
|
QueryRequest: queryRequest,
|
|
TaskQueue: msResp.GetStickyTaskQueue(),
|
|
}
|
|
|
|
// using a clean new context in case customer provide a context which has
|
|
// a really short deadline, causing we clear the stickiness
|
|
stickyContext, cancel := context.WithTimeout(context.Background(), timestamp.DurationValue(msResp.GetStickyTaskQueueScheduleToStartTimeout()))
|
|
stickyStopWatch := scope.StartTimer(metrics.DirectQueryDispatchStickyLatency)
|
|
matchingResp, err := e.rawMatchingClient.QueryWorkflow(stickyContext, stickyMatchingRequest)
|
|
stickyStopWatch.Stop()
|
|
cancel()
|
|
if err == nil {
|
|
scope.IncCounter(metrics.DirectQueryDispatchStickySuccessCount)
|
|
return &historyservice.QueryWorkflowResponse{
|
|
Response: &workflowservice.QueryWorkflowResponse{
|
|
QueryResult: matchingResp.GetQueryResult(),
|
|
QueryRejected: matchingResp.GetQueryRejected(),
|
|
}}, nil
|
|
}
|
|
if !common.IsContextDeadlineExceededErr(err) && !common.IsContextCanceledErr(err) && !common.IsStickyWorkerUnavailable(err) {
|
|
return nil, err
|
|
}
|
|
if msResp.GetWorkflowStatus() == enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
|
resetContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
clearStickinessStopWatch := scope.StartTimer(metrics.DirectQueryDispatchClearStickinessLatency)
|
|
_, err := e.ResetStickyTaskQueue(resetContext, &historyservice.ResetStickyTaskQueueRequest{
|
|
NamespaceId: namespaceID,
|
|
Execution: queryRequest.GetExecution(),
|
|
})
|
|
clearStickinessStopWatch.Stop()
|
|
cancel()
|
|
if err != nil && err != consts.ErrWorkflowCompleted {
|
|
return nil, err
|
|
}
|
|
scope.IncCounter(metrics.DirectQueryDispatchClearStickinessSuccessCount)
|
|
}
|
|
}
|
|
|
|
if err := common.IsValidContext(ctx); err != nil {
|
|
scope.IncCounter(metrics.DirectQueryDispatchTimeoutBeforeNonStickyCount)
|
|
return nil, err
|
|
}
|
|
|
|
nonStickyMatchingRequest := &matchingservice.QueryWorkflowRequest{
|
|
NamespaceId: namespaceID,
|
|
QueryRequest: queryRequest,
|
|
TaskQueue: msResp.TaskQueue,
|
|
}
|
|
|
|
nonStickyStopWatch := scope.StartTimer(metrics.DirectQueryDispatchNonStickyLatency)
|
|
matchingResp, err := e.matchingClient.QueryWorkflow(ctx, nonStickyMatchingRequest)
|
|
nonStickyStopWatch.Stop()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scope.IncCounter(metrics.DirectQueryDispatchNonStickySuccessCount)
|
|
return &historyservice.QueryWorkflowResponse{
|
|
Response: &workflowservice.QueryWorkflowResponse{
|
|
QueryResult: matchingResp.GetQueryResult(),
|
|
QueryRejected: matchingResp.GetQueryRejected(),
|
|
}}, err
|
|
}
|
|
|
|
func (e *historyEngineImpl) getMutableState(
|
|
ctx context.Context,
|
|
workflowKey definition.WorkflowKey,
|
|
) (_ *historyservice.GetMutableStateResponse, retError error) {
|
|
|
|
if len(workflowKey.RunID) == 0 {
|
|
return nil, serviceerror.NewInternal(fmt.Sprintf(
|
|
"getMutableState encountered empty run ID: %v", workflowKey,
|
|
))
|
|
}
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
workflowKey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
mutableState, err := weCtx.GetContext().LoadMutableState(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return e.mutableStateToGetResponse(mutableState)
|
|
}
|
|
|
|
func (e *historyEngineImpl) mutableStateToGetResponse(
|
|
mutableState workflow.MutableState,
|
|
) (_ *historyservice.GetMutableStateResponse, retError error) {
|
|
currentBranchToken, err := mutableState.GetCurrentBranchToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
executionInfo := mutableState.GetExecutionInfo()
|
|
workflowState, workflowStatus := mutableState.GetWorkflowStateStatus()
|
|
lastFirstEventID, lastFirstEventTxnID := mutableState.GetLastFirstEventIDTxnID()
|
|
return &historyservice.GetMutableStateResponse{
|
|
Execution: &commonpb.WorkflowExecution{
|
|
WorkflowId: mutableState.GetExecutionInfo().WorkflowId,
|
|
RunId: mutableState.GetExecutionState().RunId,
|
|
},
|
|
WorkflowType: &commonpb.WorkflowType{Name: executionInfo.WorkflowTypeName},
|
|
LastFirstEventId: lastFirstEventID,
|
|
LastFirstEventTxnId: lastFirstEventTxnID,
|
|
NextEventId: mutableState.GetNextEventID(),
|
|
PreviousStartedEventId: mutableState.GetPreviousStartedEventID(),
|
|
TaskQueue: &taskqueuepb.TaskQueue{
|
|
Name: executionInfo.TaskQueue,
|
|
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
|
|
},
|
|
StickyTaskQueue: &taskqueuepb.TaskQueue{
|
|
Name: executionInfo.StickyTaskQueue,
|
|
Kind: enumspb.TASK_QUEUE_KIND_STICKY,
|
|
},
|
|
StickyTaskQueueScheduleToStartTimeout: executionInfo.StickyScheduleToStartTimeout,
|
|
CurrentBranchToken: currentBranchToken,
|
|
WorkflowState: workflowState,
|
|
WorkflowStatus: workflowStatus,
|
|
IsStickyTaskQueueEnabled: mutableState.IsStickyTaskQueueEnabled(),
|
|
VersionHistories: versionhistory.CopyVersionHistories(
|
|
mutableState.GetExecutionInfo().GetVersionHistories(),
|
|
),
|
|
FirstExecutionRunId: executionInfo.FirstExecutionRunId,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) DescribeMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.DescribeMutableStateRequest,
|
|
) (response *historyservice.DescribeMutableStateResponse, retError error) {
|
|
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
err := api.ValidateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Execution.WorkflowId,
|
|
request.Execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
response = &historyservice.DescribeMutableStateResponse{}
|
|
|
|
if weCtx.GetContext().(*workflow.ContextImpl).MutableState != nil {
|
|
msb := weCtx.GetContext().(*workflow.ContextImpl).MutableState
|
|
response.CacheMutableState = msb.CloneToProto()
|
|
}
|
|
|
|
// clear mutable state to force reload from persistence. This API returns both cached and persisted version.
|
|
weCtx.GetContext().Clear()
|
|
mutableState, err := weCtx.GetContext().LoadMutableState(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) {
|
|
return resetstickytaskqueue.Invoke(ctx, resetRequest, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// 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 := api.ValidateNamespaceUUID(namespaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.Request.Execution.WorkflowId,
|
|
request.Request.Execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
mutableState := weCtx.GetMutableState()
|
|
executionInfo := mutableState.GetExecutionInfo()
|
|
executionState := mutableState.GetExecutionState()
|
|
result := &historyservice.DescribeWorkflowExecutionResponse{
|
|
ExecutionConfig: &workflowpb.WorkflowExecutionConfig{
|
|
TaskQueue: &taskqueuepb.TaskQueue{
|
|
Name: executionInfo.TaskQueue,
|
|
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
|
|
},
|
|
WorkflowExecutionTimeout: executionInfo.WorkflowExecutionTimeout,
|
|
WorkflowRunTimeout: executionInfo.WorkflowRunTimeout,
|
|
DefaultWorkflowTaskTimeout: executionInfo.DefaultWorkflowTaskTimeout,
|
|
},
|
|
WorkflowExecutionInfo: &workflowpb.WorkflowExecutionInfo{
|
|
Execution: &commonpb.WorkflowExecution{
|
|
WorkflowId: executionInfo.WorkflowId,
|
|
RunId: executionState.RunId,
|
|
},
|
|
Type: &commonpb.WorkflowType{Name: executionInfo.WorkflowTypeName},
|
|
StartTime: executionInfo.StartTime,
|
|
Status: executionState.Status,
|
|
HistoryLength: mutableState.GetNextEventID() - common.FirstEventID,
|
|
ExecutionTime: executionInfo.ExecutionTime,
|
|
Memo: &commonpb.Memo{Fields: executionInfo.Memo},
|
|
SearchAttributes: &commonpb.SearchAttributes{IndexedFields: executionInfo.SearchAttributes},
|
|
AutoResetPoints: executionInfo.AutoResetPoints,
|
|
TaskQueue: executionInfo.TaskQueue,
|
|
StateTransitionCount: executionInfo.StateTransitionCount,
|
|
},
|
|
}
|
|
|
|
if executionInfo.ParentRunId != "" {
|
|
result.WorkflowExecutionInfo.ParentExecution = &commonpb.WorkflowExecution{
|
|
WorkflowId: executionInfo.ParentWorkflowId,
|
|
RunId: executionInfo.ParentRunId,
|
|
}
|
|
result.WorkflowExecutionInfo.ParentNamespaceId = executionInfo.ParentNamespaceId
|
|
}
|
|
if executionState.State == enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED {
|
|
// for closed workflow
|
|
result.WorkflowExecutionInfo.Status = executionState.Status
|
|
closeTime, err := mutableState.GetWorkflowCloseTime(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.WorkflowExecutionInfo.CloseTime = closeTime
|
|
}
|
|
|
|
if len(mutableState.GetPendingActivityInfos()) > 0 {
|
|
for _, ai := range mutableState.GetPendingActivityInfos() {
|
|
p := &workflowpb.PendingActivityInfo{
|
|
ActivityId: ai.ActivityId,
|
|
}
|
|
if ai.CancelRequested {
|
|
p.State = enumspb.PENDING_ACTIVITY_STATE_CANCEL_REQUESTED
|
|
} else if ai.StartedEventId != common.EmptyEventID {
|
|
p.State = enumspb.PENDING_ACTIVITY_STATE_STARTED
|
|
} else {
|
|
p.State = enumspb.PENDING_ACTIVITY_STATE_SCHEDULED
|
|
}
|
|
if !timestamp.TimeValue(ai.LastHeartbeatUpdateTime).IsZero() {
|
|
p.LastHeartbeatTime = ai.LastHeartbeatUpdateTime
|
|
p.HeartbeatDetails = ai.LastHeartbeatDetails
|
|
}
|
|
// TODO: move to mutable state instead of loading it from event
|
|
scheduledEvent, err := mutableState.GetActivityScheduledEvent(ctx, ai.ScheduledEventId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.ActivityType = scheduledEvent.GetActivityTaskScheduledEventAttributes().ActivityType
|
|
if p.State == enumspb.PENDING_ACTIVITY_STATE_SCHEDULED {
|
|
p.ScheduledTime = ai.ScheduledTime
|
|
} else {
|
|
p.LastStartedTime = ai.StartedTime
|
|
}
|
|
p.LastWorkerIdentity = ai.StartedIdentity
|
|
if ai.HasRetryPolicy {
|
|
p.Attempt = ai.Attempt
|
|
p.ExpirationTime = ai.RetryExpirationTime
|
|
if ai.RetryMaximumAttempts != 0 {
|
|
p.MaximumAttempts = ai.RetryMaximumAttempts
|
|
}
|
|
if ai.RetryLastFailure != nil {
|
|
p.LastFailure = ai.RetryLastFailure
|
|
}
|
|
if p.LastWorkerIdentity == "" && ai.RetryLastWorkerIdentity != "" {
|
|
p.LastWorkerIdentity = ai.RetryLastWorkerIdentity
|
|
}
|
|
} else {
|
|
p.Attempt = 1
|
|
}
|
|
result.PendingActivities = append(result.PendingActivities, p)
|
|
}
|
|
}
|
|
|
|
if len(mutableState.GetPendingChildExecutionInfos()) > 0 {
|
|
for _, ch := range mutableState.GetPendingChildExecutionInfos() {
|
|
p := &workflowpb.PendingChildExecutionInfo{
|
|
WorkflowId: ch.StartedWorkflowId,
|
|
RunId: ch.StartedRunId,
|
|
WorkflowTypeName: ch.WorkflowTypeName,
|
|
InitiatedId: ch.InitiatedEventId,
|
|
ParentClosePolicy: ch.ParentClosePolicy,
|
|
}
|
|
result.PendingChildren = append(result.PendingChildren, p)
|
|
}
|
|
}
|
|
|
|
if pendingWorkflowTask, ok := mutableState.GetPendingWorkflowTask(); ok {
|
|
result.PendingWorkflowTask = &workflowpb.PendingWorkflowTaskInfo{
|
|
State: enumspb.PENDING_WORKFLOW_TASK_STATE_SCHEDULED,
|
|
ScheduledTime: pendingWorkflowTask.ScheduledTime,
|
|
OriginalScheduledTime: pendingWorkflowTask.OriginalScheduledTime,
|
|
Attempt: pendingWorkflowTask.Attempt,
|
|
}
|
|
if pendingWorkflowTask.StartedEventID != common.EmptyEventID {
|
|
result.PendingWorkflowTask.State = enumspb.PENDING_WORKFLOW_TASK_STATE_STARTED
|
|
result.PendingWorkflowTask.StartedTime = pendingWorkflowTask.StartedTime
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) RecordActivityTaskStarted(
|
|
ctx context.Context,
|
|
request *historyservice.RecordActivityTaskStartedRequest,
|
|
) (*historyservice.RecordActivityTaskStartedResponse, error) {
|
|
return recordactivitytaskstarted.Invoke(ctx, request, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// ScheduleWorkflowTask schedules a workflow task if no outstanding workflow task found
|
|
func (e *historyEngineImpl) ScheduleWorkflowTask(
|
|
ctx context.Context,
|
|
req *historyservice.ScheduleWorkflowTaskRequest,
|
|
) error {
|
|
return e.workflowTaskHandler.handleWorkflowTaskScheduled(ctx, req)
|
|
}
|
|
|
|
func (e *historyEngineImpl) VerifyFirstWorkflowTaskScheduled(
|
|
ctx context.Context,
|
|
request *historyservice.VerifyFirstWorkflowTaskScheduledRequest,
|
|
) (retError error) {
|
|
return e.workflowTaskHandler.verifyFirstWorkflowTaskScheduled(ctx, request)
|
|
}
|
|
|
|
// RecordWorkflowTaskStarted starts a workflow task
|
|
func (e *historyEngineImpl) RecordWorkflowTaskStarted(
|
|
ctx context.Context,
|
|
request *historyservice.RecordWorkflowTaskStartedRequest,
|
|
) (*historyservice.RecordWorkflowTaskStartedResponse, error) {
|
|
return e.workflowTaskHandler.handleWorkflowTaskStarted(ctx, request)
|
|
}
|
|
|
|
// RespondWorkflowTaskCompleted completes a workflow task
|
|
func (e *historyEngineImpl) RespondWorkflowTaskCompleted(
|
|
ctx context.Context,
|
|
req *historyservice.RespondWorkflowTaskCompletedRequest,
|
|
) (*historyservice.RespondWorkflowTaskCompletedResponse, error) {
|
|
return e.workflowTaskHandler.handleWorkflowTaskCompleted(ctx, req)
|
|
}
|
|
|
|
// RespondWorkflowTaskFailed fails a workflow task
|
|
func (e *historyEngineImpl) RespondWorkflowTaskFailed(
|
|
ctx context.Context,
|
|
req *historyservice.RespondWorkflowTaskFailedRequest,
|
|
) error {
|
|
return e.workflowTaskHandler.handleWorkflowTaskFailed(ctx, req)
|
|
}
|
|
|
|
// RespondActivityTaskCompleted completes an activity task.
|
|
func (e *historyEngineImpl) RespondActivityTaskCompleted(
|
|
ctx context.Context,
|
|
req *historyservice.RespondActivityTaskCompletedRequest,
|
|
) (*historyservice.RespondActivityTaskCompletedResponse, error) {
|
|
return respondactivitytaskcompleted.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// RespondActivityTaskFailed completes an activity task failure.
|
|
func (e *historyEngineImpl) RespondActivityTaskFailed(
|
|
ctx context.Context,
|
|
req *historyservice.RespondActivityTaskFailedRequest,
|
|
) (*historyservice.RespondActivityTaskFailedResponse, error) {
|
|
return respondactivitytaskfailed.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// RespondActivityTaskCanceled completes an activity task failure.
|
|
func (e *historyEngineImpl) RespondActivityTaskCanceled(
|
|
ctx context.Context,
|
|
req *historyservice.RespondActivityTaskCanceledRequest,
|
|
) (*historyservice.RespondActivityTaskCanceledResponse, error) {
|
|
return respondactivitytaskcandeled.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// 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) {
|
|
return recordactivitytaskheartbeat.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// RequestCancelWorkflowExecution records request cancellation event for workflow execution
|
|
func (e *historyEngineImpl) RequestCancelWorkflowExecution(
|
|
ctx context.Context,
|
|
req *historyservice.RequestCancelWorkflowExecutionRequest,
|
|
) (resp *historyservice.RequestCancelWorkflowExecutionResponse, retError error) {
|
|
return requestcancelworkflow.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
func (e *historyEngineImpl) SignalWorkflowExecution(
|
|
ctx context.Context,
|
|
req *historyservice.SignalWorkflowExecutionRequest,
|
|
) (resp *historyservice.SignalWorkflowExecutionResponse, retError error) {
|
|
return signalworkflow.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
// SignalWithStartWorkflowExecution signals current workflow (if running) or creates & signals a new workflow
|
|
// Consistency guarantee: always write
|
|
func (e *historyEngineImpl) SignalWithStartWorkflowExecution(
|
|
ctx context.Context,
|
|
req *historyservice.SignalWithStartWorkflowExecutionRequest,
|
|
) (_ *historyservice.SignalWithStartWorkflowExecutionResponse, retError error) {
|
|
return signalwithstartworkflow.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
func (h *historyEngineImpl) UpdateWorkflow(
|
|
ctx context.Context,
|
|
request *historyservice.UpdateWorkflowRequest,
|
|
) (*historyservice.UpdateWorkflowResponse, error) {
|
|
return nil, serviceerror.NewUnimplemented("UpdateWorkflow is not supported on this server")
|
|
}
|
|
|
|
// RemoveSignalMutableState remove the signal request id in signal_requested for deduplicate
|
|
func (e *historyEngineImpl) RemoveSignalMutableState(
|
|
ctx context.Context,
|
|
request *historyservice.RemoveSignalMutableStateRequest,
|
|
) error {
|
|
|
|
_, err := e.getActiveNamespaceEntry(namespace.ID(request.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return api.GetAndUpdateWorkflowWithNew(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.WorkflowExecution.WorkflowId,
|
|
request.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
mutableState.DeleteSignalRequested(request.GetRequestId())
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
},
|
|
nil,
|
|
e.shard,
|
|
e.workflowConsistencyChecker,
|
|
)
|
|
}
|
|
|
|
func (e *historyEngineImpl) TerminateWorkflowExecution(
|
|
ctx context.Context,
|
|
req *historyservice.TerminateWorkflowExecutionRequest,
|
|
) (*historyservice.TerminateWorkflowExecutionResponse, error) {
|
|
return terminateworkflow.Invoke(ctx, req, e.shard, e.workflowConsistencyChecker)
|
|
}
|
|
|
|
func (e *historyEngineImpl) DeleteWorkflowExecution(
|
|
ctx context.Context,
|
|
request *historyservice.DeleteWorkflowExecutionRequest,
|
|
) (retError error) {
|
|
|
|
weCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.WorkflowExecution.WorkflowId,
|
|
request.WorkflowExecution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { weCtx.GetReleaseFn()(retError) }()
|
|
|
|
// Open and Close workflow executions are deleted differently.
|
|
// Open workflow execution is deleted by terminating with special flag `deleteAfterTerminate` set to true.
|
|
// This flag will be carried over with CloseExecutionTask and workflow will be deleted as the last step while processing the task.
|
|
//
|
|
// Close workflow execution is deleted using DeleteExecutionTask.
|
|
//
|
|
// DeleteWorkflowExecution is not replicated automatically. Workflow executions must be deleted separately in each cluster.
|
|
// Although running workflows in active cluster are terminated first and the termination event might be replicated.
|
|
// In passive cluster, workflow executions are just deleted in regardless of its state.
|
|
|
|
if weCtx.GetMutableState().IsWorkflowExecutionRunning() {
|
|
if request.GetClosedWorkflowOnly() {
|
|
// skip delete open workflow
|
|
return nil
|
|
}
|
|
ns, err := e.shard.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(request.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ns.ActiveInCluster(e.shard.GetClusterMetadata().GetCurrentClusterName()) {
|
|
// If workflow execution is running and in active cluster.
|
|
return api.UpdateWorkflowWithNew(
|
|
e.shard,
|
|
ctx,
|
|
weCtx,
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
eventBatchFirstEventID := mutableState.GetNextEventID()
|
|
|
|
return api.UpdateWorkflowWithoutWorkflowTask, workflow.TerminateWorkflow(
|
|
mutableState,
|
|
eventBatchFirstEventID,
|
|
"Delete workflow execution",
|
|
nil,
|
|
consts.IdentityHistoryService,
|
|
true,
|
|
)
|
|
},
|
|
nil,
|
|
)
|
|
}
|
|
}
|
|
|
|
// If workflow execution is closed or in passive cluster.
|
|
return e.workflowDeleteManager.AddDeleteWorkflowExecutionTask(
|
|
ctx,
|
|
namespace.ID(request.GetNamespaceId()),
|
|
commonpb.WorkflowExecution{
|
|
WorkflowId: request.GetWorkflowExecution().GetWorkflowId(),
|
|
RunId: request.GetWorkflowExecution().GetRunId(),
|
|
},
|
|
weCtx.GetMutableState(),
|
|
request.GetWorkflowVersion(),
|
|
)
|
|
}
|
|
|
|
// RecordChildExecutionCompleted records the completion of child execution into parent execution history
|
|
func (e *historyEngineImpl) RecordChildExecutionCompleted(
|
|
ctx context.Context,
|
|
completionRequest *historyservice.RecordChildExecutionCompletedRequest,
|
|
) error {
|
|
|
|
_, err := e.getActiveNamespaceEntry(namespace.ID(completionRequest.GetNamespaceId()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
parentInitiatedID := completionRequest.ParentInitiatedId
|
|
parentInitiatedVersion := completionRequest.ParentInitiatedVersion
|
|
|
|
return api.GetAndUpdateWorkflowWithNew(
|
|
ctx,
|
|
completionRequest.Clock,
|
|
func(mutableState workflow.MutableState) bool {
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
// current branch already closed, we won't perform any operation, pass the check
|
|
return true
|
|
}
|
|
|
|
onCurrentBranch, err := historyEventOnCurrentBranch(mutableState, parentInitiatedID, parentInitiatedVersion)
|
|
if err != nil {
|
|
// can't find initiated event, potential stale mutable, fail the predicate check
|
|
return false
|
|
}
|
|
if !onCurrentBranch {
|
|
// found on different branch, since we don't record completion on a different branch, pass the check
|
|
return true
|
|
}
|
|
|
|
ci, isRunning := mutableState.GetChildExecutionInfo(parentInitiatedID)
|
|
return !(isRunning && ci.StartedEventId == common.EmptyEventID) // !(potential stale)
|
|
},
|
|
definition.NewWorkflowKey(
|
|
completionRequest.NamespaceId,
|
|
completionRequest.WorkflowExecution.WorkflowId,
|
|
completionRequest.WorkflowExecution.RunId,
|
|
),
|
|
func(workflowContext api.WorkflowContext) (*api.UpdateWorkflowAction, error) {
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
return nil, consts.ErrWorkflowCompleted
|
|
}
|
|
|
|
onCurrentBranch, err := historyEventOnCurrentBranch(mutableState, parentInitiatedID, parentInitiatedVersion)
|
|
if err != nil || !onCurrentBranch {
|
|
return nil, consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
// Check mutable state to make sure child execution is in pending child executions
|
|
ci, isRunning := mutableState.GetChildExecutionInfo(parentInitiatedID)
|
|
if !isRunning || ci.StartedEventId == common.EmptyEventID {
|
|
// note we already checked if startedEventID is empty (in consistency predicate)
|
|
// and reloaded mutable state
|
|
return nil, consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
completedExecution := completionRequest.CompletedExecution
|
|
if ci.GetStartedWorkflowId() != completedExecution.GetWorkflowId() {
|
|
// this can only happen when we don't have the initiated version
|
|
return nil, consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
completionEvent := completionRequest.CompletionEvent
|
|
switch completionEvent.GetEventType() {
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED:
|
|
attributes := completionEvent.GetWorkflowExecutionCompletedEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionCompletedEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_FAILED:
|
|
attributes := completionEvent.GetWorkflowExecutionFailedEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionFailedEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED:
|
|
attributes := completionEvent.GetWorkflowExecutionCanceledEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionCanceledEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TERMINATED:
|
|
attributes := completionEvent.GetWorkflowExecutionTerminatedEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionTerminatedEvent(parentInitiatedID, completedExecution, attributes)
|
|
case enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_TIMED_OUT:
|
|
attributes := completionEvent.GetWorkflowExecutionTimedOutEventAttributes()
|
|
_, err = mutableState.AddChildWorkflowExecutionTimedOutEvent(parentInitiatedID, completedExecution, attributes)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: true,
|
|
}, nil
|
|
},
|
|
nil,
|
|
e.shard,
|
|
e.workflowConsistencyChecker,
|
|
)
|
|
}
|
|
|
|
func (e *historyEngineImpl) VerifyChildExecutionCompletionRecorded(
|
|
ctx context.Context,
|
|
request *historyservice.VerifyChildExecutionCompletionRecordedRequest,
|
|
) (retError error) {
|
|
namespaceID := namespace.ID(request.GetNamespaceId())
|
|
if err := api.ValidateNamespaceUUID(namespaceID); err != nil {
|
|
return err
|
|
}
|
|
|
|
workflowContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
request.Clock,
|
|
// it's ok we have stale state when doing verification,
|
|
// the logic will return WorkflowNotReady error and the caller will retry
|
|
// this can prevent keep reloading mutable state when there's a replication lag
|
|
// in parent shard.
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
request.NamespaceId,
|
|
request.ParentExecution.WorkflowId,
|
|
request.ParentExecution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { workflowContext.GetReleaseFn()(retError) }()
|
|
|
|
mutableState := workflowContext.GetMutableState()
|
|
if !mutableState.IsWorkflowExecutionRunning() &&
|
|
mutableState.GetExecutionState().State != enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE {
|
|
// parent has already completed and can't be blocked after failover.
|
|
return nil
|
|
}
|
|
|
|
onCurrentBranch, err := historyEventOnCurrentBranch(mutableState, request.ParentInitiatedId, request.ParentInitiatedVersion)
|
|
if err != nil {
|
|
// initiated event not found on any branch
|
|
return consts.ErrWorkflowNotReady
|
|
}
|
|
|
|
if !onCurrentBranch {
|
|
// due to conflict resolution, the initiated event may on a different branch of the workflow.
|
|
// we don't have to do anything and can simply return not found error. Standby logic
|
|
// after seeing this error will give up verification.
|
|
return consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
ci, isRunning := mutableState.GetChildExecutionInfo(request.ParentInitiatedId)
|
|
if isRunning {
|
|
if ci.StartedEventId != common.EmptyEventID &&
|
|
ci.GetStartedWorkflowId() != request.ChildExecution.GetWorkflowId() {
|
|
// this can happen since we may not have the initiated version
|
|
return consts.ErrChildExecutionNotFound
|
|
}
|
|
|
|
return consts.ErrWorkflowNotReady
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) ReplicateEventsV2(
|
|
ctx context.Context,
|
|
replicateRequest *historyservice.ReplicateEventsV2Request,
|
|
) error {
|
|
|
|
return e.nDCReplicator.ApplyEvents(ctx, replicateRequest)
|
|
}
|
|
|
|
// ReplicateWorkflowState is an experimental method to replicate workflow state. This should not expose outside of history service role.
|
|
func (e *historyEngineImpl) ReplicateWorkflowState(
|
|
ctx context.Context,
|
|
request *historyservice.ReplicateWorkflowStateRequest,
|
|
) error {
|
|
|
|
return e.nDCReplicator.ApplyWorkflowState(ctx, request)
|
|
}
|
|
|
|
func (e *historyEngineImpl) SyncShardStatus(
|
|
ctx context.Context,
|
|
request *historyservice.SyncShardStatusRequest,
|
|
) error {
|
|
|
|
clusterName := request.GetSourceCluster()
|
|
now := timestamp.TimeValue(request.GetStatusTime())
|
|
|
|
// here there are 3 main things
|
|
// 1. update the view of remote cluster's shard time
|
|
// 2. notify the timer gate in the timer queue standby processor
|
|
// 3, notify the transfer (essentially a no op, just put it here so it looks symmetric)
|
|
e.shard.SetCurrentTime(clusterName, now)
|
|
for _, processor := range e.queueProcessors {
|
|
processor.NotifyNewTasks(clusterName, []tasks.Task{})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) SyncActivity(
|
|
ctx context.Context,
|
|
request *historyservice.SyncActivityRequest,
|
|
) (retError error) {
|
|
|
|
return e.nDCActivityReplicator.SyncActivity(ctx, request)
|
|
}
|
|
|
|
// ResetWorkflowExecution terminates current workflow (if running) and replay & create new workflow
|
|
// Consistency guarantee: always write
|
|
func (e *historyEngineImpl) ResetWorkflowExecution(
|
|
ctx context.Context,
|
|
resetRequest *historyservice.ResetWorkflowExecutionRequest,
|
|
) (response *historyservice.ResetWorkflowExecutionResponse, retError error) {
|
|
|
|
request := resetRequest.ResetRequest
|
|
namespaceID := namespace.ID(resetRequest.GetNamespaceId())
|
|
workflowID := request.WorkflowExecution.GetWorkflowId()
|
|
baseRunID := request.WorkflowExecution.GetRunId()
|
|
|
|
baseWFContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
baseRunID,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { baseWFContext.GetReleaseFn()(retError) }()
|
|
|
|
baseMutableState := baseWFContext.GetMutableState()
|
|
if request.GetWorkflowTaskFinishEventId() <= common.FirstEventID ||
|
|
request.GetWorkflowTaskFinishEventId() >= baseMutableState.GetNextEventID() {
|
|
return nil, serviceerror.NewInvalidArgument("Workflow task finish ID must be > 1 && <= workflow last event ID.")
|
|
}
|
|
|
|
// also load the current run of the workflow, it can be different from the base runID
|
|
currentRunID, err := e.workflowConsistencyChecker.GetCurrentRunID(
|
|
ctx,
|
|
namespaceID.String(),
|
|
request.WorkflowExecution.GetWorkflowId(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if baseRunID == "" {
|
|
baseRunID = currentRunID
|
|
}
|
|
|
|
var currentWFContext api.WorkflowContext
|
|
if currentRunID == baseRunID {
|
|
currentWFContext = baseWFContext
|
|
} else {
|
|
currentWFContext, err = e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
currentRunID,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { currentWFContext.GetReleaseFn()(retError) }()
|
|
}
|
|
|
|
// dedup by requestID
|
|
if currentWFContext.GetMutableState().GetExecutionState().CreateRequestId == request.GetRequestId() {
|
|
e.logger.Info("Duplicated reset request",
|
|
tag.WorkflowID(workflowID),
|
|
tag.WorkflowRunID(currentRunID),
|
|
tag.WorkflowNamespaceID(namespaceID.String()))
|
|
return &historyservice.ResetWorkflowExecutionResponse{
|
|
RunId: currentRunID,
|
|
}, nil
|
|
}
|
|
|
|
resetRunID := uuid.New()
|
|
baseRebuildLastEventID := request.GetWorkflowTaskFinishEventId() - 1
|
|
baseVersionHistories := baseMutableState.GetExecutionInfo().GetVersionHistories()
|
|
baseCurrentVersionHistory, err := versionhistory.GetCurrentVersionHistory(baseVersionHistories)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseRebuildLastEventVersion, err := versionhistory.GetVersionHistoryEventVersion(baseCurrentVersionHistory, baseRebuildLastEventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseCurrentBranchToken := baseCurrentVersionHistory.GetBranchToken()
|
|
baseNextEventID := baseMutableState.GetNextEventID()
|
|
|
|
if err := e.workflowResetter.resetWorkflow(
|
|
ctx,
|
|
namespaceID,
|
|
workflowID,
|
|
baseRunID,
|
|
baseCurrentBranchToken,
|
|
baseRebuildLastEventID,
|
|
baseRebuildLastEventVersion,
|
|
baseNextEventID,
|
|
resetRunID,
|
|
request.GetRequestId(),
|
|
newNDCWorkflow(
|
|
ctx,
|
|
e.shard.GetNamespaceRegistry(),
|
|
e.shard.GetClusterMetadata(),
|
|
currentWFContext.GetContext(),
|
|
currentWFContext.GetMutableState(),
|
|
currentWFContext.GetReleaseFn(),
|
|
),
|
|
request.GetReason(),
|
|
nil,
|
|
request.GetResetReapplyType(),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.ResetWorkflowExecutionResponse{
|
|
RunId: resetRunID,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) NotifyNewHistoryEvent(
|
|
notification *events.Notification,
|
|
) {
|
|
|
|
e.eventNotifier.NotifyNewHistoryEvent(notification)
|
|
}
|
|
|
|
func (e *historyEngineImpl) NotifyNewTasks(
|
|
clusterName string,
|
|
newTasks map[tasks.Category][]tasks.Task,
|
|
) {
|
|
for category, tasksByCategory := range newTasks {
|
|
// TODO: make replicatorProcessor part of queueProcessors list
|
|
// and get rid of the special case here.
|
|
if category == tasks.CategoryReplication {
|
|
if e.replicationAckMgr != nil {
|
|
e.replicationAckMgr.NotifyNewTasks(tasksByCategory)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if len(tasksByCategory) > 0 {
|
|
e.queueProcessors[category].NotifyNewTasks(clusterName, tasksByCategory)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *historyEngineImpl) getActiveNamespaceEntry(
|
|
namespaceUUID namespace.ID,
|
|
) (*namespace.Namespace, error) {
|
|
return api.GetActiveNamespace(e.shard, namespaceUUID)
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetReplicationMessages(
|
|
ctx context.Context,
|
|
pollingCluster string,
|
|
ackMessageID int64,
|
|
ackTimestamp time.Time,
|
|
queryMessageID int64,
|
|
) (*replicationspb.ReplicationMessages, error) {
|
|
|
|
if ackMessageID != persistence.EmptyQueueMessageID {
|
|
if err := e.shard.UpdateQueueClusterAckLevel(
|
|
tasks.CategoryReplication,
|
|
pollingCluster,
|
|
tasks.NewImmediateKey(ackMessageID),
|
|
); err != nil {
|
|
e.logger.Error("error updating replication level for shard", tag.Error(err), tag.OperationFailed)
|
|
}
|
|
e.shard.UpdateRemoteClusterInfo(pollingCluster, ackMessageID, ackTimestamp)
|
|
}
|
|
|
|
replicationMessages, err := e.replicationAckMgr.GetTasks(
|
|
ctx,
|
|
pollingCluster,
|
|
queryMessageID,
|
|
)
|
|
if err != nil {
|
|
e.logger.Error("Failed to retrieve replication messages.", tag.Error(err))
|
|
return nil, err
|
|
}
|
|
e.logger.Debug("Successfully fetched replication messages.", tag.Counter(len(replicationMessages.ReplicationTasks)))
|
|
|
|
return replicationMessages, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetDLQReplicationMessages(
|
|
ctx context.Context,
|
|
taskInfos []*replicationspb.ReplicationTaskInfo,
|
|
) ([]*replicationspb.ReplicationTask, error) {
|
|
|
|
tasks := make([]*replicationspb.ReplicationTask, 0, len(taskInfos))
|
|
for _, taskInfo := range taskInfos {
|
|
task, err := e.replicationAckMgr.GetTask(ctx, taskInfo)
|
|
if err != nil {
|
|
e.logger.Error("Failed to fetch DLQ replication messages.", tag.Error(err))
|
|
return nil, err
|
|
}
|
|
tasks = append(tasks, task)
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) ReapplyEvents(
|
|
ctx context.Context,
|
|
namespaceUUID namespace.ID,
|
|
workflowID string,
|
|
runID string,
|
|
reapplyEvents []*historypb.HistoryEvent,
|
|
) error {
|
|
|
|
if e.config.SkipReapplicationByNamespaceID(namespaceUUID.String()) {
|
|
return nil
|
|
}
|
|
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespaceUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
return api.GetAndUpdateWorkflowWithNew(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
workflowID,
|
|
"",
|
|
),
|
|
func(workflowContext api.WorkflowContext) (action *api.UpdateWorkflowAction, retErr error) {
|
|
context := workflowContext.GetContext()
|
|
mutableState := workflowContext.GetMutableState()
|
|
// Filter out reapply event from the same cluster
|
|
toReapplyEvents := make([]*historypb.HistoryEvent, 0, len(reapplyEvents))
|
|
lastWriteVersion, err := mutableState.GetLastWriteVersion()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceMutableState := mutableState
|
|
if sourceMutableState.GetWorkflowKey().RunID != runID {
|
|
originCtx, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(namespaceID.String(), workflowID, runID),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { originCtx.GetReleaseFn()(retErr) }()
|
|
sourceMutableState = originCtx.GetMutableState()
|
|
}
|
|
|
|
for _, event := range reapplyEvents {
|
|
if event.GetVersion() == lastWriteVersion {
|
|
// The reapply is from the same cluster. Ignoring.
|
|
continue
|
|
}
|
|
dedupResource := definition.NewEventReappliedID(runID, event.GetEventId(), event.GetVersion())
|
|
if mutableState.IsResourceDuplicated(dedupResource) {
|
|
// already apply the signal
|
|
continue
|
|
}
|
|
versionHistories := sourceMutableState.GetExecutionInfo().GetVersionHistories()
|
|
if e.containsHistoryEvent(versionHistories, event.GetEventId(), event.GetVersion()) {
|
|
continue
|
|
}
|
|
|
|
toReapplyEvents = append(toReapplyEvents, event)
|
|
}
|
|
if len(toReapplyEvents) == 0 {
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
if !mutableState.IsWorkflowExecutionRunning() {
|
|
// need to reset target workflow (which is also the current workflow)
|
|
// to accept events to be reapplied
|
|
baseRunID := mutableState.GetExecutionState().GetRunId()
|
|
resetRunID := uuid.New()
|
|
baseRebuildLastEventID := mutableState.GetPreviousStartedEventID()
|
|
|
|
// TODO when https://github.com/uber/cadence/issues/2420 is finished, remove this block,
|
|
// since cannot reapply event to a finished workflow which had no workflow tasks started
|
|
if baseRebuildLastEventID == common.EmptyEventID {
|
|
e.logger.Warn("cannot reapply event to a finished workflow with no workflow task",
|
|
tag.WorkflowNamespaceID(namespaceID.String()),
|
|
tag.WorkflowID(workflowID),
|
|
)
|
|
e.metricsClient.IncCounter(metrics.HistoryReapplyEventsScope, metrics.EventReapplySkippedCount)
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
baseVersionHistories := mutableState.GetExecutionInfo().GetVersionHistories()
|
|
baseCurrentVersionHistory, err := versionhistory.GetCurrentVersionHistory(baseVersionHistories)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseRebuildLastEventVersion, err := versionhistory.GetVersionHistoryEventVersion(baseCurrentVersionHistory, baseRebuildLastEventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseCurrentBranchToken := baseCurrentVersionHistory.GetBranchToken()
|
|
baseNextEventID := mutableState.GetNextEventID()
|
|
|
|
err = e.workflowResetter.resetWorkflow(
|
|
ctx,
|
|
namespaceID,
|
|
workflowID,
|
|
baseRunID,
|
|
baseCurrentBranchToken,
|
|
baseRebuildLastEventID,
|
|
baseRebuildLastEventVersion,
|
|
baseNextEventID,
|
|
resetRunID,
|
|
uuid.New(),
|
|
newNDCWorkflow(
|
|
ctx,
|
|
e.shard.GetNamespaceRegistry(),
|
|
e.shard.GetClusterMetadata(),
|
|
context,
|
|
mutableState,
|
|
workflow.NoopReleaseFn,
|
|
),
|
|
eventsReapplicationResetWorkflowReason,
|
|
toReapplyEvents,
|
|
enumspb.RESET_REAPPLY_TYPE_SIGNAL,
|
|
)
|
|
switch err.(type) {
|
|
case *serviceerror.InvalidArgument:
|
|
// no-op. Usually this is due to reset workflow with pending child workflows
|
|
e.logger.Warn("Cannot reset workflow. Ignoring reapply events.", tag.Error(err))
|
|
case nil:
|
|
// no-op
|
|
default:
|
|
return nil, err
|
|
}
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
|
|
postActions := &api.UpdateWorkflowAction{
|
|
Noop: false,
|
|
CreateWorkflowTask: true,
|
|
}
|
|
if mutableState.IsWorkflowPendingOnWorkflowTaskBackoff() {
|
|
// Do not create workflow task when the workflow has first workflow task backoff and execution is not started yet
|
|
postActions.CreateWorkflowTask = false
|
|
}
|
|
reappliedEvents, err := e.eventsReapplier.reapplyEvents(
|
|
ctx,
|
|
mutableState,
|
|
toReapplyEvents,
|
|
runID,
|
|
)
|
|
if err != nil {
|
|
e.logger.Error("failed to re-apply stale events", tag.Error(err))
|
|
return nil, err
|
|
}
|
|
if len(reappliedEvents) == 0 {
|
|
return &api.UpdateWorkflowAction{
|
|
Noop: true,
|
|
CreateWorkflowTask: false,
|
|
}, nil
|
|
}
|
|
return postActions, nil
|
|
},
|
|
nil,
|
|
e.shard,
|
|
e.workflowConsistencyChecker,
|
|
)
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetDLQMessages(
|
|
ctx context.Context,
|
|
request *historyservice.GetDLQMessagesRequest,
|
|
) (*historyservice.GetDLQMessagesResponse, error) {
|
|
|
|
_, ok := e.clusterMetadata.GetAllClusterInfo()[request.GetSourceCluster()]
|
|
if !ok {
|
|
return nil, consts.ErrUnknownCluster
|
|
}
|
|
|
|
tasks, token, err := e.replicationDLQHandler.GetMessages(
|
|
ctx,
|
|
request.GetSourceCluster(),
|
|
request.GetInclusiveEndMessageId(),
|
|
int(request.GetMaximumPageSize()),
|
|
request.GetNextPageToken(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.GetDLQMessagesResponse{
|
|
Type: request.GetType(),
|
|
ReplicationTasks: tasks,
|
|
NextPageToken: token,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) PurgeDLQMessages(
|
|
ctx context.Context,
|
|
request *historyservice.PurgeDLQMessagesRequest,
|
|
) error {
|
|
|
|
_, ok := e.clusterMetadata.GetAllClusterInfo()[request.GetSourceCluster()]
|
|
if !ok {
|
|
return consts.ErrUnknownCluster
|
|
}
|
|
|
|
return e.replicationDLQHandler.PurgeMessages(
|
|
ctx,
|
|
request.GetSourceCluster(),
|
|
request.GetInclusiveEndMessageId(),
|
|
)
|
|
}
|
|
|
|
func (e *historyEngineImpl) MergeDLQMessages(
|
|
ctx context.Context,
|
|
request *historyservice.MergeDLQMessagesRequest,
|
|
) (*historyservice.MergeDLQMessagesResponse, error) {
|
|
|
|
_, ok := e.clusterMetadata.GetAllClusterInfo()[request.GetSourceCluster()]
|
|
if !ok {
|
|
return nil, consts.ErrUnknownCluster
|
|
}
|
|
|
|
token, err := e.replicationDLQHandler.MergeMessages(
|
|
ctx,
|
|
request.GetSourceCluster(),
|
|
request.GetInclusiveEndMessageId(),
|
|
int(request.GetMaximumPageSize()),
|
|
request.GetNextPageToken(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.MergeDLQMessagesResponse{
|
|
NextPageToken: token,
|
|
}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) RebuildMutableState(
|
|
ctx context.Context,
|
|
namespaceUUID namespace.ID,
|
|
execution commonpb.WorkflowExecution,
|
|
) error {
|
|
return e.workflowRebuilder.rebuild(
|
|
ctx,
|
|
definition.NewWorkflowKey(
|
|
namespaceUUID.String(),
|
|
execution.GetWorkflowId(),
|
|
execution.GetRunId(),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (e *historyEngineImpl) RefreshWorkflowTasks(
|
|
ctx context.Context,
|
|
namespaceUUID namespace.ID,
|
|
execution commonpb.WorkflowExecution,
|
|
) (retError error) {
|
|
|
|
err := api.ValidateNamespaceUUID(namespaceUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wfContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceUUID.String(),
|
|
execution.WorkflowId,
|
|
execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { wfContext.GetReleaseFn()(retError) }()
|
|
|
|
mutableState := wfContext.GetMutableState()
|
|
mutableStateTaskRefresher := workflow.NewTaskRefresher(
|
|
e.shard,
|
|
e.shard.GetConfig(),
|
|
e.shard.GetNamespaceRegistry(),
|
|
e.shard.GetEventsCache(),
|
|
e.shard.GetLogger(),
|
|
)
|
|
|
|
now := e.shard.GetTimeSource().Now()
|
|
|
|
err = mutableStateTaskRefresher.RefreshTasks(ctx, now, mutableState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return e.shard.AddTasks(ctx, &persistence.AddHistoryTasksRequest{
|
|
ShardID: e.shard.GetShardID(),
|
|
// RangeID is set by shard
|
|
NamespaceID: namespaceUUID.String(),
|
|
WorkflowID: execution.WorkflowId,
|
|
RunID: execution.RunId,
|
|
Tasks: mutableState.PopTasks(),
|
|
})
|
|
}
|
|
|
|
func (e *historyEngineImpl) GenerateLastHistoryReplicationTasks(
|
|
ctx context.Context,
|
|
request *historyservice.GenerateLastHistoryReplicationTasksRequest,
|
|
) (_ *historyservice.GenerateLastHistoryReplicationTasksResponse, retError error) {
|
|
namespaceEntry, err := e.getActiveNamespaceEntry(namespace.ID(request.GetNamespaceId()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
namespaceID := namespaceEntry.ID()
|
|
|
|
wfContext, err := e.workflowConsistencyChecker.GetWorkflowContext(
|
|
ctx,
|
|
nil,
|
|
api.BypassMutableStateConsistencyPredicate,
|
|
definition.NewWorkflowKey(
|
|
namespaceID.String(),
|
|
request.Execution.WorkflowId,
|
|
request.Execution.RunId,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { wfContext.GetReleaseFn()(retError) }()
|
|
|
|
now := e.shard.GetTimeSource().Now()
|
|
task, err := wfContext.GetMutableState().GenerateMigrationTasks(now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = e.shard.AddTasks(ctx, &persistence.AddHistoryTasksRequest{
|
|
ShardID: e.shard.GetShardID(),
|
|
// RangeID is set by shard
|
|
NamespaceID: string(namespaceID),
|
|
WorkflowID: request.Execution.WorkflowId,
|
|
RunID: request.Execution.RunId,
|
|
Tasks: map[tasks.Category][]tasks.Task{
|
|
tasks.CategoryReplication: {task},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &historyservice.GenerateLastHistoryReplicationTasksResponse{}, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) GetReplicationStatus(
|
|
ctx context.Context,
|
|
request *historyservice.GetReplicationStatusRequest,
|
|
) (_ *historyservice.ShardReplicationStatus, retError error) {
|
|
|
|
resp := &historyservice.ShardReplicationStatus{
|
|
ShardId: e.shard.GetShardID(),
|
|
ShardLocalTime: timestamp.TimePtr(e.shard.GetTimeSource().Now()),
|
|
}
|
|
|
|
maxReplicationTaskId, maxTaskVisibilityTimeStamp := e.replicationAckMgr.GetMaxTaskInfo()
|
|
resp.MaxReplicationTaskId = maxReplicationTaskId
|
|
resp.MaxReplicationTaskVisibilityTime = timestamp.TimePtr(maxTaskVisibilityTimeStamp)
|
|
|
|
remoteClusters, handoverNamespaces, err := e.shard.GetReplicationStatus(request.RemoteClusters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.RemoteClusters = remoteClusters
|
|
resp.HandoverNamespaces = handoverNamespaces
|
|
return resp, nil
|
|
}
|
|
|
|
func (e *historyEngineImpl) containsHistoryEvent(
|
|
versionHistories *historyspb.VersionHistories,
|
|
reappliedEventID int64,
|
|
reappliedEventVersion int64,
|
|
) bool {
|
|
// Check if the source workflow contains the reapply event.
|
|
// If it does, it means the event is received in this cluster, no need to reapply.
|
|
_, err := versionhistory.FindFirstVersionHistoryIndexByVersionHistoryItem(
|
|
versionHistories,
|
|
versionhistory.NewVersionHistoryItem(reappliedEventID, reappliedEventVersion),
|
|
)
|
|
return err == nil
|
|
}
|
|
|
|
func historyEventOnCurrentBranch(
|
|
mutableState workflow.MutableState,
|
|
eventID int64,
|
|
eventVersion int64,
|
|
) (bool, error) {
|
|
if eventVersion == 0 {
|
|
if eventID >= mutableState.GetNextEventID() {
|
|
return false, &serviceerror.NotFound{Message: "History event not found"}
|
|
}
|
|
|
|
// there's no version, assume the event is on the current branch
|
|
return true, nil
|
|
}
|
|
|
|
versionHistoryies := mutableState.GetExecutionInfo().GetVersionHistories()
|
|
versionHistoryItem := versionhistory.NewVersionHistoryItem(eventID, eventVersion)
|
|
if _, err := versionhistory.FindFirstVersionHistoryIndexByVersionHistoryItem(
|
|
versionHistoryies,
|
|
versionHistoryItem,
|
|
); err != nil {
|
|
return false, &serviceerror.NotFound{Message: "History event not found"}
|
|
}
|
|
|
|
// check if on current branch
|
|
currentVersionHistory, err := versionhistory.GetCurrentVersionHistory(versionHistoryies)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return versionhistory.ContainsVersionHistoryItem(
|
|
currentVersionHistory,
|
|
versionHistoryItem,
|
|
), nil
|
|
}
|