peridot/vendor/go.temporal.io/sdk/internal/internal_decision_state_machine.go
2022-07-07 22:13:21 +02:00

1361 lines
49 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 internal
import (
"container/list"
"fmt"
"strconv"
commandpb "go.temporal.io/api/command/v1"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
failurepb "go.temporal.io/api/failure/v1"
historypb "go.temporal.io/api/history/v1"
"go.temporal.io/sdk/converter"
"go.temporal.io/sdk/internal/common/util"
)
type (
commandState int32
commandType int32
commandID struct {
commandType commandType
id string
}
commandStateMachine interface {
getState() commandState
getID() commandID
isDone() bool
getCommand() *commandpb.Command // return nil if there is no command in current state
cancel()
handleStartedEvent()
handleCancelInitiatedEvent()
handleCanceledEvent()
handleCancelFailedEvent()
handleCompletionEvent()
handleInitiationFailedEvent()
handleInitiatedEvent()
handleCommandSent()
setData(data interface{})
getData() interface{}
}
commandStateMachineBase struct {
id commandID
state commandState
history []string
data interface{}
helper *commandsHelper
}
activityCommandStateMachine struct {
*commandStateMachineBase
scheduleID int64
attributes *commandpb.ScheduleActivityTaskCommandAttributes
}
cancelActivityStateMachine struct {
*commandStateMachineBase
attributes *commandpb.RequestCancelActivityTaskCommandAttributes
// The commandsHelper.nextCommandEventIDResetCounter when this command
// incremented commandsHelper.commandsCancelledDuringWFCancellation.
cancelledOnEventIDResetCounter uint64
}
timerCommandStateMachine struct {
*commandStateMachineBase
attributes *commandpb.StartTimerCommandAttributes
}
cancelTimerCommandStateMachine struct {
*commandStateMachineBase
attributes *commandpb.CancelTimerCommandAttributes
// The commandsHelper.nextCommandEventIDResetCounter when this command
// incremented commandsHelper.commandsCancelledDuringWFCancellation.
cancelledOnEventIDResetCounter uint64
}
childWorkflowCommandStateMachine struct {
*commandStateMachineBase
attributes *commandpb.StartChildWorkflowExecutionCommandAttributes
}
naiveCommandStateMachine struct {
*commandStateMachineBase
command *commandpb.Command
}
// only possible state transition is: CREATED->SENT->INITIATED->COMPLETED
cancelExternalWorkflowCommandStateMachine struct {
*naiveCommandStateMachine
}
signalExternalWorkflowCommandStateMachine struct {
*naiveCommandStateMachine
}
// only possible state transition is: CREATED->SENT->COMPLETED
markerCommandStateMachine struct {
*naiveCommandStateMachine
}
upsertSearchAttributesCommandStateMachine struct {
*naiveCommandStateMachine
}
commandsHelper struct {
nextCommandEventID int64
orderedCommands *list.List
commands map[commandID]*list.Element
scheduledEventIDToActivityID map[int64]string
scheduledEventIDToCancellationID map[int64]string
scheduledEventIDToSignalID map[int64]string
versionMarkerLookup map[int64]string
commandsCancelledDuringWFCancellation int64
workflowExecutionIsCancelling bool
// Incremented everytime nextCommandEventID and
// commandsCancelledDuringWFCancellation is reset (i.e. on new workflow
// task). Won't ever happen, but technically the way this value is compared
// is safe for overflow wrap around.
nextCommandEventIDResetCounter uint64
}
// panic when command state machine is in illegal state
stateMachineIllegalStatePanic struct {
message string
}
)
const (
commandStateCreated commandState = 0
commandStateCommandSent commandState = 1
commandStateCanceledBeforeInitiated commandState = 2
commandStateInitiated commandState = 3
commandStateStarted commandState = 4
commandStateCanceledAfterInitiated commandState = 5
commandStateCanceledAfterStarted commandState = 6
commandStateCancellationCommandSent commandState = 7
commandStateCompletedAfterCancellationCommandSent commandState = 8
commandStateCompleted commandState = 9
commandStateCanceledBeforeSent commandState = 10
commandStateCancellationCommandAccepted commandState = 11
)
const (
commandTypeActivity commandType = 0
commandTypeChildWorkflow commandType = 1
commandTypeCancellation commandType = 2
commandTypeMarker commandType = 3
commandTypeTimer commandType = 4
commandTypeSignal commandType = 5
commandTypeUpsertSearchAttributes commandType = 6
commandTypeCancelTimer commandType = 7
commandTypeRequestCancelActivityTask commandType = 8
)
const (
eventCancel = "cancel"
eventCommandSent = "handleCommandSent"
eventInitiated = "handleInitiatedEvent"
eventInitiationFailed = "handleInitiationFailedEvent"
eventStarted = "handleStartedEvent"
eventCompletion = "handleCompletionEvent"
eventCancelInitiated = "handleCancelInitiatedEvent"
eventCancelFailed = "handleCancelFailedEvent"
eventCanceled = "handleCanceledEvent"
eventExternalWorkflowExecutionCancelRequested = "handleExternalWorkflowExecutionCancelRequested"
)
const (
sideEffectMarkerName = "SideEffect"
versionMarkerName = "Version"
localActivityMarkerName = "LocalActivity"
mutableSideEffectMarkerName = "MutableSideEffect"
sideEffectMarkerIDName = "side-effect-id"
sideEffectMarkerDataName = "data"
versionMarkerChangeIDName = "change-id"
versionMarkerDataName = "version"
localActivityMarkerDataName = "data"
localActivityResultName = "result"
)
func (d commandState) String() string {
switch d {
case commandStateCreated:
return "Created"
case commandStateCommandSent:
return "CommandSent"
case commandStateCanceledBeforeInitiated:
return "CanceledBeforeInitiated"
case commandStateInitiated:
return "Initiated"
case commandStateStarted:
return "Started"
case commandStateCanceledAfterInitiated:
return "CanceledAfterInitiated"
case commandStateCanceledAfterStarted:
return "CanceledAfterStarted"
case commandStateCancellationCommandSent:
return "CancellationCommandSent"
case commandStateCompletedAfterCancellationCommandSent:
return "CompletedAfterCancellationCommandSent"
case commandStateCompleted:
return "Completed"
case commandStateCanceledBeforeSent:
return "CanceledBeforeSent"
case commandStateCancellationCommandAccepted:
return "CancellationCommandAccepted"
default:
return fmt.Sprintf("Unknown: %d", int32(d))
}
}
func (d commandType) String() string {
switch d {
case commandTypeActivity:
return "Activity"
case commandTypeChildWorkflow:
return "ChildWorkflow"
case commandTypeCancellation:
return "Cancellation"
case commandTypeMarker:
return "Marker"
case commandTypeTimer:
return "Timer"
case commandTypeSignal:
return "Signal"
case commandTypeCancelTimer:
return "CancelTimer"
case commandTypeRequestCancelActivityTask:
return "RequestCancelActivityTask"
default:
return "Unknown"
}
}
func (d commandID) String() string {
return fmt.Sprintf("CommandType: %v, ID: %v", d.commandType, d.id)
}
func makeCommandID(commandType commandType, id string) commandID {
return commandID{commandType: commandType, id: id}
}
func (h *commandsHelper) newCommandStateMachineBase(commandType commandType, id string) *commandStateMachineBase {
return &commandStateMachineBase{
id: makeCommandID(commandType, id),
state: commandStateCreated,
history: []string{commandStateCreated.String()},
helper: h,
}
}
func (h *commandsHelper) newActivityCommandStateMachine(
scheduleID int64,
attributes *commandpb.ScheduleActivityTaskCommandAttributes,
) *activityCommandStateMachine {
base := h.newCommandStateMachineBase(commandTypeActivity, attributes.GetActivityId())
return &activityCommandStateMachine{
commandStateMachineBase: base,
scheduleID: scheduleID,
attributes: attributes,
}
}
func (h *commandsHelper) newCancelActivityStateMachine(attributes *commandpb.RequestCancelActivityTaskCommandAttributes) *cancelActivityStateMachine {
base := h.newCommandStateMachineBase(commandTypeRequestCancelActivityTask, strconv.FormatInt(attributes.GetScheduledEventId(), 10))
return &cancelActivityStateMachine{
commandStateMachineBase: base,
attributes: attributes,
}
}
func (h *commandsHelper) newTimerCommandStateMachine(attributes *commandpb.StartTimerCommandAttributes) *timerCommandStateMachine {
base := h.newCommandStateMachineBase(commandTypeTimer, attributes.GetTimerId())
return &timerCommandStateMachine{
commandStateMachineBase: base,
attributes: attributes,
}
}
func (h *commandsHelper) newCancelTimerCommandStateMachine(attributes *commandpb.CancelTimerCommandAttributes) *cancelTimerCommandStateMachine {
base := h.newCommandStateMachineBase(commandTypeCancelTimer, attributes.GetTimerId())
return &cancelTimerCommandStateMachine{
commandStateMachineBase: base,
attributes: attributes,
}
}
func (h *commandsHelper) newChildWorkflowCommandStateMachine(attributes *commandpb.StartChildWorkflowExecutionCommandAttributes) *childWorkflowCommandStateMachine {
base := h.newCommandStateMachineBase(commandTypeChildWorkflow, attributes.GetWorkflowId())
return &childWorkflowCommandStateMachine{
commandStateMachineBase: base,
attributes: attributes,
}
}
func (h *commandsHelper) newNaiveCommandStateMachine(commandType commandType, id string, command *commandpb.Command) *naiveCommandStateMachine {
base := h.newCommandStateMachineBase(commandType, id)
return &naiveCommandStateMachine{
commandStateMachineBase: base,
command: command,
}
}
func (h *commandsHelper) newMarkerCommandStateMachine(id string, attributes *commandpb.RecordMarkerCommandAttributes) *markerCommandStateMachine {
d := createNewCommand(enumspb.COMMAND_TYPE_RECORD_MARKER)
d.Attributes = &commandpb.Command_RecordMarkerCommandAttributes{RecordMarkerCommandAttributes: attributes}
return &markerCommandStateMachine{
naiveCommandStateMachine: h.newNaiveCommandStateMachine(commandTypeMarker, id, d),
}
}
func (h *commandsHelper) newCancelExternalWorkflowStateMachine(attributes *commandpb.RequestCancelExternalWorkflowExecutionCommandAttributes, cancellationID string) *cancelExternalWorkflowCommandStateMachine {
d := createNewCommand(enumspb.COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION)
d.Attributes = &commandpb.Command_RequestCancelExternalWorkflowExecutionCommandAttributes{RequestCancelExternalWorkflowExecutionCommandAttributes: attributes}
return &cancelExternalWorkflowCommandStateMachine{
naiveCommandStateMachine: h.newNaiveCommandStateMachine(commandTypeCancellation, cancellationID, d),
}
}
func (h *commandsHelper) newSignalExternalWorkflowStateMachine(attributes *commandpb.SignalExternalWorkflowExecutionCommandAttributes, signalID string) *signalExternalWorkflowCommandStateMachine {
d := createNewCommand(enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION)
d.Attributes = &commandpb.Command_SignalExternalWorkflowExecutionCommandAttributes{SignalExternalWorkflowExecutionCommandAttributes: attributes}
return &signalExternalWorkflowCommandStateMachine{
naiveCommandStateMachine: h.newNaiveCommandStateMachine(commandTypeSignal, signalID, d),
}
}
func (h *commandsHelper) newUpsertSearchAttributesStateMachine(attributes *commandpb.UpsertWorkflowSearchAttributesCommandAttributes, upsertID string) *upsertSearchAttributesCommandStateMachine {
d := createNewCommand(enumspb.COMMAND_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES)
d.Attributes = &commandpb.Command_UpsertWorkflowSearchAttributesCommandAttributes{UpsertWorkflowSearchAttributesCommandAttributes: attributes}
return &upsertSearchAttributesCommandStateMachine{
naiveCommandStateMachine: h.newNaiveCommandStateMachine(commandTypeUpsertSearchAttributes, upsertID, d),
}
}
func (d *commandStateMachineBase) getState() commandState {
return d.state
}
func (d *commandStateMachineBase) getID() commandID {
return d.id
}
func (d *commandStateMachineBase) isDone() bool {
return d.state == commandStateCompleted || d.state == commandStateCompletedAfterCancellationCommandSent
}
func (d *commandStateMachineBase) setData(data interface{}) {
d.data = data
}
func (d *commandStateMachineBase) getData() interface{} {
return d.data
}
func (d *commandStateMachineBase) moveState(newState commandState, event string) {
d.history = append(d.history, event)
d.state = newState
d.history = append(d.history, newState.String())
if newState == commandStateCompleted {
if elem, ok := d.helper.commands[d.getID()]; ok {
d.helper.orderedCommands.Remove(elem)
delete(d.helper.commands, d.getID())
}
}
}
func (d stateMachineIllegalStatePanic) String() string {
return d.message
}
func panicIllegalState(message string) {
panic(stateMachineIllegalStatePanic{message: message})
}
func (d *commandStateMachineBase) failStateTransition(event string) {
// this is when we detect illegal state transition, likely due to ill history sequence or nondeterministic workflow code
panicIllegalState(fmt.Sprintf("invalid state transition: attempt to %v, %v", event, d))
}
func (d *commandStateMachineBase) handleCommandSent() {
switch d.state {
case commandStateCreated:
d.moveState(commandStateCommandSent, eventCommandSent)
}
}
func (d *commandStateMachineBase) cancel() {
switch d.state {
case commandStateCompleted, commandStateCompletedAfterCancellationCommandSent:
// No op. This is legit. People could cancel context after timer/activity is done.
case commandStateCreated:
d.moveState(commandStateCanceledBeforeSent, eventCancel)
case commandStateCommandSent:
d.moveState(commandStateCancellationCommandSent, eventCancel)
case commandStateInitiated:
if d.helper.workflowExecutionIsCancelling {
d.helper.commandsCancelledDuringWFCancellation++
}
d.moveState(commandStateCanceledAfterInitiated, eventCancel)
default:
d.failStateTransition(eventCancel)
}
}
func (d *commandStateMachineBase) handleInitiatedEvent() {
switch d.state {
case commandStateCommandSent:
d.moveState(commandStateInitiated, eventInitiated)
case commandStateCanceledBeforeInitiated, commandStateCanceledBeforeSent, commandStateCancellationCommandSent:
d.moveState(commandStateCanceledAfterInitiated, eventInitiated)
default:
d.failStateTransition(eventInitiated)
}
}
func (d *commandStateMachineBase) handleInitiationFailedEvent() {
switch d.state {
case commandStateInitiated, commandStateCommandSent, commandStateCanceledBeforeInitiated, commandStateCancellationCommandSent:
d.moveState(commandStateCompleted, eventInitiationFailed)
default:
d.failStateTransition(eventInitiationFailed)
}
}
func (d *commandStateMachineBase) handleStartedEvent() {
d.history = append(d.history, eventStarted)
}
func (d *commandStateMachineBase) handleCompletionEvent() {
switch d.state {
case commandStateCanceledAfterInitiated, commandStateInitiated:
d.moveState(commandStateCompleted, eventCompletion)
case commandStateCancellationCommandSent:
d.moveState(commandStateCompletedAfterCancellationCommandSent, eventCompletion)
default:
d.failStateTransition(eventCompletion)
}
}
func (d *commandStateMachineBase) handleCancelInitiatedEvent() {
d.history = append(d.history, eventCancelInitiated)
switch d.state {
case commandStateCancellationCommandSent, commandStateCanceledAfterInitiated:
// No state change
default:
d.failStateTransition(eventCancelInitiated)
}
}
func (d *commandStateMachineBase) handleCancelFailedEvent() {
switch d.state {
case commandStateCompletedAfterCancellationCommandSent:
d.moveState(commandStateCompleted, eventCancelFailed)
default:
d.failStateTransition(eventCancelFailed)
}
}
func (d *commandStateMachineBase) handleCanceledEvent() {
switch d.state {
case commandStateCancellationCommandSent, commandStateCanceledAfterInitiated, commandStateCancellationCommandAccepted:
d.moveState(commandStateCompleted, eventCanceled)
default:
d.failStateTransition(eventCanceled)
}
}
func (d *commandStateMachineBase) String() string {
return fmt.Sprintf("%v, state=%v, isDone()=%v, history=%v",
d.id, d.state, d.isDone(), d.history)
}
func (d *activityCommandStateMachine) getCommand() *commandpb.Command {
switch d.state {
case commandStateCreated, commandStateCanceledBeforeSent:
command := createNewCommand(enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK)
command.Attributes = &commandpb.Command_ScheduleActivityTaskCommandAttributes{ScheduleActivityTaskCommandAttributes: d.attributes}
return command
default:
return nil
}
}
func (d *activityCommandStateMachine) handleCommandSent() {
switch d.state {
case commandStateCanceledAfterInitiated:
d.moveState(commandStateCancellationCommandSent, eventCommandSent)
default:
d.commandStateMachineBase.handleCommandSent()
}
}
func (d *activityCommandStateMachine) handleCancelFailedEvent() {
// Request to cancel activity now results in either activity completion, failed, timedout, or canceled
// Request to cancel itself can never fail and invalid RequestCancelActivity commands results in the
// entire command being failed.
d.failStateTransition(eventCancelFailed)
}
func (d *activityCommandStateMachine) cancel() {
switch d.state {
case commandStateCreated, commandStateCommandSent, commandStateInitiated:
attribs := &commandpb.RequestCancelActivityTaskCommandAttributes{
ScheduledEventId: d.scheduleID,
}
cancelCmd := d.helper.newCancelActivityStateMachine(attribs)
d.helper.addCommand(cancelCmd)
// We must mark the event ID reset counter for when we performed this
// increment so a potential decrement can only decrement if it wasn't
// reset
cancelCmd.cancelledOnEventIDResetCounter = d.helper.nextCommandEventIDResetCounter
}
d.commandStateMachineBase.cancel()
}
func (d *timerCommandStateMachine) cancel() {
switch d.state {
case commandStateCreated, commandStateCommandSent, commandStateInitiated:
attribs := &commandpb.CancelTimerCommandAttributes{
TimerId: d.attributes.TimerId,
}
cancelCmd := d.helper.newCancelTimerCommandStateMachine(attribs)
d.helper.addCommand(cancelCmd)
// We must mark the event ID reset counter for when we performed this
// increment so a potential decrement can only decrement if it wasn't
// reset
cancelCmd.cancelledOnEventIDResetCounter = d.helper.nextCommandEventIDResetCounter
}
d.commandStateMachineBase.cancel()
}
func (d *timerCommandStateMachine) isDone() bool {
return d.state == commandStateCompleted
}
func (d *timerCommandStateMachine) handleCommandSent() {
switch d.state {
case commandStateCanceledAfterInitiated:
d.moveState(commandStateCancellationCommandSent, eventCommandSent)
default:
d.commandStateMachineBase.handleCommandSent()
}
}
func (d *cancelActivityStateMachine) getCommand() *commandpb.Command {
switch d.state {
case commandStateCreated:
command := createNewCommand(enumspb.COMMAND_TYPE_REQUEST_CANCEL_ACTIVITY_TASK)
command.Attributes = &commandpb.Command_RequestCancelActivityTaskCommandAttributes{RequestCancelActivityTaskCommandAttributes: d.attributes}
return command
default:
return nil
}
}
func (d *timerCommandStateMachine) getCommand() *commandpb.Command {
switch d.state {
case commandStateCreated, commandStateCanceledBeforeSent:
command := createNewCommand(enumspb.COMMAND_TYPE_START_TIMER)
command.Attributes = &commandpb.Command_StartTimerCommandAttributes{StartTimerCommandAttributes: d.attributes}
return command
default:
return nil
}
}
func (d *cancelTimerCommandStateMachine) getCommand() *commandpb.Command {
switch d.state {
case commandStateCreated:
command := createNewCommand(enumspb.COMMAND_TYPE_CANCEL_TIMER)
command.Attributes = &commandpb.Command_CancelTimerCommandAttributes{CancelTimerCommandAttributes: d.attributes}
return command
default:
return nil
}
}
func (d *childWorkflowCommandStateMachine) getCommand() *commandpb.Command {
switch d.state {
case commandStateCreated:
command := createNewCommand(enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION)
command.Attributes = &commandpb.Command_StartChildWorkflowExecutionCommandAttributes{StartChildWorkflowExecutionCommandAttributes: d.attributes}
return command
case commandStateCanceledAfterStarted:
command := createNewCommand(enumspb.COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION)
command.Attributes = &commandpb.Command_RequestCancelExternalWorkflowExecutionCommandAttributes{RequestCancelExternalWorkflowExecutionCommandAttributes: &commandpb.RequestCancelExternalWorkflowExecutionCommandAttributes{
Namespace: d.attributes.Namespace,
WorkflowId: d.attributes.WorkflowId,
ChildWorkflowOnly: true,
}}
return command
default:
return nil
}
}
func (d *childWorkflowCommandStateMachine) handleCommandSent() {
switch d.state {
case commandStateCanceledAfterStarted:
d.moveState(commandStateCancellationCommandSent, eventCommandSent)
default:
d.commandStateMachineBase.handleCommandSent()
}
}
func (d *childWorkflowCommandStateMachine) handleStartedEvent() {
switch d.state {
case commandStateInitiated:
d.moveState(commandStateStarted, eventStarted)
case commandStateCanceledAfterInitiated:
d.moveState(commandStateCanceledAfterStarted, eventStarted)
default:
d.commandStateMachineBase.handleStartedEvent()
}
}
func (d *childWorkflowCommandStateMachine) handleInitiatedEvent() {
switch d.state {
case commandStateCancellationCommandSent:
d.failStateTransition(eventInitiated)
default:
d.commandStateMachineBase.handleInitiatedEvent()
}
}
func (d *childWorkflowCommandStateMachine) handleCancelFailedEvent() {
switch d.state {
case commandStateCancellationCommandSent:
d.moveState(commandStateStarted, eventCancelFailed)
default:
d.commandStateMachineBase.handleCancelFailedEvent()
}
}
func (d *childWorkflowCommandStateMachine) cancel() {
switch d.state {
case commandStateStarted:
if d.helper.workflowExecutionIsCancelling {
d.helper.commandsCancelledDuringWFCancellation++
}
d.moveState(commandStateCanceledAfterStarted, eventCancel)
// A child workflow may be canceled _after_ something like an activity start
// happens inside a simulated goroutine. However, since the state of the
// entire child workflow is recorded based on when it started not when it
// was canceled, we have to move it to the end once canceled to keep the
// expected commands in order of when they actually occurred.
d.helper.moveCommandToBack(d)
d.helper.incrementNextCommandEventID()
default:
d.commandStateMachineBase.cancel()
}
}
func (d *childWorkflowCommandStateMachine) handleCanceledEvent() {
switch d.state {
case commandStateStarted:
d.moveState(commandStateCompleted, eventCanceled)
case commandStateCancellationCommandSent:
// We've sent the command but haven't seen the server accept the cancellation. We must ensure this command hangs
// around, because it is possible for the child workflow to be canceled before we've seen the event.
d.moveState(commandStateCompletedAfterCancellationCommandSent, eventCanceled)
default:
d.commandStateMachineBase.handleCanceledEvent()
}
}
func (d *childWorkflowCommandStateMachine) handleCompletionEvent() {
switch d.state {
case commandStateStarted, commandStateCanceledAfterStarted,
commandStateCompletedAfterCancellationCommandSent, commandStateCancellationCommandAccepted:
d.moveState(commandStateCompleted, eventCompletion)
default:
d.commandStateMachineBase.handleCompletionEvent()
}
}
func (d *childWorkflowCommandStateMachine) handleExternalWorkflowExecutionCancelRequested() {
if d.getState() == commandStateCompletedAfterCancellationCommandSent {
// Now we're really done.
d.handleCompletionEvent()
} else {
// We should be in the cancellation command sent stage - new state to indicate we have seen the cancel accepted
d.moveState(commandStateCancellationCommandAccepted, eventExternalWorkflowExecutionCancelRequested)
}
}
func (d *naiveCommandStateMachine) getCommand() *commandpb.Command {
switch d.state {
case commandStateCreated:
return d.command
default:
return nil
}
}
func (d *naiveCommandStateMachine) cancel() {
panic("unsupported operation")
}
func (d *naiveCommandStateMachine) handleCompletionEvent() {
panic("unsupported operation")
}
func (d *naiveCommandStateMachine) handleInitiatedEvent() {
panic("unsupported operation")
}
func (d *naiveCommandStateMachine) handleInitiationFailedEvent() {
panic("unsupported operation")
}
func (d *naiveCommandStateMachine) handleStartedEvent() {
panic("unsupported operation")
}
func (d *naiveCommandStateMachine) handleCanceledEvent() {
panic("unsupported operation")
}
func (d *naiveCommandStateMachine) handleCancelFailedEvent() {
panic("unsupported operation")
}
func (d *naiveCommandStateMachine) handleCancelInitiatedEvent() {
panic("unsupported operation")
}
func (d *cancelExternalWorkflowCommandStateMachine) handleInitiatedEvent() {
switch d.state {
case commandStateCommandSent:
d.moveState(commandStateInitiated, eventInitiated)
default:
d.failStateTransition(eventInitiated)
}
}
func (d *cancelExternalWorkflowCommandStateMachine) handleCompletionEvent() {
switch d.state {
case commandStateInitiated:
d.moveState(commandStateCompleted, eventCompletion)
default:
d.failStateTransition(eventCompletion)
}
}
func (d *signalExternalWorkflowCommandStateMachine) handleInitiatedEvent() {
switch d.state {
case commandStateCommandSent:
d.moveState(commandStateInitiated, eventInitiated)
default:
d.failStateTransition(eventInitiated)
}
}
func (d *signalExternalWorkflowCommandStateMachine) handleCompletionEvent() {
switch d.state {
case commandStateInitiated:
d.moveState(commandStateCompleted, eventCompletion)
default:
d.failStateTransition(eventCompletion)
}
}
func (d *markerCommandStateMachine) handleCommandSent() {
// Marker command state machine is considered as completed once command is sent.
// For SideEffect/Version markers, when the history event is applied, there is no marker command state machine yet
// because we preload those marker events.
// For local activity, when we apply the history event, we use it to create the marker state machine, there is no
// other event to drive it to completed state.
switch d.state {
case commandStateCreated:
d.moveState(commandStateCompleted, eventCommandSent)
}
}
func (d *upsertSearchAttributesCommandStateMachine) handleCommandSent() {
// This command is considered as completed once command is sent.
switch d.state {
case commandStateCreated:
d.moveState(commandStateCompleted, eventCommandSent)
}
}
func newCommandsHelper() *commandsHelper {
return &commandsHelper{
orderedCommands: list.New(),
commands: make(map[commandID]*list.Element),
scheduledEventIDToActivityID: make(map[int64]string),
scheduledEventIDToCancellationID: make(map[int64]string),
scheduledEventIDToSignalID: make(map[int64]string),
versionMarkerLookup: make(map[int64]string),
commandsCancelledDuringWFCancellation: 0,
}
}
func (h *commandsHelper) incrementNextCommandEventID() {
h.nextCommandEventID++
}
func (h *commandsHelper) setCurrentWorkflowTaskStartedEventID(workflowTaskStartedEventID int64) {
// Server always processes the commands in the same order it is generated by client and each command results in
// corresponding history event after processing. So we can use workflow task started event id + 2 as the offset as
// workflow task completed event is always the first event in the workflow task followed by events generated from
// commands. This allows client sdk to deterministically predict history event ids generated by processing of the
// command. We must also add the number of cancel commands that were spawned during cancellation of the workflow
// execution as those canceled command events will show up *after* the workflow task completed event.
h.nextCommandEventID = workflowTaskStartedEventID + 2 + h.commandsCancelledDuringWFCancellation
h.commandsCancelledDuringWFCancellation = 0
// We must change the counter here so that others who mutate
// commandsCancelledDuringWFCancellation know it has since been reset
h.nextCommandEventIDResetCounter++
}
func (h *commandsHelper) getNextID() int64 {
// First check if we have a GetVersion marker in the lookup map
h.incrementNextCommandEventIDIfVersionMarker()
if h.nextCommandEventID == 0 {
panic("Attempt to generate a command before processing WorkflowTaskStarted event")
}
return h.nextCommandEventID
}
func (h *commandsHelper) incrementNextCommandEventIDIfVersionMarker() {
_, ok := h.versionMarkerLookup[h.nextCommandEventID]
for ok {
// Remove the marker from the lookup map and increment nextCommandEventID by 2 because call to GetVersion
// results in 2 events in the history. One is GetVersion marker event for changeID and change version, other
// is UpsertSearchableAttributes to keep track of executions using particular version of code.
delete(h.versionMarkerLookup, h.nextCommandEventID)
h.incrementNextCommandEventID()
h.incrementNextCommandEventID()
_, ok = h.versionMarkerLookup[h.nextCommandEventID]
}
}
func (h *commandsHelper) getCommand(id commandID) commandStateMachine {
command, ok := h.commands[id]
if !ok {
panicMsg := fmt.Sprintf("unknown command %v, possible causes are nondeterministic workflow definition code"+
" or incompatible change in the workflow definition", id)
panicIllegalState(panicMsg)
}
return command.Value.(commandStateMachine)
}
func (h *commandsHelper) addCommand(command commandStateMachine) {
if _, ok := h.commands[command.getID()]; ok {
panicMsg := fmt.Sprintf("adding duplicate command %v", command)
panicIllegalState(panicMsg)
}
element := h.orderedCommands.PushBack(command)
h.commands[command.getID()] = element
// Every time new command is added increment the counter used for generating ID
h.incrementNextCommandEventIDIfVersionMarker()
h.incrementNextCommandEventID()
}
// This really should not exist, but is unavoidable without totally redesigning the Go SDK to avoid
// doing event number counting. EX: Because a workflow execution cancel requested event calls a callback
// on timers that immediately cancels them, we will queue up a cancel timer command even though that timer firing
// might be in the same workflow task. In practice this only seems to happen during unhandled command events.
func (h *commandsHelper) removeCancelOfResolvedCommand(commandID commandID) {
// Ensure this isn't misused for non-cancel commands
if commandID.commandType != commandTypeCancelTimer && commandID.commandType != commandTypeRequestCancelActivityTask {
panic("removeCancelOfResolvedCommand should only be called for cancel timer / activity")
}
orderedCmdEl, ok := h.commands[commandID]
if ok {
delete(h.commands, commandID)
command := h.orderedCommands.Remove(orderedCmdEl)
// Sometimes commandsCancelledDuringWFCancellation was incremented before
// it was reset and sometimes not. We use the reset counter to see if we're
// still on the same iteration where we may have incremented it before.
switch command := command.(type) {
case *cancelActivityStateMachine:
if command.cancelledOnEventIDResetCounter == h.nextCommandEventIDResetCounter {
h.commandsCancelledDuringWFCancellation--
}
case *cancelTimerCommandStateMachine:
if command.cancelledOnEventIDResetCounter == h.nextCommandEventIDResetCounter {
h.commandsCancelledDuringWFCancellation--
}
}
}
}
func (h *commandsHelper) moveCommandToBack(command commandStateMachine) {
elem := h.commands[command.getID()]
if elem == nil {
panicIllegalState(fmt.Sprintf("moving command not present %v", command))
}
h.orderedCommands.Remove(elem)
h.commands[command.getID()] = h.orderedCommands.PushBack(command)
}
func (h *commandsHelper) scheduleActivityTask(
scheduleID int64,
attributes *commandpb.ScheduleActivityTaskCommandAttributes,
) commandStateMachine {
h.scheduledEventIDToActivityID[scheduleID] = attributes.GetActivityId()
command := h.newActivityCommandStateMachine(scheduleID, attributes)
h.addCommand(command)
return command
}
func (h *commandsHelper) requestCancelActivityTask(activityID string) commandStateMachine {
id := makeCommandID(commandTypeActivity, activityID)
command := h.getCommand(id)
command.cancel()
return command
}
func (h *commandsHelper) handleActivityTaskClosed(activityID string, scheduledEventID int64) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeActivity, activityID))
// If, for whatever reason, we were going to send an activity cancel request, don't do that anymore
// since we already know the activity is resolved.
possibleCancelID := makeCommandID(commandTypeRequestCancelActivityTask, activityID)
h.removeCancelOfResolvedCommand(possibleCancelID)
command.handleCompletionEvent()
delete(h.scheduledEventIDToActivityID, scheduledEventID)
return command
}
func (h *commandsHelper) handleActivityTaskScheduled(activityID string, scheduledEventID int64) {
if _, ok := h.scheduledEventIDToActivityID[scheduledEventID]; !ok {
panicMsg := fmt.Sprintf("lookup failed for scheduledEventID to activityID: scheduleEventID: %v, activityID: %v",
scheduledEventID, activityID)
panicIllegalState(panicMsg)
}
command := h.getCommand(makeCommandID(commandTypeActivity, activityID))
command.handleInitiatedEvent()
}
func (h *commandsHelper) handleActivityTaskCancelRequested(scheduledEventID int64) {
activityID, ok := h.scheduledEventIDToActivityID[scheduledEventID]
if !ok {
panicIllegalState(fmt.Sprintf("unable to find activityID for the scheduledEventID: %v", scheduledEventID))
}
command := h.getCommand(makeCommandID(commandTypeActivity, activityID))
command.handleCancelInitiatedEvent()
}
func (h *commandsHelper) handleActivityTaskCanceled(activityID string, scheduledEventID int64) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeActivity, activityID))
command.handleCanceledEvent()
delete(h.scheduledEventIDToActivityID, scheduledEventID)
return command
}
func (h *commandsHelper) getActivityAndScheduledEventIDs(event *historypb.HistoryEvent) (string, int64) {
var scheduledEventID int64 = -1
switch event.GetEventType() {
case enumspb.EVENT_TYPE_ACTIVITY_TASK_CANCELED:
scheduledEventID = event.GetActivityTaskCanceledEventAttributes().GetScheduledEventId()
case enumspb.EVENT_TYPE_ACTIVITY_TASK_COMPLETED:
scheduledEventID = event.GetActivityTaskCompletedEventAttributes().GetScheduledEventId()
case enumspb.EVENT_TYPE_ACTIVITY_TASK_FAILED:
scheduledEventID = event.GetActivityTaskFailedEventAttributes().GetScheduledEventId()
case enumspb.EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT:
scheduledEventID = event.GetActivityTaskTimedOutEventAttributes().GetScheduledEventId()
default:
panicIllegalState(fmt.Sprintf("unexpected event type: %v", event.GetEventType()))
}
activityID, ok := h.scheduledEventIDToActivityID[scheduledEventID]
if !ok {
panicIllegalState(fmt.Sprintf("unable to find activityID for the event: %v", util.HistoryEventToString(event)))
}
return activityID, scheduledEventID
}
func (h *commandsHelper) recordVersionMarker(changeID string, version Version, dc converter.DataConverter) commandStateMachine {
markerID := fmt.Sprintf("%v_%v", versionMarkerName, changeID)
changeIDPayload, err := dc.ToPayloads(changeID)
if err != nil {
panic(err)
}
versionPayload, err := dc.ToPayloads(version)
if err != nil {
panic(err)
}
recordMarker := &commandpb.RecordMarkerCommandAttributes{
MarkerName: versionMarkerName,
Details: map[string]*commonpb.Payloads{
versionMarkerChangeIDName: changeIDPayload,
versionMarkerDataName: versionPayload,
},
}
command := h.newMarkerCommandStateMachine(markerID, recordMarker)
h.addCommand(command)
return command
}
func (h *commandsHelper) handleVersionMarker(eventID int64, changeID string) {
if _, ok := h.versionMarkerLookup[eventID]; ok {
panicMsg := fmt.Sprintf("marker event already exists for eventID in lookup: eventID: %v, changeID: %v",
eventID, changeID)
panicIllegalState(panicMsg)
}
// During processing of a workflow task we reorder all GetVersion markers and process them first.
// Keep track of all GetVersion marker events during the processing of workflow task so we can
// generate correct eventIDs for other events during replay.
h.versionMarkerLookup[eventID] = changeID
}
func (h *commandsHelper) recordSideEffectMarker(sideEffectID int64, data *commonpb.Payloads, dc converter.DataConverter) commandStateMachine {
markerID := fmt.Sprintf("%v_%v", sideEffectMarkerName, sideEffectID)
sideEffectIDPayload, err := dc.ToPayloads(sideEffectID)
if err != nil {
panic(err)
}
attributes := &commandpb.RecordMarkerCommandAttributes{
MarkerName: sideEffectMarkerName,
Details: map[string]*commonpb.Payloads{
sideEffectMarkerIDName: sideEffectIDPayload,
sideEffectMarkerDataName: data,
},
}
command := h.newMarkerCommandStateMachine(markerID, attributes)
h.addCommand(command)
return command
}
func (h *commandsHelper) recordLocalActivityMarker(activityID string, details map[string]*commonpb.Payloads, failure *failurepb.Failure) commandStateMachine {
markerID := fmt.Sprintf("%v_%v", localActivityMarkerName, activityID)
attributes := &commandpb.RecordMarkerCommandAttributes{
MarkerName: localActivityMarkerName,
Failure: failure,
Details: details,
}
command := h.newMarkerCommandStateMachine(markerID, attributes)
// LocalActivity marker is added only when it completes and schedule logic never relies on GenerateSequence to
// create a unique activity id like in the case of ExecuteActivity. This causes the problem as we only perform
// the check to increment counter to account for GetVersion special handling as part of it. This will result
// in wrong IDs to be generated if there is GetVersion call before local activities. Explicitly calling getNextID
// to correctly incrementing counter before adding the command.
h.getNextID()
h.addCommand(command)
return command
}
func (h *commandsHelper) recordMutableSideEffectMarker(mutableSideEffectID string, data *commonpb.Payloads, dc converter.DataConverter) commandStateMachine {
markerID := fmt.Sprintf("%v_%v", mutableSideEffectMarkerName, mutableSideEffectID)
mutableSideEffectIDPayload, err := dc.ToPayloads(mutableSideEffectID)
if err != nil {
panic(err)
}
attributes := &commandpb.RecordMarkerCommandAttributes{
MarkerName: mutableSideEffectMarkerName,
Details: map[string]*commonpb.Payloads{
sideEffectMarkerIDName: mutableSideEffectIDPayload,
sideEffectMarkerDataName: data,
},
}
command := h.newMarkerCommandStateMachine(markerID, attributes)
h.addCommand(command)
return command
}
func (h *commandsHelper) startChildWorkflowExecution(attributes *commandpb.StartChildWorkflowExecutionCommandAttributes) commandStateMachine {
command := h.newChildWorkflowCommandStateMachine(attributes)
h.addCommand(command)
return command
}
func (h *commandsHelper) handleStartChildWorkflowExecutionInitiated(workflowID string) {
command := h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.handleInitiatedEvent()
}
func (h *commandsHelper) handleStartChildWorkflowExecutionFailed(workflowID string) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.handleInitiationFailedEvent()
return command
}
func (h *commandsHelper) requestCancelExternalWorkflowExecution(namespace, workflowID, runID string, cancellationID string, childWorkflowOnly bool) commandStateMachine {
if childWorkflowOnly {
// For cancellation of child workflow only, we do not use cancellation ID
// since the child workflow cancellation go through the existing child workflow
// state machine, and we use workflow ID as identifier
// we also do not use run ID, since child workflow can do continue-as-new
// which will have different run ID
// there will be server side validation that target workflow is child workflow
// sanity check that cancellation ID is not set
if len(cancellationID) != 0 {
panic("cancellation on child workflow should not use cancellation ID")
}
// sanity check that run ID is not set
if len(runID) != 0 {
panic("cancellation on child workflow should not use run ID")
}
// targeting child workflow
command := h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.cancel()
return command
}
// For cancellation of external workflow, we have to use cancellation ID
// to identify different cancellation request (command) / response (history event)
// client can also use this code path to cancel its own child workflow, however, there will
// be no server side validation that target workflow is the child
// sanity check that cancellation ID is set
if len(cancellationID) == 0 {
panic("cancellation on external workflow should use cancellation ID")
}
attributes := &commandpb.RequestCancelExternalWorkflowExecutionCommandAttributes{
Namespace: namespace,
WorkflowId: workflowID,
RunId: runID,
Control: cancellationID,
ChildWorkflowOnly: false,
}
command := h.newCancelExternalWorkflowStateMachine(attributes, cancellationID)
h.addCommand(command)
return command
}
func (h *commandsHelper) handleRequestCancelExternalWorkflowExecutionInitiated(initiatedeventID int64, workflowID, cancellationID string) {
if h.isCancelExternalWorkflowEventForChildWorkflow(cancellationID) {
// this is cancellation for child workflow only
command := h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.handleCancelInitiatedEvent()
} else {
// this is cancellation for external workflow
h.scheduledEventIDToCancellationID[initiatedeventID] = cancellationID
command := h.getCommand(makeCommandID(commandTypeCancellation, cancellationID))
command.handleInitiatedEvent()
}
}
func (h *commandsHelper) handleExternalWorkflowExecutionCancelRequested(initiatedeventID int64, workflowID string) (bool, commandStateMachine) {
var command commandStateMachine
cancellationID, isExternal := h.scheduledEventIDToCancellationID[initiatedeventID]
if !isExternal {
command = h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
asChildWfCmd := command.(*childWorkflowCommandStateMachine)
asChildWfCmd.handleExternalWorkflowExecutionCancelRequested()
} else {
// this is cancellation for external workflow
command = h.getCommand(makeCommandID(commandTypeCancellation, cancellationID))
command.handleCompletionEvent()
}
return isExternal, command
}
func (h *commandsHelper) handleRequestCancelExternalWorkflowExecutionFailed(initiatedeventID int64, workflowID string) (bool, commandStateMachine) {
var command commandStateMachine
cancellationID, isExternal := h.scheduledEventIDToCancellationID[initiatedeventID]
if !isExternal {
// this is cancellation for child workflow only
command = h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.handleCancelFailedEvent()
} else {
// this is cancellation for external workflow
command = h.getCommand(makeCommandID(commandTypeCancellation, cancellationID))
command.handleCompletionEvent()
}
return isExternal, command
}
func (h *commandsHelper) signalExternalWorkflowExecution(
namespace string,
workflowID string,
runID string,
signalName string,
input *commonpb.Payloads,
header *commonpb.Header,
signalID string,
childWorkflowOnly bool,
) commandStateMachine {
attributes := &commandpb.SignalExternalWorkflowExecutionCommandAttributes{
Namespace: namespace,
Execution: &commonpb.WorkflowExecution{
WorkflowId: workflowID,
RunId: runID,
},
SignalName: signalName,
Input: input,
Control: signalID,
ChildWorkflowOnly: childWorkflowOnly,
Header: header,
}
command := h.newSignalExternalWorkflowStateMachine(attributes, signalID)
h.addCommand(command)
return command
}
func (h *commandsHelper) upsertSearchAttributes(upsertID string, searchAttr *commonpb.SearchAttributes) commandStateMachine {
attributes := &commandpb.UpsertWorkflowSearchAttributesCommandAttributes{
SearchAttributes: searchAttr,
}
command := h.newUpsertSearchAttributesStateMachine(attributes, upsertID)
h.addCommand(command)
return command
}
func (h *commandsHelper) handleSignalExternalWorkflowExecutionInitiated(initiatedEventID int64, signalID string) {
h.scheduledEventIDToSignalID[initiatedEventID] = signalID
command := h.getCommand(makeCommandID(commandTypeSignal, signalID))
command.handleInitiatedEvent()
}
func (h *commandsHelper) handleSignalExternalWorkflowExecutionCompleted(initiatedEventID int64) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeSignal, h.getSignalID(initiatedEventID)))
command.handleCompletionEvent()
return command
}
func (h *commandsHelper) handleSignalExternalWorkflowExecutionFailed(initiatedEventID int64) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeSignal, h.getSignalID(initiatedEventID)))
command.handleCompletionEvent()
return command
}
func (h *commandsHelper) getSignalID(initiatedEventID int64) string {
signalID, ok := h.scheduledEventIDToSignalID[initiatedEventID]
if !ok {
panic(fmt.Sprintf("unable to find signalID for initiatedEventID: %v", initiatedEventID))
}
return signalID
}
func (h *commandsHelper) startTimer(attributes *commandpb.StartTimerCommandAttributes) commandStateMachine {
command := h.newTimerCommandStateMachine(attributes)
h.addCommand(command)
return command
}
func (h *commandsHelper) cancelTimer(timerID TimerID) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeTimer, timerID.id))
command.cancel()
return command
}
func (h *commandsHelper) handleTimerClosed(timerID string) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeTimer, timerID))
// If, for whatever reason, we were going to send a timer cancel command, don't do that anymore
// since we already know the timer is resolved.
possibleCancelID := makeCommandID(commandTypeCancelTimer, timerID)
h.removeCancelOfResolvedCommand(possibleCancelID)
command.handleCompletionEvent()
return command
}
func (h *commandsHelper) handleTimerStarted(timerID string) {
command := h.getCommand(makeCommandID(commandTypeTimer, timerID))
command.handleInitiatedEvent()
}
func (h *commandsHelper) handleTimerCanceled(timerID string) {
command := h.getCommand(makeCommandID(commandTypeTimer, timerID))
command.handleCanceledEvent()
}
func (h *commandsHelper) handleChildWorkflowExecutionStarted(workflowID string) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.handleStartedEvent()
return command
}
func (h *commandsHelper) handleChildWorkflowExecutionClosed(workflowID string) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.handleCompletionEvent()
return command
}
func (h *commandsHelper) handleChildWorkflowExecutionCanceled(workflowID string) commandStateMachine {
command := h.getCommand(makeCommandID(commandTypeChildWorkflow, workflowID))
command.handleCanceledEvent()
return command
}
func (h *commandsHelper) getCommands(markAsSent bool) []*commandpb.Command {
var result []*commandpb.Command
for curr := h.orderedCommands.Front(); curr != nil; {
next := curr.Next() // get next item here as we might need to remove curr in the loop
d := curr.Value.(commandStateMachine)
command := d.getCommand()
if command != nil {
result = append(result, command)
}
if markAsSent {
d.handleCommandSent()
}
// remove completed command state machines
if d.getState() == commandStateCompleted {
h.orderedCommands.Remove(curr)
delete(h.commands, d.getID())
}
curr = next
}
return result
}
func (h *commandsHelper) isCancelExternalWorkflowEventForChildWorkflow(cancellationID string) bool {
// the cancellationID, i.e. Control in RequestCancelExternalWorkflowExecutionInitiatedEventAttributes
// will be empty if the event is for child workflow.
// for cancellation external workflow, Control in RequestCancelExternalWorkflowExecutionInitiatedEventAttributes
// will have a client generated sequence ID
return len(cancellationID) == 0
}