Files
Carly de FrondevilleandGitHub 8126039ecd Versioning refactor (#7733)
## What changed?
Change version string to a struct
- The worker management workflows have not changed in this PR, instead,
the new inputs are converted to the old inputs when they come in, and
the responses are filled in to have both the old and new fields
- Only the new way to specify version transition is stored in MS, but
both are populated on read
- Change VersioningOverride so that we can safely specify multiple types
of pinned override in the future
- Store only the new fields, return the old fields on read.

## Why?
So that we don't have to rely on the string representation of a version,
and to make way for more types of PINNED.

## How did you test it?
- [x] built
- [x] run locally and tested manually
- [x] covered by existing tests
- [x] added new unit test(s)
- [x] added new functional test(s)

## Potential risks
This could theoretically break how workflow execution versioning info
and old versioning overrides are handled when they are read, how
pinned-ness is calculated, etc. The functional tests in
versioning_3_test.go test for this, and they revealed mistakes I had
made, which means we know those code paths are executed.

I did not change anything in the Deployment Manager workflows, so this
PR does not risk breaking those workflows via an incompatible change.

Tests in DeploymentVersionSuite guard against the risk of older SDK
users not seeing the fields they expect.
2025-05-19 07:57:07 +00:00

477 lines
17 KiB
Go

package describeworkflow
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/sony/gobreaker"
commonpb "go.temporal.io/api/common/v1"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
taskqueuepb "go.temporal.io/api/taskqueue/v1"
workflowpb "go.temporal.io/api/workflow/v1"
enumsspb "go.temporal.io/server/api/enums/v1"
"go.temporal.io/server/api/historyservice/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/common"
"go.temporal.io/server/common/definition"
"go.temporal.io/server/common/locks"
"go.temporal.io/server/common/log/tag"
"go.temporal.io/server/common/namespace"
"go.temporal.io/server/common/persistence/visibility/manager"
"go.temporal.io/server/components/callbacks"
"go.temporal.io/server/components/nexusoperations"
"go.temporal.io/server/service/history/api"
"go.temporal.io/server/service/history/circuitbreakerpool"
"go.temporal.io/server/service/history/hsm"
historyi "go.temporal.io/server/service/history/interfaces"
"go.temporal.io/server/service/history/tasks"
"go.temporal.io/server/service/history/workflow"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
var (
errCallbackStateUnspecified = errors.New("callback with UNSPECIFIED state")
errNexusOperationStateUnspecified = errors.New("Nexus operation with UNSPECIFIED state")
)
func clonePayloadMap(source map[string]*commonpb.Payload) map[string]*commonpb.Payload {
target := make(map[string]*commonpb.Payload, len(source))
for k, v := range source {
metadata := make(map[string][]byte, len(v.GetMetadata()))
for mk, mv := range v.GetMetadata() {
metadata[mk] = mv
}
target[k] = &commonpb.Payload{
Metadata: metadata,
Data: v.GetData(),
}
}
return target
}
func Invoke(
ctx context.Context,
req *historyservice.DescribeWorkflowExecutionRequest,
shard historyi.ShardContext,
workflowConsistencyChecker api.WorkflowConsistencyChecker,
persistenceVisibilityMgr manager.VisibilityManager,
outboundQueueCBPool *circuitbreakerpool.OutboundQueueCircuitBreakerPool,
) (_ *historyservice.DescribeWorkflowExecutionResponse, retError error) {
namespaceID := namespace.ID(req.GetNamespaceId())
err := api.ValidateNamespaceUUID(namespaceID)
if err != nil {
return nil, err
}
workflowLease, err := workflowConsistencyChecker.GetWorkflowLease(
ctx,
nil,
definition.NewWorkflowKey(
req.NamespaceId,
req.Request.Execution.WorkflowId,
req.Request.Execution.RunId,
),
locks.PriorityHigh,
)
if err != nil {
return nil, err
}
// We release the lock on this workflow just before we return from this method, at which point mutable state might
// be mutated. Take extra care to clone all response methods as marshalling happens after we return and it is unsafe
// to mutate proto fields during marshalling.
defer func() { workflowLease.GetReleaseFn()(retError) }()
mutableState := workflowLease.GetMutableState()
executionInfo := mutableState.GetExecutionInfo()
executionState := mutableState.GetExecutionState()
// fetch the start event to get the associated user metadata.
startEvent, err := mutableState.GetStartEvent(ctx)
if err != nil {
return nil, err
}
result := &historyservice.DescribeWorkflowExecutionResponse{
ExecutionConfig: &workflowpb.WorkflowExecutionConfig{
TaskQueue: &taskqueuepb.TaskQueue{
Name: executionInfo.TaskQueue,
Kind: enumspb.TASK_QUEUE_KIND_NORMAL,
},
WorkflowExecutionTimeout: executionInfo.WorkflowExecutionTimeout,
WorkflowRunTimeout: executionInfo.WorkflowRunTimeout,
DefaultWorkflowTaskTimeout: executionInfo.DefaultWorkflowTaskTimeout,
UserMetadata: startEvent.UserMetadata,
},
WorkflowExecutionInfo: &workflowpb.WorkflowExecutionInfo{
Execution: &commonpb.WorkflowExecution{
WorkflowId: executionInfo.WorkflowId,
RunId: executionState.RunId,
},
Type: &commonpb.WorkflowType{Name: executionInfo.WorkflowTypeName},
StartTime: executionState.StartTime,
Status: executionState.Status,
HistoryLength: mutableState.GetNextEventID() - common.FirstEventID,
ExecutionTime: executionInfo.ExecutionTime,
// Memo and SearchAttributes are set below
AutoResetPoints: common.CloneProto(executionInfo.AutoResetPoints),
TaskQueue: executionInfo.TaskQueue,
StateTransitionCount: executionInfo.StateTransitionCount,
HistorySizeBytes: executionInfo.GetExecutionStats().GetHistorySize(),
RootExecution: &commonpb.WorkflowExecution{
WorkflowId: executionInfo.RootWorkflowId,
RunId: executionInfo.RootRunId,
},
MostRecentWorkerVersionStamp: executionInfo.MostRecentWorkerVersionStamp,
AssignedBuildId: executionInfo.AssignedBuildId,
InheritedBuildId: executionInfo.InheritedBuildId,
FirstRunId: executionInfo.FirstExecutionRunId,
VersioningInfo: executionInfo.VersioningInfo,
WorkerDeploymentName: executionInfo.WorkerDeploymentName,
Priority: executionInfo.Priority,
},
WorkflowExtendedInfo: &workflowpb.WorkflowExecutionExtendedInfo{
ExecutionExpirationTime: executionInfo.WorkflowExecutionExpirationTime,
RunExpirationTime: executionInfo.WorkflowRunExpirationTime,
OriginalStartTime: startEvent.EventTime,
CancelRequested: executionInfo.CancelRequested,
ResetRunId: executionInfo.ResetRunId,
RequestIdInfos: make(map[string]*workflowpb.RequestIdInfo),
},
}
if mutableState.IsResetRun() {
result.WorkflowExtendedInfo.LastResetTime = executionState.StartTime
}
for requestID, requestIDInfo := range mutableState.GetExecutionState().GetRequestIds() {
info := &workflowpb.RequestIdInfo{
EventType: requestIDInfo.EventType,
Buffered: requestIDInfo.EventId == common.BufferedEventID,
}
if !info.Buffered {
info.EventId = requestIDInfo.EventId
}
result.WorkflowExtendedInfo.RequestIdInfos[requestID] = info
}
if executionInfo.ParentRunId != "" {
result.WorkflowExecutionInfo.ParentExecution = &commonpb.WorkflowExecution{
WorkflowId: executionInfo.ParentWorkflowId,
RunId: executionInfo.ParentRunId,
}
result.WorkflowExecutionInfo.ParentNamespaceId = executionInfo.ParentNamespaceId
}
if executionState.State == enumsspb.WORKFLOW_EXECUTION_STATE_COMPLETED {
// for closed workflow
result.WorkflowExecutionInfo.Status = executionState.Status
closeTime, err := mutableState.GetWorkflowCloseTime(ctx)
if err != nil {
return nil, err
}
executionDuration, err := mutableState.GetWorkflowExecutionDuration(ctx)
if err != nil {
return nil, err
}
result.WorkflowExecutionInfo.CloseTime = timestamppb.New(closeTime)
result.WorkflowExecutionInfo.ExecutionDuration = durationpb.New(executionDuration)
}
for _, ai := range mutableState.GetPendingActivityInfos() {
p, err := workflow.GetPendingActivityInfo(ctx, shard, mutableState, ai)
if err != nil {
return nil, err
}
result.PendingActivities = append(result.PendingActivities, p)
}
for _, ch := range mutableState.GetPendingChildExecutionInfos() {
p := &workflowpb.PendingChildExecutionInfo{
WorkflowId: ch.StartedWorkflowId,
RunId: ch.StartedRunId,
WorkflowTypeName: ch.WorkflowTypeName,
InitiatedId: ch.InitiatedEventId,
ParentClosePolicy: ch.ParentClosePolicy,
}
result.PendingChildren = append(result.PendingChildren, p)
}
if pendingWorkflowTask := mutableState.GetPendingWorkflowTask(); pendingWorkflowTask != nil {
result.PendingWorkflowTask = &workflowpb.PendingWorkflowTaskInfo{
State: enumspb.PENDING_WORKFLOW_TASK_STATE_SCHEDULED,
ScheduledTime: timestamppb.New(pendingWorkflowTask.ScheduledTime),
OriginalScheduledTime: timestamppb.New(pendingWorkflowTask.OriginalScheduledTime),
Attempt: pendingWorkflowTask.Attempt,
}
if pendingWorkflowTask.StartedEventID != common.EmptyEventID {
result.PendingWorkflowTask.State = enumspb.PENDING_WORKFLOW_TASK_STATE_STARTED
result.PendingWorkflowTask.StartedTime = timestamppb.New(pendingWorkflowTask.StartedTime)
}
}
relocatableAttrsFetcher := workflow.RelocatableAttributesFetcherProvider(
shard.GetConfig(),
persistenceVisibilityMgr,
)
relocatableAttributes, err := relocatableAttrsFetcher.Fetch(ctx, mutableState)
if err != nil {
shard.GetLogger().Error(
"Failed to fetch relocatable attributes",
tag.WorkflowNamespaceID(namespaceID.String()),
tag.WorkflowID(executionInfo.WorkflowId),
tag.WorkflowRunID(executionState.RunId),
tag.Error(err),
)
return nil, serviceerror.NewInternal("Failed to fetch memo and search attributes")
}
result.WorkflowExecutionInfo.Memo = &commonpb.Memo{
Fields: clonePayloadMap(relocatableAttributes.Memo.GetFields()),
}
result.WorkflowExecutionInfo.SearchAttributes = &commonpb.SearchAttributes{
IndexedFields: clonePayloadMap(relocatableAttributes.SearchAttributes.GetIndexedFields()),
}
cbColl := callbacks.MachineCollection(mutableState.HSM())
cbs := cbColl.List()
result.Callbacks = make([]*workflowpb.CallbackInfo, 0, len(cbs))
for _, node := range cbs {
callback, err := cbColl.Data(node.Key.ID)
if err != nil {
shard.GetLogger().Error(
"failed to load callback data while building describe response",
tag.WorkflowNamespaceID(namespaceID.String()),
tag.WorkflowID(executionInfo.WorkflowId),
tag.WorkflowRunID(executionState.RunId),
tag.Error(err),
)
return nil, serviceerror.NewInternal("failed to construct describe response")
}
callbackInfo, err := buildCallbackInfo(namespaceID, callback, outboundQueueCBPool)
if err != nil {
shard.GetLogger().Error(
"failed to build callback info while building describe response",
tag.WorkflowNamespaceID(namespaceID.String()),
tag.WorkflowID(executionInfo.WorkflowId),
tag.WorkflowRunID(executionState.RunId),
tag.Error(err),
)
return nil, serviceerror.NewInternal("failed to construct describe response")
}
if callbackInfo == nil {
continue
}
result.Callbacks = append(result.Callbacks, callbackInfo)
}
opColl := nexusoperations.MachineCollection(mutableState.HSM())
ops := opColl.List()
result.PendingNexusOperations = make([]*workflowpb.PendingNexusOperationInfo, 0, len(ops))
for _, node := range ops {
op, err := opColl.Data(node.Key.ID)
if err != nil {
shard.GetLogger().Error(
"failed to load Nexus operation data while building describe response",
tag.WorkflowNamespaceID(namespaceID.String()),
tag.WorkflowID(executionInfo.WorkflowId),
tag.WorkflowRunID(executionState.RunId),
tag.Error(err),
)
return nil, serviceerror.NewInternal("failed to construct describe response")
}
operationInfo, err := buildPendingNexusOperationInfo(namespaceID, node, op, outboundQueueCBPool)
if err != nil {
shard.GetLogger().Error(
"failed to build Nexus operation info while building describe response",
tag.WorkflowNamespaceID(namespaceID.String()),
tag.WorkflowID(executionInfo.WorkflowId),
tag.WorkflowRunID(executionState.RunId),
tag.Error(err),
)
return nil, serviceerror.NewInternal("failed to construct describe response")
}
if operationInfo == nil {
// Operation is not pending
continue
}
result.PendingNexusOperations = append(result.PendingNexusOperations, operationInfo)
}
return result, nil
}
func buildCallbackInfo(
namespaceID namespace.ID,
callback callbacks.Callback,
outboundQueueCBPool *circuitbreakerpool.OutboundQueueCircuitBreakerPool,
) (*workflowpb.CallbackInfo, error) {
if callback.Callback.GetNexus() == nil {
// Ignore non-nexus callbacks for now (there aren't any just yet).
return nil, nil
}
cbSpec, err := workflow.PersistenceCallbackToAPICallback(callback.Callback)
if err != nil {
return nil, err
}
var state enumspb.CallbackState
switch callback.State() {
case enumsspb.CALLBACK_STATE_UNSPECIFIED:
return nil, errCallbackStateUnspecified
case enumsspb.CALLBACK_STATE_STANDBY:
state = enumspb.CALLBACK_STATE_STANDBY
case enumsspb.CALLBACK_STATE_SCHEDULED:
state = enumspb.CALLBACK_STATE_SCHEDULED
case enumsspb.CALLBACK_STATE_BACKING_OFF:
state = enumspb.CALLBACK_STATE_BACKING_OFF
case enumsspb.CALLBACK_STATE_FAILED:
state = enumspb.CALLBACK_STATE_FAILED
case enumsspb.CALLBACK_STATE_SUCCEEDED:
state = enumspb.CALLBACK_STATE_SUCCEEDED
}
blockedReason := ""
if state == enumspb.CALLBACK_STATE_SCHEDULED {
cb := outboundQueueCBPool.Get(tasks.TaskGroupNamespaceIDAndDestination{
TaskGroup: callbacks.TaskTypeInvocation,
NamespaceID: namespaceID.String(),
Destination: cbSpec.GetNexus().GetUrl(),
})
if cb.State() != gobreaker.StateClosed {
state = enumspb.CALLBACK_STATE_BLOCKED
blockedReason = "The circuit breaker is open."
}
}
trigger := &workflowpb.CallbackInfo_Trigger{}
switch callback.Trigger.Variant.(type) {
case *persistencespb.CallbackInfo_Trigger_WorkflowClosed:
trigger.Variant = &workflowpb.CallbackInfo_Trigger_WorkflowClosed{}
}
return &workflowpb.CallbackInfo{
Callback: cbSpec,
Trigger: trigger,
RegistrationTime: callback.RegistrationTime,
State: state,
Attempt: callback.Attempt,
LastAttemptCompleteTime: callback.LastAttemptCompleteTime,
LastAttemptFailure: callback.LastAttemptFailure,
NextAttemptScheduleTime: callback.NextAttemptScheduleTime,
BlockedReason: blockedReason,
}, nil
}
func buildPendingNexusOperationInfo(
namespaceID namespace.ID,
node *hsm.Node,
op nexusoperations.Operation,
outboundQueueCBPool *circuitbreakerpool.OutboundQueueCircuitBreakerPool,
) (*workflowpb.PendingNexusOperationInfo, error) {
var state enumspb.PendingNexusOperationState
switch op.State() {
case enumsspb.NEXUS_OPERATION_STATE_UNSPECIFIED:
return nil, errNexusOperationStateUnspecified
case enumsspb.NEXUS_OPERATION_STATE_BACKING_OFF:
state = enumspb.PENDING_NEXUS_OPERATION_STATE_BACKING_OFF
case enumsspb.NEXUS_OPERATION_STATE_SCHEDULED:
state = enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED
case enumsspb.NEXUS_OPERATION_STATE_STARTED:
state = enumspb.PENDING_NEXUS_OPERATION_STATE_STARTED
case enumsspb.NEXUS_OPERATION_STATE_CANCELED,
enumsspb.NEXUS_OPERATION_STATE_FAILED,
enumsspb.NEXUS_OPERATION_STATE_SUCCEEDED,
enumsspb.NEXUS_OPERATION_STATE_TIMED_OUT:
// Operation is not pending
return nil, nil
}
blockedReason := ""
if state == enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED {
cb := outboundQueueCBPool.Get(tasks.TaskGroupNamespaceIDAndDestination{
TaskGroup: nexusoperations.TaskTypeInvocation,
NamespaceID: namespaceID.String(),
Destination: op.Endpoint,
})
if cb.State() != gobreaker.StateClosed {
state = enumspb.PENDING_NEXUS_OPERATION_STATE_BLOCKED
blockedReason = "The circuit breaker is open."
}
}
cancellationInfo, err := buildNexusOperationCancellationInfo(namespaceID, node, op, outboundQueueCBPool)
if err != nil {
return nil, err
}
// We store nexus operations in the tree by their string formatted scheduled event ID.
scheduledEventID, err := strconv.ParseInt(node.Key.ID, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to determine Nexus operation scheduled event ID: %w", err)
}
return &workflowpb.PendingNexusOperationInfo{
Endpoint: op.Endpoint,
Service: op.Service,
Operation: op.Operation,
// TODO(bergundy): Remove this fallback after the 1.27 release.
OperationId: op.OperationToken,
OperationToken: op.OperationToken,
ScheduledEventId: scheduledEventID,
ScheduleToCloseTimeout: op.ScheduleToCloseTimeout,
ScheduledTime: op.ScheduledTime,
State: state,
Attempt: op.Attempt,
LastAttemptCompleteTime: op.LastAttemptCompleteTime,
LastAttemptFailure: op.LastAttemptFailure,
NextAttemptScheduleTime: op.NextAttemptScheduleTime,
CancellationInfo: cancellationInfo,
BlockedReason: blockedReason,
}, nil
}
func buildNexusOperationCancellationInfo(
namespaceID namespace.ID,
node *hsm.Node,
op nexusoperations.Operation,
outboundQueueCBPool *circuitbreakerpool.OutboundQueueCircuitBreakerPool,
) (*workflowpb.NexusOperationCancellationInfo, error) {
cancelation, err := op.Cancelation(node)
if err != nil {
return nil, fmt.Errorf("failed to load Nexus operation cancelation data: %w", err)
}
if cancelation == nil {
return nil, nil
}
state := cancelation.State()
blockedReason := ""
if state == enumspb.NEXUS_OPERATION_CANCELLATION_STATE_SCHEDULED {
cb := outboundQueueCBPool.Get(tasks.TaskGroupNamespaceIDAndDestination{
TaskGroup: nexusoperations.TaskTypeCancelation,
NamespaceID: namespaceID.String(),
Destination: op.Endpoint,
})
if cb.State() != gobreaker.StateClosed {
state = enumspb.NEXUS_OPERATION_CANCELLATION_STATE_BLOCKED
blockedReason = "The circuit breaker is open."
}
}
return &workflowpb.NexusOperationCancellationInfo{
RequestedTime: cancelation.RequestedTime,
State: state,
Attempt: cancelation.Attempt,
LastAttemptCompleteTime: cancelation.LastAttemptCompleteTime,
LastAttemptFailure: cancelation.LastAttemptFailure,
NextAttemptScheduleTime: cancelation.NextAttemptScheduleTime,
BlockedReason: blockedReason,
}, nil
}