Files
tasks/service/history/transferQueueActiveTaskExecutor.go

1508 lines
47 KiB
Go

// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package history
import (
"context"
"fmt"
"time"
"github.com/gogo/protobuf/proto"
"github.com/pborman/uuid"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
historypb "go.temporal.io/api/history/v1"
"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/api/historyservice/v1"
"go.temporal.io/server/api/matchingservice/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
workflowspb "go.temporal.io/server/api/workflow/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/backoff"
"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/sdk"
"go.temporal.io/server/service/history/configs"
"go.temporal.io/server/service/history/consts"
"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"
"go.temporal.io/server/service/worker/parentclosepolicy"
)
type (
transferQueueActiveTaskExecutor struct {
*transferQueueTaskExecutorBase
registry namespace.Registry
workflowResetter *workflowResetterImpl
historyClient historyservice.HistoryServiceClient
parentClosePolicyClient parentclosepolicy.Client
}
)
func newTransferQueueActiveTaskExecutor(
shard shard.Context,
workflowCache workflow.Cache,
archivalClient archiver.Client,
sdkClientFactory sdk.ClientFactory,
logger log.Logger,
config *configs.Config,
matchingClient matchingservice.MatchingServiceClient,
) queueTaskExecutor {
return &transferQueueActiveTaskExecutor{
transferQueueTaskExecutorBase: newTransferQueueTaskExecutorBase(
shard,
workflowCache,
archivalClient,
logger,
matchingClient,
),
historyClient: shard.GetHistoryClient(),
workflowResetter: newWorkflowResetter(
shard,
workflowCache,
logger,
),
parentClosePolicyClient: parentclosepolicy.NewClient(
shard.GetMetricsClient(),
shard.GetLogger(),
sdkClientFactory,
config.NumParentClosePolicySystemWorkflows(),
),
registry: shard.GetNamespaceRegistry(),
}
}
func (t *transferQueueActiveTaskExecutor) execute(
ctx context.Context,
taskInfo tasks.Task,
shouldProcessTask bool,
) error {
if !shouldProcessTask {
return nil
}
switch task := taskInfo.(type) {
case *tasks.ActivityTask:
return t.processActivityTask(ctx, task)
case *tasks.WorkflowTask:
return t.processWorkflowTask(ctx, task)
case *tasks.CloseExecutionTask:
return t.processCloseExecution(ctx, task)
case *tasks.CancelExecutionTask:
return t.processCancelExecution(ctx, task)
case *tasks.SignalExecutionTask:
return t.processSignalExecution(ctx, task)
case *tasks.StartChildExecutionTask:
return t.processStartChildExecution(ctx, task)
case *tasks.ResetWorkflowTask:
return t.processResetWorkflow(ctx, task)
case *tasks.DeleteExecutionTask:
return t.processDeleteExecutionTask(ctx, task)
default:
return errUnknownTransferTask
}
}
func (t *transferQueueActiveTaskExecutor) processActivityTask(
pctx context.Context,
task *tasks.ActivityTask,
) (retError error) {
ctx, cancel := context.WithTimeout(pctx, taskTimeout)
defer cancel()
namespaceID, execution := t.getNamespaceIDAndWorkflowExecution(task)
context, release, err := t.cache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { release(retError) }()
mutableState, err := loadMutableStateForTransferTask(ctx, context, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if mutableState == nil || !mutableState.IsWorkflowExecutionRunning() {
return nil
}
ai, ok := mutableState.GetActivityInfo(task.ScheduleID)
if !ok {
return nil
}
ok = VerifyTaskVersion(t.shard, t.logger, mutableState.GetNamespaceEntry(), ai.Version, task.Version, task)
if !ok {
return nil
}
timeout := timestamp.DurationValue(ai.ScheduleToStartTimeout)
// NOTE: do not access anything related mutable state after this lock release
// release the context lock since we no longer need mutable state builder and
// the rest of logic is making RPC call, which takes time.
release(nil)
return t.pushActivity(task, &timeout)
}
func (t *transferQueueActiveTaskExecutor) processWorkflowTask(
pctx context.Context,
task *tasks.WorkflowTask,
) (retError error) {
ctx, cancel := context.WithTimeout(pctx, taskTimeout)
defer cancel()
namespaceID, execution := t.getNamespaceIDAndWorkflowExecution(task)
context, release, err := t.cache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { release(retError) }()
mutableState, err := loadMutableStateForTransferTask(ctx, context, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if mutableState == nil || !mutableState.IsWorkflowExecutionRunning() {
return nil
}
workflowTask, found := mutableState.GetWorkflowTaskInfo(task.ScheduleID)
if !found {
return nil
}
ok := VerifyTaskVersion(t.shard, t.logger, mutableState.GetNamespaceEntry(), workflowTask.Version, task.Version, task)
if !ok {
return nil
}
executionInfo := mutableState.GetExecutionInfo()
// NOTE: previously this section check whether mutable state has enabled
// sticky workflowTask, if so convert the workflowTask to a sticky workflowTask.
// that logic has a bug which timer task for that sticky workflowTask is not generated
// the correct logic should check whether the workflow task is a sticky workflowTask
// task or not.
var taskQueue *taskqueuepb.TaskQueue
var taskScheduleToStartTimeoutSeconds = int64(0)
if mutableState.GetExecutionInfo().TaskQueue != task.TaskQueue {
// this workflowTask is an sticky workflowTask
// there shall already be an timer set
taskQueue = &taskqueuepb.TaskQueue{
Name: task.TaskQueue,
Kind: enumspb.TASK_QUEUE_KIND_STICKY,
}
taskScheduleToStartTimeoutSeconds = int64(timestamp.DurationValue(executionInfo.StickyScheduleToStartTimeout).Seconds())
} else {
taskQueue = &taskqueuepb.TaskQueue{
Name: task.TaskQueue,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
}
workflowRunTimeout := timestamp.DurationValue(executionInfo.WorkflowRunTimeout)
taskScheduleToStartTimeoutSeconds = int64(workflowRunTimeout.Round(time.Second).Seconds())
}
// NOTE: do not access anything related mutable state after this lock release
// release the context lock since we no longer need mutable state builder and
// the rest of logic is making RPC call, which takes time.
release(nil)
return t.pushWorkflowTask(task, taskQueue, timestamp.DurationFromSeconds(taskScheduleToStartTimeoutSeconds))
}
func (t *transferQueueActiveTaskExecutor) processCloseExecution(
pctx context.Context,
task *tasks.CloseExecutionTask,
) (retError error) {
ctx, cancel := context.WithTimeout(pctx, taskTimeout)
defer cancel()
namespaceID, execution := t.getNamespaceIDAndWorkflowExecution(task)
weContext, release, err := t.cache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { release(retError) }()
mutableState, err := loadMutableStateForTransferTask(ctx, weContext, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if mutableState == nil || mutableState.IsWorkflowExecutionRunning() {
return nil
}
lastWriteVersion, err := mutableState.GetLastWriteVersion()
if err != nil {
return err
}
ok := VerifyTaskVersion(t.shard, t.logger, mutableState.GetNamespaceEntry(), lastWriteVersion, task.Version, task)
if !ok {
return nil
}
executionInfo := mutableState.GetExecutionInfo()
executionState := mutableState.GetExecutionState()
replyToParentWorkflow := mutableState.HasParentExecution() && executionInfo.NewExecutionRunId == ""
completionEvent, err := mutableState.GetCompletionEvent(ctx)
if err != nil {
return err
}
wfCloseTime := timestamp.TimeValue(completionEvent.GetEventTime())
parentNamespaceID := executionInfo.ParentNamespaceId
parentWorkflowID := executionInfo.ParentWorkflowId
parentRunID := executionInfo.ParentRunId
initiatedID := executionInfo.InitiatedId
workflowTypeName := executionInfo.WorkflowTypeName
workflowCloseTime := wfCloseTime
workflowStatus := executionState.Status
workflowHistoryLength := mutableState.GetNextEventID() - 1
workflowStartTime := timestamp.TimeValue(mutableState.GetExecutionInfo().GetStartTime())
workflowExecutionTime := timestamp.TimeValue(mutableState.GetExecutionInfo().GetExecutionTime())
visibilityMemo := getWorkflowMemo(copyMemo(executionInfo.Memo))
searchAttr := getSearchAttributes(copySearchAttributes(executionInfo.SearchAttributes))
namespaceName := mutableState.GetNamespaceEntry().Name()
children := copyChildWorkflowInfos(mutableState.GetPendingChildExecutionInfos())
// NOTE: do not access anything related mutable state after this lock release
// release the context lock since we no longer need mutable state builder and
// the rest of logic is making RPC call, which takes time.
release(nil)
err = t.recordWorkflowClosed(
namespace.ID(task.NamespaceID),
task.WorkflowID,
task.RunID,
workflowTypeName,
workflowStartTime,
workflowExecutionTime,
workflowCloseTime,
workflowStatus,
workflowHistoryLength,
visibilityMemo,
searchAttr,
)
if err != nil {
return err
}
// Communicate the result to parent execution if this is Child Workflow execution
if replyToParentWorkflow {
ctx, cancel := context.WithTimeout(context.Background(), transferActiveTaskDefaultTimeout)
defer cancel()
_, err := t.historyClient.RecordChildExecutionCompleted(ctx, &historyservice.RecordChildExecutionCompletedRequest{
NamespaceId: parentNamespaceID,
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: parentWorkflowID,
RunId: parentRunID,
},
InitiatedId: initiatedID,
CompletedExecution: &commonpb.WorkflowExecution{
WorkflowId: task.WorkflowID,
RunId: task.RunID,
},
CompletionEvent: completionEvent,
})
switch err.(type) {
case nil:
// noop
case *serviceerror.NotFound:
// parent gone, noop
default:
return err
}
}
return t.processParentClosePolicy(
namespaceID.String(),
namespaceName.String(),
&execution,
children,
)
}
func (t *transferQueueActiveTaskExecutor) processCancelExecution(
pctx context.Context,
task *tasks.CancelExecutionTask,
) (retError error) {
ctx, cancel := context.WithTimeout(pctx, taskTimeout)
defer cancel()
namespaceID, execution := t.getNamespaceIDAndWorkflowExecution(task)
executionContext, release, err := t.cache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { release(retError) }()
mutableState, err := loadMutableStateForTransferTask(ctx, executionContext, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if mutableState == nil || !mutableState.IsWorkflowExecutionRunning() {
return nil
}
requestCancelInfo, ok := mutableState.GetRequestCancelInfo(task.InitiatedID)
if !ok {
return nil
}
ok = VerifyTaskVersion(t.shard, t.logger, mutableState.GetNamespaceEntry(), requestCancelInfo.Version, task.Version, task)
if !ok {
return nil
}
initiatedEvent, err := mutableState.GetRequesteCancelExternalInitiatedEvent(ctx, task.InitiatedID)
if err != nil {
return err
}
attributes := initiatedEvent.GetRequestCancelExternalWorkflowExecutionInitiatedEventAttributes()
targetNamespaceEntry, err := t.shard.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(task.TargetNamespaceID))
if err != nil {
return err
}
targetNamespace := targetNamespaceEntry.Name()
// handle workflow cancel itself
if task.NamespaceID == task.TargetNamespaceID && task.WorkflowID == task.TargetWorkflowID {
// it does not matter if the run ID is a mismatch
err = t.requestCancelExternalExecutionFailed(ctx, task, executionContext, targetNamespace, task.TargetWorkflowID, task.TargetRunID)
if _, ok := err.(*serviceerror.NotFound); ok {
// this could happen if this is a duplicate processing of the task, and the execution has already completed.
return nil
}
return err
}
if err = t.requestCancelExternalExecutionWithRetry(
task,
targetNamespace,
requestCancelInfo,
attributes,
); err != nil {
t.logger.Debug(fmt.Sprintf("Failed to cancel external workflow execution. Error: %v", err))
// Check to see if the error is non-transient, in which case add RequestCancelFailed
// event and complete transfer task by setting the err = nil
if common.IsServiceTransientError(err) || common.IsContextDeadlineExceededErr(err) {
// for retryable error just return
return err
}
return t.requestCancelExternalExecutionFailed(
ctx,
task,
executionContext,
targetNamespace,
task.TargetWorkflowID,
task.TargetRunID,
)
}
// Record ExternalWorkflowExecutionCancelRequested in source execution
return t.requestCancelExternalExecutionCompleted(
ctx,
task,
executionContext,
targetNamespace,
task.TargetWorkflowID,
task.TargetRunID,
)
}
func (t *transferQueueActiveTaskExecutor) processSignalExecution(
pctx context.Context,
task *tasks.SignalExecutionTask,
) (retError error) {
ctx, cancel := context.WithTimeout(pctx, taskTimeout)
defer cancel()
namespaceID, execution := t.getNamespaceIDAndWorkflowExecution(task)
weContext, release, err := t.cache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { release(retError) }()
mutableState, err := loadMutableStateForTransferTask(ctx, weContext, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if mutableState == nil || !mutableState.IsWorkflowExecutionRunning() {
return nil
}
signalInfo, ok := mutableState.GetSignalInfo(task.InitiatedID)
if !ok {
// TODO: here we should also RemoveSignalMutableState from target workflow
// Otherwise, target SignalRequestID still can leak if shard restart after signalExternalExecutionCompleted
// To do that, probably need to add the SignalRequestID in transfer task.
return nil
}
ok = VerifyTaskVersion(t.shard, t.logger, mutableState.GetNamespaceEntry(), signalInfo.Version, task.Version, task)
if !ok {
return nil
}
initiatedEvent, err := mutableState.GetSignalExternalInitiatedEvent(ctx, task.InitiatedID)
if err != nil {
return err
}
attributes := initiatedEvent.GetSignalExternalWorkflowExecutionInitiatedEventAttributes()
targetNamespaceEntry, err := t.shard.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(task.TargetNamespaceID))
if err != nil {
return err
}
targetNamespace := targetNamespaceEntry.Name()
// handle workflow signal itself
if task.NamespaceID == task.TargetNamespaceID && task.WorkflowID == task.TargetWorkflowID {
// it does not matter if the run ID is a mismatch
return t.signalExternalExecutionFailed(
ctx,
task,
weContext,
targetNamespace,
task.TargetWorkflowID,
task.TargetRunID,
attributes.Control,
)
}
if err = t.signalExternalExecutionWithRetry(
task,
targetNamespace,
signalInfo,
attributes,
); err != nil {
t.logger.Debug("Failed to signal external workflow execution", tag.Error(err))
// Check to see if the error is non-transient, in which case add SignalFailed
// event and complete transfer task by setting the err = nil
if common.IsServiceTransientError(err) || common.IsContextDeadlineExceededErr(err) {
// for retryable error just return
return err
}
return t.signalExternalExecutionFailed(
ctx,
task,
weContext,
targetNamespace,
task.TargetWorkflowID,
task.TargetRunID,
attributes.Control,
)
}
err = t.signalExternalExecutionCompleted(
ctx,
task,
weContext,
targetNamespace,
task.TargetWorkflowID,
task.TargetRunID,
attributes.Control,
)
if err != nil {
return err
}
signalRequestID := signalInfo.GetRequestId()
// release the weContext lock since we no longer need mutable state builder and
// the rest of logic is making RPC call, which takes time.
release(retError)
// remove signalRequestedID from target workflow, after Signal detail is removed from source workflow
ctx, cancel = context.WithTimeout(context.Background(), taskTimeout)
defer cancel()
_, err = t.historyClient.RemoveSignalMutableState(ctx, &historyservice.RemoveSignalMutableStateRequest{
NamespaceId: task.TargetNamespaceID,
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: task.TargetWorkflowID,
RunId: task.TargetRunID,
},
RequestId: signalRequestID,
})
return err
}
func (t *transferQueueActiveTaskExecutor) processStartChildExecution(
pctx context.Context,
task *tasks.StartChildExecutionTask,
) (retError error) {
ctx, cancel := context.WithTimeout(pctx, taskTimeout)
defer cancel()
namespaceID, execution := t.getNamespaceIDAndWorkflowExecution(task)
context, release, err := t.cache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { release(retError) }()
mutableState, err := loadMutableStateForTransferTask(ctx, context, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if mutableState == nil {
return nil
}
childInfo, ok := mutableState.GetChildExecutionInfo(task.InitiatedID)
if !ok {
return nil
}
ok = VerifyTaskVersion(t.shard, t.logger, mutableState.GetNamespaceEntry(), childInfo.Version, task.Version, task)
if !ok {
return nil
}
// workflow running or not, child started or not, parent close policy is abandon or not
// 8 cases in total
workflowRunning := mutableState.IsWorkflowExecutionRunning()
childStarted := childInfo.StartedId != common.EmptyEventID
if !workflowRunning && (!childStarted || childInfo.ParentClosePolicy != enumspb.PARENT_CLOSE_POLICY_ABANDON) {
// three cases here:
// case 1: workflow not running, child started, parent close policy is not abandon
// case 2: workflow not running, child not started, parent close policy is not abandon
// case 3: workflow not running, child not started, parent close policy is abandon
//
// NOTE: ideally for case 3, we should continue to start child. However, with current start child
// and standby start child verification logic, we can't do that because:
// 1. Once workflow is closed, we can't update mutable state or record child started event.
// If the RPC call for scheduling first workflow task times out but the call actually succeeds on child workflow.
// Then the child workflow can run, complete and another unrelated workflow can reuse this workflowID.
// Now when the start child task retries, we can't rely on requestID to dedup the start child call. (We can use runID instead of requestID to dedup)
// 2. No update to mutable state and child started event means we are not able to replicate the information
// to the standby cluster, so standby start child logic won't be able to verify the child has started.
// To resolve the issue above, we need to
// 1. Start child workflow and schedule the first workflow task in one transaction. Use runID to perform deduplication
// 2. Standby start child logic need to verify if child worflow actually started instead of relying on the information
// in parent mutable state.
return nil
}
// ChildExecution already started, just create WorkflowTask and complete transfer task
// If parent already closed, since child workflow started event already written to history,
// still schedule the workflowTask if the parent close policy is Abandon.
// If parent close policy cancel or terminate, parent close policy will be applied in another
// transfer task.
// case 4, 5: workflow started, child started, parent close policy is or is not abandon
// case 6: workflow closed, child started, parent close policy is abandon
if childStarted {
childExecution := &commonpb.WorkflowExecution{
WorkflowId: childInfo.StartedWorkflowId,
RunId: childInfo.StartedRunId,
}
// NOTE: do not access anything related mutable state after this lock release
// release the context lock since we no longer need mutable state builder and
// the rest of logic is making RPC call, which takes time.
release(nil)
return t.createFirstWorkflowTask(task.TargetNamespaceID, childExecution)
}
// remaining 2 cases:
// case 7, 8: workflow running, child not started, parent close policy is or is not abandon
initiatedEvent, err := mutableState.GetChildExecutionInitiatedEvent(ctx, task.InitiatedID)
if err != nil {
return err
}
attributes := initiatedEvent.GetStartChildWorkflowExecutionInitiatedEventAttributes()
var parentNamespaceName namespace.Name
if namespaceEntry, err := t.shard.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(task.NamespaceID)); err != nil {
if _, ok := err.(*serviceerror.NotFound); !ok {
return err
}
// it is possible that the namespace got deleted. Use namespaceID instead as this is only needed for the history event
parentNamespaceName = namespace.Name(task.NamespaceID)
} else {
parentNamespaceName = namespaceEntry.Name()
}
// Get target namespace name
var targetNamespaceName namespace.Name
if namespaceEntry, err := t.shard.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(task.TargetNamespaceID)); err != nil {
if _, ok := err.(*serviceerror.NotFound); !ok {
return err
}
// it is possible that the namespace got deleted. Use namespaceID instead as this is only needed for the history event
targetNamespaceName = namespace.Name(task.TargetNamespaceID)
} else {
targetNamespaceName = namespaceEntry.Name()
}
childRunID, err := t.startWorkflowWithRetry(
task,
parentNamespaceName,
targetNamespaceName,
childInfo,
attributes,
)
if err != nil {
t.logger.Debug("Failed to start child workflow execution", tag.Error(err))
// Check to see if the error is non-transient, in which case add StartChildWorkflowExecutionFailed
// event and complete transfer task by setting the err = nil
if _, ok := err.(*serviceerror.WorkflowExecutionAlreadyStarted); ok {
err = t.recordStartChildExecutionFailed(ctx, task, context, attributes)
}
return err
}
t.logger.Debug("Child Execution started successfully",
tag.WorkflowID(attributes.WorkflowId), tag.WorkflowRunID(childRunID))
// Child execution is successfully started, record ChildExecutionStartedEvent in parent execution
err = t.recordChildExecutionStarted(ctx, task, context, attributes, childRunID)
if err != nil {
return err
}
// NOTE: do not access anything related mutable state after this lock release
// release the context lock since we no longer need mutable state builder and
// the rest of logic is making RPC call, which takes time.
release(nil)
return t.createFirstWorkflowTask(task.TargetNamespaceID, &commonpb.WorkflowExecution{
WorkflowId: task.TargetWorkflowID,
RunId: childRunID,
})
}
func (t *transferQueueActiveTaskExecutor) processResetWorkflow(
pctx context.Context,
task *tasks.ResetWorkflowTask,
) (retError error) {
ctx, cancel := context.WithTimeout(pctx, taskTimeout)
defer cancel()
namespaceID, execution := t.getNamespaceIDAndWorkflowExecution(task)
currentContext, currentRelease, err := t.cache.GetOrCreateWorkflowExecution(
ctx,
namespaceID,
execution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { currentRelease(retError) }()
currentMutableState, err := loadMutableStateForTransferTask(ctx, currentContext, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if currentMutableState == nil {
return nil
}
logger := log.With(
t.logger,
tag.WorkflowNamespaceID(task.NamespaceID),
tag.WorkflowID(task.WorkflowID),
tag.WorkflowRunID(task.RunID),
)
if !currentMutableState.IsWorkflowExecutionRunning() {
// it means this this might not be current anymore, we need to check
var resp *persistence.GetCurrentExecutionResponse
resp, err = t.shard.GetCurrentExecution(ctx, &persistence.GetCurrentExecutionRequest{
ShardID: t.shard.GetShardID(),
NamespaceID: task.NamespaceID,
WorkflowID: task.WorkflowID,
})
if err != nil {
return err
}
if resp.RunID != task.RunID {
logger.Warn("Auto-Reset is skipped, because current run is stale.")
return nil
}
}
// TODO: current reset doesn't allow childWFs, in the future we will release this restriction
if len(currentMutableState.GetPendingChildExecutionInfos()) > 0 {
logger.Warn("Auto-Reset is skipped, because current run has pending child executions.")
return nil
}
currentStartVersion, err := currentMutableState.GetStartVersion()
if err != nil {
return err
}
ok := VerifyTaskVersion(t.shard, t.logger, currentMutableState.GetNamespaceEntry(), currentStartVersion, task.Version, task)
if !ok {
return nil
}
executionInfo := currentMutableState.GetExecutionInfo()
executionState := currentMutableState.GetExecutionState()
namespaceEntry, err := t.shard.GetNamespaceRegistry().GetNamespaceByID(namespace.ID(executionInfo.NamespaceId))
if err != nil {
return err
}
logger = log.With(logger, tag.WorkflowNamespace(namespaceEntry.Name().String()))
reason, resetPoint := workflow.FindAutoResetPoint(t.shard.GetTimeSource(), namespaceEntry.VerifyBinaryChecksum, executionInfo.AutoResetPoints)
if resetPoint == nil {
logger.Warn("Auto-Reset is skipped, because reset point is not found.")
return nil
}
logger = log.With(
logger,
tag.WorkflowResetBaseRunID(resetPoint.GetRunId()),
tag.WorkflowBinaryChecksum(resetPoint.GetBinaryChecksum()),
tag.WorkflowEventID(resetPoint.GetFirstWorkflowTaskCompletedId()),
)
var baseContext workflow.Context
var baseMutableState workflow.MutableState
var baseRelease workflow.ReleaseCacheFunc
if resetPoint.GetRunId() == executionState.RunId {
baseContext = currentContext
baseMutableState = currentMutableState
baseRelease = currentRelease
} else {
baseExecution := &commonpb.WorkflowExecution{
WorkflowId: task.WorkflowID,
RunId: resetPoint.GetRunId(),
}
ctx, cancel := context.WithTimeout(context.Background(), taskTimeout)
defer cancel()
baseContext, baseRelease, err = t.cache.GetOrCreateWorkflowExecution(
ctx,
namespace.ID(task.NamespaceID),
*baseExecution,
workflow.CallerTypeTask,
)
if err != nil {
return err
}
defer func() { baseRelease(retError) }()
baseMutableState, err = loadMutableStateForTransferTask(ctx, baseContext, task, t.metricsClient, t.logger)
if err != nil {
return err
}
if baseMutableState == nil {
return nil
}
}
if err := t.resetWorkflow(
task,
reason,
resetPoint,
baseMutableState,
currentContext,
currentMutableState,
logger,
); err != nil {
return err
}
return nil
}
func (t *transferQueueActiveTaskExecutor) recordChildExecutionStarted(
ctx context.Context,
task *tasks.StartChildExecutionTask,
context workflow.Context,
initiatedAttributes *historypb.StartChildWorkflowExecutionInitiatedEventAttributes,
runID string,
) error {
return t.updateWorkflowExecution(ctx, context, true,
func(mutableState workflow.MutableState) error {
if !mutableState.IsWorkflowExecutionRunning() {
return serviceerror.NewNotFound("Workflow execution already completed.")
}
namespace := namespace.Name(initiatedAttributes.Namespace)
ci, ok := mutableState.GetChildExecutionInfo(task.InitiatedID)
if !ok || ci.StartedId != common.EmptyEventID {
return serviceerror.NewNotFound("Pending child execution not found.")
}
_, err := mutableState.AddChildWorkflowExecutionStartedEvent(
namespace,
&commonpb.WorkflowExecution{
WorkflowId: task.TargetWorkflowID,
RunId: runID,
},
initiatedAttributes.WorkflowType,
task.InitiatedID,
initiatedAttributes.Header,
)
return err
})
}
func (t *transferQueueActiveTaskExecutor) recordStartChildExecutionFailed(
ctx context.Context,
task *tasks.StartChildExecutionTask,
context workflow.Context,
initiatedAttributes *historypb.StartChildWorkflowExecutionInitiatedEventAttributes,
) error {
return t.updateWorkflowExecution(ctx, context, true,
func(mutableState workflow.MutableState) error {
if !mutableState.IsWorkflowExecutionRunning() {
return serviceerror.NewNotFound("Workflow execution already completed.")
}
ci, ok := mutableState.GetChildExecutionInfo(task.InitiatedID)
if !ok || ci.StartedId != common.EmptyEventID {
return serviceerror.NewNotFound("Pending child execution not found.")
}
_, err := mutableState.AddStartChildWorkflowExecutionFailedEvent(
task.InitiatedID,
enumspb.START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_WORKFLOW_ALREADY_EXISTS,
initiatedAttributes,
)
return err
})
}
// createFirstWorkflowTask is used by StartChildExecution transfer task to create the first workflow task for
// child execution.
func (t *transferQueueActiveTaskExecutor) createFirstWorkflowTask(
namespaceID string,
execution *commonpb.WorkflowExecution,
) error {
ctx, cancel := context.WithTimeout(context.Background(), transferActiveTaskDefaultTimeout)
defer cancel()
_, err := t.historyClient.ScheduleWorkflowTask(ctx, &historyservice.ScheduleWorkflowTaskRequest{
NamespaceId: namespaceID,
WorkflowExecution: execution,
IsFirstWorkflowTask: true,
})
if err != nil {
if _, ok := err.(*serviceerror.NotFound); ok {
// Maybe child workflow execution already timed out or terminated
// Safe to discard the error and complete this transfer task
return nil
}
}
return err
}
func (t *transferQueueActiveTaskExecutor) requestCancelExternalExecutionCompleted(
ctx context.Context,
task *tasks.CancelExecutionTask,
context workflow.Context,
targetNamespace namespace.Name,
targetWorkflowID string,
targetRunID string,
) error {
err := t.updateWorkflowExecution(ctx, context, true,
func(mutableState workflow.MutableState) error {
if !mutableState.IsWorkflowExecutionRunning() {
return &serviceerror.NotFound{Message: "Workflow execution already completed."}
}
_, ok := mutableState.GetRequestCancelInfo(task.InitiatedID)
if !ok {
return workflow.ErrMissingRequestCancelInfo
}
_, err := mutableState.AddExternalWorkflowExecutionCancelRequested(
task.InitiatedID,
targetNamespace,
targetWorkflowID,
targetRunID,
)
return err
})
if _, ok := err.(*serviceerror.NotFound); ok {
// this could happen if this is a duplicate processing of the task,
// or the execution has already completed.
return nil
}
return err
}
func (t *transferQueueActiveTaskExecutor) signalExternalExecutionCompleted(
ctx context.Context,
task *tasks.SignalExecutionTask,
context workflow.Context,
targetNamespace namespace.Name,
targetWorkflowID string,
targetRunID string,
control string,
) error {
err := t.updateWorkflowExecution(ctx, context, true,
func(mutableState workflow.MutableState) error {
if !mutableState.IsWorkflowExecutionRunning() {
return &serviceerror.NotFound{Message: "Workflow execution already completed."}
}
_, ok := mutableState.GetSignalInfo(task.InitiatedID)
if !ok {
return workflow.ErrMissingSignalInfo
}
_, err := mutableState.AddExternalWorkflowExecutionSignaled(
task.InitiatedID,
targetNamespace,
targetWorkflowID,
targetRunID,
control,
)
return err
})
if _, ok := err.(*serviceerror.NotFound); ok {
// this could happen if this is a duplicate processing of the task,
// or the execution has already completed.
return nil
}
return err
}
func (t *transferQueueActiveTaskExecutor) requestCancelExternalExecutionFailed(
ctx context.Context,
task *tasks.CancelExecutionTask,
context workflow.Context,
targetNamespace namespace.Name,
targetWorkflowID string,
targetRunID string,
) error {
err := t.updateWorkflowExecution(ctx, context, true,
func(mutableState workflow.MutableState) error {
if !mutableState.IsWorkflowExecutionRunning() {
return &serviceerror.NotFound{Message: "Workflow execution already completed."}
}
_, ok := mutableState.GetRequestCancelInfo(task.InitiatedID)
if !ok {
return workflow.ErrMissingRequestCancelInfo
}
_, err := mutableState.AddRequestCancelExternalWorkflowExecutionFailedEvent(
task.InitiatedID,
targetNamespace,
targetWorkflowID,
targetRunID,
enumspb.CANCEL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_EXTERNAL_WORKFLOW_EXECUTION_NOT_FOUND,
)
return err
})
if _, ok := err.(*serviceerror.NotFound); ok {
// this could happen if this is a duplicate processing of the task,
// or the execution has already completed.
return nil
}
return err
}
func (t *transferQueueActiveTaskExecutor) signalExternalExecutionFailed(
ctx context.Context,
task *tasks.SignalExecutionTask,
context workflow.Context,
targetNamespace namespace.Name,
targetWorkflowID string,
targetRunID string,
control string,
) error {
err := t.updateWorkflowExecution(ctx, context, true,
func(mutableState workflow.MutableState) error {
if !mutableState.IsWorkflowExecutionRunning() {
return &serviceerror.NotFound{Message: "Workflow is not running."}
}
_, ok := mutableState.GetSignalInfo(task.InitiatedID)
if !ok {
return workflow.ErrMissingSignalInfo
}
_, err := mutableState.AddSignalExternalWorkflowExecutionFailedEvent(
task.InitiatedID,
targetNamespace,
targetWorkflowID,
targetRunID,
control,
enumspb.SIGNAL_EXTERNAL_WORKFLOW_EXECUTION_FAILED_CAUSE_EXTERNAL_WORKFLOW_EXECUTION_NOT_FOUND,
)
return err
})
if _, ok := err.(*serviceerror.NotFound); ok {
// this could happen if this is a duplicate processing of the task,
// or the execution has already completed.
return nil
}
return err
}
func (t *transferQueueActiveTaskExecutor) updateWorkflowExecution(
ctx context.Context,
context workflow.Context,
createWorkflowTask bool,
action func(builder workflow.MutableState) error,
) error {
mutableState, err := context.LoadWorkflowExecution(ctx)
if err != nil {
return err
}
if err := action(mutableState); err != nil {
return err
}
if createWorkflowTask {
// Create a transfer task to schedule a workflow task
err := workflow.ScheduleWorkflowTask(mutableState)
if err != nil {
return err
}
}
return context.UpdateWorkflowExecutionAsActive(ctx, t.shard.GetTimeSource().Now())
}
func (t *transferQueueActiveTaskExecutor) requestCancelExternalExecutionWithRetry(
task *tasks.CancelExecutionTask,
targetNamespace namespace.Name,
requestCancelInfo *persistencespb.RequestCancelInfo,
attributes *historypb.RequestCancelExternalWorkflowExecutionInitiatedEventAttributes,
) error {
request := &historyservice.RequestCancelWorkflowExecutionRequest{
NamespaceId: task.TargetNamespaceID,
CancelRequest: &workflowservice.RequestCancelWorkflowExecutionRequest{
Namespace: targetNamespace.String(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: task.TargetWorkflowID,
RunId: task.TargetRunID,
},
Identity: consts.IdentityHistoryService,
// Use the same request ID to dedupe RequestCancelWorkflowExecution calls
RequestId: requestCancelInfo.GetCancelRequestId(),
Reason: attributes.Reason,
},
ExternalInitiatedEventId: task.InitiatedID,
ExternalWorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: task.WorkflowID,
RunId: task.RunID,
},
ChildWorkflowOnly: task.TargetChildWorkflowOnly,
}
ctx, cancel := context.WithTimeout(context.Background(), transferActiveTaskDefaultTimeout)
defer cancel()
op := func() error {
_, err := t.historyClient.RequestCancelWorkflowExecution(ctx, request)
return err
}
err := backoff.Retry(op, workflow.PersistenceOperationRetryPolicy, common.IsPersistenceTransientError)
return err
}
func (t *transferQueueActiveTaskExecutor) signalExternalExecutionWithRetry(
task *tasks.SignalExecutionTask,
targetNamespace namespace.Name,
signalInfo *persistencespb.SignalInfo,
attributes *historypb.SignalExternalWorkflowExecutionInitiatedEventAttributes,
) error {
request := &historyservice.SignalWorkflowExecutionRequest{
NamespaceId: task.TargetNamespaceID,
SignalRequest: &workflowservice.SignalWorkflowExecutionRequest{
Namespace: targetNamespace.String(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: task.TargetWorkflowID,
RunId: task.TargetRunID,
},
Identity: consts.IdentityHistoryService,
SignalName: attributes.SignalName,
Input: attributes.Input,
// Use same request ID to deduplicate SignalWorkflowExecution calls
RequestId: signalInfo.GetRequestId(),
Control: attributes.Control,
Header: attributes.Header,
},
ExternalWorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: task.WorkflowID,
RunId: task.RunID,
},
ChildWorkflowOnly: task.TargetChildWorkflowOnly,
}
ctx, cancel := context.WithTimeout(context.Background(), transferActiveTaskDefaultTimeout)
defer cancel()
op := func() error {
_, err := t.historyClient.SignalWorkflowExecution(ctx, request)
return err
}
return backoff.Retry(op, workflow.PersistenceOperationRetryPolicy, common.IsPersistenceTransientError)
}
func (t *transferQueueActiveTaskExecutor) startWorkflowWithRetry(
task *tasks.StartChildExecutionTask,
namespace namespace.Name,
targetNamespace namespace.Name,
childInfo *persistencespb.ChildExecutionInfo,
attributes *historypb.StartChildWorkflowExecutionInitiatedEventAttributes,
) (string, error) {
request := common.CreateHistoryStartWorkflowRequest(
task.TargetNamespaceID,
&workflowservice.StartWorkflowExecutionRequest{
Namespace: targetNamespace.String(),
WorkflowId: attributes.WorkflowId,
WorkflowType: attributes.WorkflowType,
TaskQueue: attributes.TaskQueue,
Input: attributes.Input,
Header: attributes.Header,
WorkflowExecutionTimeout: attributes.WorkflowExecutionTimeout,
WorkflowRunTimeout: attributes.WorkflowRunTimeout,
WorkflowTaskTimeout: attributes.WorkflowTaskTimeout,
// Use the same request ID to dedupe StartWorkflowExecution calls
RequestId: childInfo.CreateRequestId,
WorkflowIdReusePolicy: attributes.WorkflowIdReusePolicy,
RetryPolicy: attributes.RetryPolicy,
CronSchedule: attributes.CronSchedule,
Memo: attributes.Memo,
SearchAttributes: attributes.SearchAttributes,
},
&workflowspb.ParentExecutionInfo{
NamespaceId: task.NamespaceID,
Namespace: namespace.String(),
Execution: &commonpb.WorkflowExecution{
WorkflowId: task.WorkflowID,
RunId: task.RunID,
},
InitiatedId: task.InitiatedID,
},
t.shard.GetTimeSource().Now(),
)
ctx, cancel := context.WithTimeout(context.Background(), transferActiveTaskDefaultTimeout)
defer cancel()
var response *historyservice.StartWorkflowExecutionResponse
var err error
op := func() error {
response, err = t.historyClient.StartWorkflowExecution(ctx, request)
return err
}
err = backoff.Retry(op, workflow.PersistenceOperationRetryPolicy, common.IsPersistenceTransientError)
if err != nil {
return "", err
}
return response.GetRunId(), nil
}
func (t *transferQueueActiveTaskExecutor) resetWorkflow(
task *tasks.ResetWorkflowTask,
reason string,
resetPoint *workflowpb.ResetPointInfo,
baseMutableState workflow.MutableState,
currentContext workflow.Context,
currentMutableState workflow.MutableState,
logger log.Logger,
) error {
var err error
ctx, cancel := context.WithTimeout(context.Background(), transferActiveTaskDefaultTimeout)
defer cancel()
namespaceID := namespace.ID(task.NamespaceID)
workflowID := task.WorkflowID
baseRunID := baseMutableState.GetExecutionState().GetRunId()
resetRunID := uuid.New()
baseRebuildLastEventID := resetPoint.GetFirstWorkflowTaskCompletedId() - 1
baseVersionHistories := baseMutableState.GetExecutionInfo().GetVersionHistories()
baseCurrentVersionHistory, err := versionhistory.GetCurrentVersionHistory(baseVersionHistories)
if err != nil {
return err
}
baseRebuildLastEventVersion, err := versionhistory.GetVersionHistoryEventVersion(baseCurrentVersionHistory, baseRebuildLastEventID)
if err != nil {
return err
}
baseCurrentBranchToken := baseCurrentVersionHistory.GetBranchToken()
baseNextEventID := baseMutableState.GetNextEventID()
err = t.workflowResetter.resetWorkflow(
ctx,
namespaceID,
workflowID,
baseRunID,
baseCurrentBranchToken,
baseRebuildLastEventID,
baseRebuildLastEventVersion,
baseNextEventID,
resetRunID,
uuid.New(),
newNDCWorkflow(
ctx,
t.shard.GetNamespaceRegistry(),
t.shard.GetClusterMetadata(),
currentContext,
currentMutableState,
workflow.NoopReleaseFn, // this is fine since caller will defer on release
),
reason,
nil,
enumspb.RESET_REAPPLY_TYPE_SIGNAL,
)
switch err.(type) {
case nil:
return nil
case *serviceerror.NotFound:
// This means the reset point is corrupted and not retry able.
// There must be a bug in our system that we must fix.(for example, history is not the same in active/passive)
t.metricsClient.IncCounter(metrics.TransferQueueProcessorScope, metrics.AutoResetPointCorruptionCounter)
logger.Error("Auto-Reset workflow failed and not retryable. The reset point is corrupted.", tag.Error(err))
return nil
default:
// log this error and retry
logger.Error("Auto-Reset workflow failed", tag.Error(err))
return err
}
}
func (t *transferQueueActiveTaskExecutor) processParentClosePolicy(
namespaceID string,
namespaceName string,
parentExecution *commonpb.WorkflowExecution,
childInfos map[int64]*persistencespb.ChildExecutionInfo,
) error {
if len(childInfos) == 0 {
return nil
}
scope := t.metricsClient.Scope(metrics.TransferActiveTaskCloseExecutionScope)
if t.shard.GetConfig().EnableParentClosePolicyWorker() &&
len(childInfos) >= t.shard.GetConfig().ParentClosePolicyThreshold(namespaceName) {
executions := make([]parentclosepolicy.RequestDetail, 0, len(childInfos))
for _, childInfo := range childInfos {
if childInfo.ParentClosePolicy == enumspb.PARENT_CLOSE_POLICY_ABANDON {
continue
}
childNamespaceId, err := t.registry.GetNamespaceID(namespace.Name(childInfo.GetNamespace()))
switch err.(type) {
case nil:
case *serviceerror.NotFound:
// If child namespace is deleted there is nothing to close.
continue
default:
return err
}
executions = append(executions, parentclosepolicy.RequestDetail{
Namespace: childInfo.Namespace,
NamespaceID: childNamespaceId.String(),
WorkflowID: childInfo.StartedWorkflowId,
RunID: childInfo.StartedRunId,
Policy: childInfo.ParentClosePolicy,
})
}
if len(executions) == 0 {
return nil
}
request := parentclosepolicy.Request{
Namespace: namespaceName,
NamespaceID: namespaceID,
ParentExecution: *parentExecution,
Executions: executions,
}
return t.parentClosePolicyClient.SendParentClosePolicyRequest(request)
}
for _, childInfo := range childInfos {
if err := t.applyParentClosePolicy(
parentExecution,
childInfo,
); err != nil {
if _, ok := err.(*serviceerror.NotFound); !ok {
scope.IncCounter(metrics.ParentClosePolicyProcessorFailures)
return err
}
}
scope.IncCounter(metrics.ParentClosePolicyProcessorSuccess)
}
return nil
}
func (t *transferQueueActiveTaskExecutor) applyParentClosePolicy(
parentExecution *commonpb.WorkflowExecution,
childInfo *persistencespb.ChildExecutionInfo,
) error {
ctx, cancel := context.WithTimeout(context.Background(), transferActiveTaskDefaultTimeout)
defer cancel()
switch childInfo.ParentClosePolicy {
case enumspb.PARENT_CLOSE_POLICY_ABANDON:
// noop
return nil
case enumspb.PARENT_CLOSE_POLICY_TERMINATE:
childNamespaceID, err := t.registry.GetNamespaceID(namespace.Name(childInfo.GetNamespace()))
switch err.(type) {
case nil:
case *serviceerror.NotFound:
// If child namespace is deleted there is nothing to close.
return nil
default:
return err
}
_, err = t.historyClient.TerminateWorkflowExecution(ctx, &historyservice.TerminateWorkflowExecutionRequest{
NamespaceId: childNamespaceID.String(),
TerminateRequest: &workflowservice.TerminateWorkflowExecutionRequest{
Namespace: childInfo.GetNamespace(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: childInfo.GetStartedWorkflowId(),
},
// Include StartedRunID as FirstExecutionRunID on the request to allow child to be terminated across runs.
// If the child does continue as new it still propagates the RunID of first execution.
FirstExecutionRunId: childInfo.GetStartedRunId(),
Reason: "by parent close policy",
Identity: consts.IdentityHistoryService,
},
ExternalWorkflowExecution: parentExecution,
ChildWorkflowOnly: true,
})
return err
case enumspb.PARENT_CLOSE_POLICY_REQUEST_CANCEL:
childNamespaceID, err := t.registry.GetNamespaceID(namespace.Name(childInfo.GetNamespace()))
switch err.(type) {
case nil:
case *serviceerror.NotFound:
// If child namespace is deleted there is nothing to close.
return nil
default:
return err
}
_, err = t.historyClient.RequestCancelWorkflowExecution(ctx, &historyservice.RequestCancelWorkflowExecutionRequest{
NamespaceId: childNamespaceID.String(),
CancelRequest: &workflowservice.RequestCancelWorkflowExecutionRequest{
Namespace: childInfo.GetNamespace(),
WorkflowExecution: &commonpb.WorkflowExecution{
WorkflowId: childInfo.GetStartedWorkflowId(),
},
// Include StartedRunID as FirstExecutionRunID on the request to allow child to be canceled across runs.
// If the child does continue as new it still propagates the RunID of first execution.
FirstExecutionRunId: childInfo.GetStartedRunId(),
Identity: consts.IdentityHistoryService,
},
ExternalWorkflowExecution: parentExecution,
ChildWorkflowOnly: true,
})
return err
default:
return serviceerror.NewInternal(
fmt.Sprintf("unknown parent close policy: %v", childInfo.ParentClosePolicy),
)
}
}
func copyChildWorkflowInfos(
input map[int64]*persistencespb.ChildExecutionInfo,
) map[int64]*persistencespb.ChildExecutionInfo {
result := make(map[int64]*persistencespb.ChildExecutionInfo)
if input == nil {
return result
}
for k, v := range input {
result[k] = proto.Clone(v).(*persistencespb.ChildExecutionInfo)
}
return result
}