Files
tasks/service/worker/batcher/workflow.go
Alan WuandGitHub cf7c62c8cc Add batch workflow refresh tasks (#8793)
## What changed?
Add batch workflow refresh tasks Admin API.

Add tdbg commands to invoke `StartAdminBatchOperation`.

## Details

AdminBatchOperations are executed in the Batched System Worker, similar
to existing Frontend BatchOperations. The worker activity will fetch
pages of workflow executions to perform operations on, and periodically
heartbeat its results. For RefreshWorkflowTasks, each individual Refresh
call will go through the admin handler.

Refreshing Workflow Tasks regenerates all pending tasks of an execution
given its mutable state.

## CLI usage

```
tdbg workflow batch-refresh-tasks \
  --namespace <ns> \
  --query "WorkflowType='MyType'" \
  --reason "fixing stuck workflows" \
  [--job-id <optional-job-id>] \
  [--archetype <optional-archetype>]
```

## Why?
Unblock new Matcher migration and allow for general use case batch Admin
calls. Currently, only supports `BatchOperationRefreshWorkflowTasks`.

## 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)
2026-01-06 19:21:57 -05:00

292 lines
11 KiB
Go

package batcher
import (
"errors"
"fmt"
"time"
batchpb "go.temporal.io/api/batch/v1"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
workflowpb "go.temporal.io/api/workflow/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/temporal"
"go.temporal.io/sdk/workflow"
batchspb "go.temporal.io/server/api/batch/v1"
"go.temporal.io/server/common/searchattribute/sadefs"
"google.golang.org/protobuf/types/known/durationpb"
)
const (
// InfiniteDuration is a long duration (20 yrs) we use for "infinite" workflows
infiniteDuration = 20 * 365 * 24 * time.Hour
defaultAttemptsOnRetryableError = 50
defaultActivityHeartBeatTimeout = time.Second * 10
)
const (
// BatchOperationTypeMemo stores batch operation type in memo
BatchOperationTypeMemo = "batch_operation_type"
// BatchReasonMemo stores batch operation reason in memo
BatchReasonMemo = "batch_operation_reason"
// BatchOperationStatsMemo stores batch operation stats in memo
BatchOperationStatsMemo = "batch_operation_stats"
// BatchTypeTerminate is batch type for terminating workflows
BatchTypeTerminate = "terminate"
// BatchTypeCancel is the batch type for canceling workflows
BatchTypeCancel = "cancel"
// BatchTypeSignal is batch type for signaling workflows
BatchTypeSignal = "signal"
// BatchTypeDelete is batch type for deleting workflows
BatchTypeDelete = "delete"
// BatchTypeReset is batch type for resetting workflows
BatchTypeReset = "reset"
// BatchTypeUpdateOptions is batch type for updating the options of workflow executions
BatchTypeUpdateOptions = "update_options"
// BatchTypeUnpauseActivities is batch type for unpausing activities
BatchTypeUnpauseActivities = "unpause_activities"
// BatchTypeUpdateActivitiesOptions is batch type for updating the options of activities
BatchTypeUpdateActivitiesOptions = "update_activity_options"
// BatchTypeResetActivities is batch type for resetting activities
BatchTypeResetActivities = "reset_activities"
)
var (
OpenBatchOperationQuery = fmt.Sprintf("%s = '%s' AND %s = %d",
sadefs.TemporalNamespaceDivision,
NamespaceDivision,
sadefs.ExecutionStatus,
int(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING),
)
OpenAdminBatchOperationQuery = fmt.Sprintf("%s = '%s' AND %s = %d",
sadefs.TemporalNamespaceDivision,
AdminNamespaceDivision,
sadefs.ExecutionStatus,
int(enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING),
)
)
type (
// HeartBeatDetails is the struct for heartbeat details
HeartBeatDetails struct {
PageToken []byte
CurrentPage int
// This is just an estimation for visibility
TotalEstimate int64
// Number of workflows processed successfully
SuccessCount int
// Number of workflows that give up due to errors.
ErrorCount int
}
task struct {
// the workflow execution to process
executionInfo *workflowpb.WorkflowExecutionInfo
// the number of attempts to process the workflow execution
attempts int
// reference to the page this task belongs to (for tracking page completion)
page *page
}
taskResponse struct {
// the error result from processing the task (nil for success)
err error
// reference to the page the completed task belonged to
page *page
}
)
var (
batchActivityRetryPolicy = temporal.RetryPolicy{
InitialInterval: 10 * time.Second,
BackoffCoefficient: 1.7,
MaximumInterval: 5 * time.Minute,
}
batchActivityOptions = workflow.ActivityOptions{
ScheduleToStartTimeout: 5 * time.Minute,
StartToCloseTimeout: infiniteDuration,
RetryPolicy: &batchActivityRetryPolicy,
}
)
// BatchWorkflowProtobuf is the workflow that runs a batch job of resetting workflows.
func BatchWorkflowProtobuf(ctx workflow.Context, batchParams *batchspb.BatchOperationInput) (HeartBeatDetails, error) {
if batchParams == nil {
return HeartBeatDetails{}, errors.New("batchParams is nil")
}
batchParams = setDefaultParams(batchParams)
batchActivityOptions.HeartbeatTimeout = batchParams.ActivityHeartbeatTimeout.AsDuration()
opt := workflow.WithActivityOptions(ctx, batchActivityOptions)
var result HeartBeatDetails
var ac *activities
err := workflow.ExecuteActivity(opt, ac.BatchActivityWithProtobuf, batchParams).Get(ctx, &result)
if err != nil {
return HeartBeatDetails{}, err
}
err = attachBatchOperationStats(ctx, result)
if err != nil {
return HeartBeatDetails{}, err
}
return result, err
}
type BatchOperationStats struct {
NumSuccess int
NumFailure int
}
// attachBatchOperationStats attaches statistics on the number of
// individual successes and failures to the memo of this workflow.
func attachBatchOperationStats(ctx workflow.Context, result HeartBeatDetails) error {
memo := map[string]interface{}{
BatchOperationStatsMemo: BatchOperationStats{
NumSuccess: result.SuccessCount,
NumFailure: result.ErrorCount,
},
}
return workflow.UpsertMemo(ctx, memo)
}
// nolint:revive,cognitive-complexity
func ValidateBatchOperation(params *workflowservice.StartBatchOperationRequest) error {
if params.GetOperation() == nil ||
params.GetReason() == "" ||
params.GetNamespace() == "" ||
(params.GetVisibilityQuery() == "" && len(params.GetExecutions()) == 0) {
return serviceerror.NewInvalidArgument("must provide required parameters: BatchType/Reason/Namespace/Query/Executions")
}
if len(params.GetJobId()) == 0 {
return serviceerror.NewInvalidArgument("JobId is not set on request.")
}
if len(params.GetNamespace()) == 0 {
return serviceerror.NewInvalidArgument("Namespace is not set on request.")
}
if len(params.GetVisibilityQuery()) == 0 && len(params.GetExecutions()) == 0 {
return serviceerror.NewInvalidArgument("VisibilityQuery or Executions must be set on request.")
}
if len(params.GetVisibilityQuery()) != 0 && len(params.GetExecutions()) != 0 {
return errors.New("batch query and executions are mutually exclusive")
}
if len(params.GetReason()) == 0 {
return serviceerror.NewInvalidArgument("Reason is not set on request.")
}
if params.GetOperation() == nil {
return serviceerror.NewInvalidArgument("Batch operation is not set on request.")
}
switch op := params.GetOperation().(type) {
case *workflowservice.StartBatchOperationRequest_SignalOperation:
if op.SignalOperation.GetSignal() == "" {
return errors.New("must provide signal name")
}
return nil
case *workflowservice.StartBatchOperationRequest_UpdateWorkflowOptionsOperation:
if op.UpdateWorkflowOptionsOperation.GetWorkflowExecutionOptions() == nil {
return errors.New("must provide UpdateOptions")
}
if op.UpdateWorkflowOptionsOperation.GetUpdateMask() == nil {
return errors.New("must provide UpdateMask")
}
// Validation for Versioning Override, if present, happens in history.
return nil
case *workflowservice.StartBatchOperationRequest_CancellationOperation,
*workflowservice.StartBatchOperationRequest_TerminationOperation,
*workflowservice.StartBatchOperationRequest_DeletionOperation:
return nil
case *workflowservice.StartBatchOperationRequest_ResetOperation:
if op.ResetOperation == nil {
return serviceerror.NewInvalidArgument("reset operation is not set")
}
if op.ResetOperation.Options != nil {
if op.ResetOperation.Options.Target == nil {
return serviceerror.NewInvalidArgument("batch reset missing target")
}
} else {
//nolint:staticcheck // SA1019: GetResetType is deprecated but still needed for backward compatibility
resetType := op.ResetOperation.GetResetType()
if _, ok := enumspb.ResetType_name[int32(resetType)]; !ok || resetType == enumspb.RESET_TYPE_UNSPECIFIED {
return serviceerror.NewInvalidArgumentf("unknown batch reset type %v", resetType)
}
}
case *workflowservice.StartBatchOperationRequest_UnpauseActivitiesOperation:
if op.UnpauseActivitiesOperation == nil {
return serviceerror.NewInvalidArgument("unpause activities operation is not set")
}
if op.UnpauseActivitiesOperation.GetActivity() == nil {
return serviceerror.NewInvalidArgument("activity filter must be set")
}
switch a := op.UnpauseActivitiesOperation.GetActivity().(type) {
case *batchpb.BatchOperationUnpauseActivities_Type:
if len(a.Type) == 0 {
return serviceerror.NewInvalidArgument("Either activity type must be set, or match all should be set to true")
}
case *batchpb.BatchOperationUnpauseActivities_MatchAll:
if !a.MatchAll {
return serviceerror.NewInvalidArgument("Either activity type must be set, or match all should be set to true")
}
}
return nil
case *workflowservice.StartBatchOperationRequest_ResetActivitiesOperation:
if op.ResetActivitiesOperation == nil {
return serviceerror.NewInvalidArgument("reset activities operation is not set")
}
if op.ResetActivitiesOperation.GetActivity() == nil && !op.ResetActivitiesOperation.GetMatchAll() {
return serviceerror.NewInvalidArgument("must provide ActivityType or MatchAll")
}
switch a := op.ResetActivitiesOperation.GetActivity().(type) {
case *batchpb.BatchOperationResetActivities_Type:
if len(a.Type) == 0 {
return serviceerror.NewInvalidArgument("Either activity type must be set, or match all should be set to true")
}
case *batchpb.BatchOperationResetActivities_MatchAll:
if !a.MatchAll {
return serviceerror.NewInvalidArgument("Either activity type must be set, or match all should be set to true")
}
}
return nil
case *workflowservice.StartBatchOperationRequest_UpdateActivityOptionsOperation:
if op.UpdateActivityOptionsOperation == nil {
return serviceerror.NewInvalidArgument("update activity options operation is not set")
}
if op.UpdateActivityOptionsOperation.GetActivityOptions() != nil && op.UpdateActivityOptionsOperation.GetRestoreOriginal() {
return serviceerror.NewInvalidArgument("cannot set both activity options and restore original")
}
if op.UpdateActivityOptionsOperation.GetActivityOptions() == nil && !op.UpdateActivityOptionsOperation.GetRestoreOriginal() {
return serviceerror.NewInvalidArgument("Either activity type must be set, or restore original should be set to true")
}
switch a := op.UpdateActivityOptionsOperation.GetActivity().(type) {
case *batchpb.BatchOperationUpdateActivityOptions_Type:
if len(a.Type) == 0 {
return serviceerror.NewInvalidArgument("Either activity type must be set, or match all should be set to true")
}
case *batchpb.BatchOperationUpdateActivityOptions_MatchAll:
if !a.MatchAll {
return serviceerror.NewInvalidArgument("Either activity type must be set, or match all should be set to true")
}
}
return nil
default:
return fmt.Errorf("not supported batch type: %v", params.GetOperation())
}
return nil
}
func setDefaultParams(params *batchspb.BatchOperationInput) *batchspb.BatchOperationInput {
if params.GetAttemptsOnRetryableError() <= 1 {
params.AttemptsOnRetryableError = defaultAttemptsOnRetryableError
}
if params.GetActivityHeartbeatTimeout().AsDuration() <= 0 {
params.ActivityHeartbeatTimeout = &durationpb.Duration{
Seconds: int64(defaultActivityHeartBeatTimeout / time.Second),
}
}
return params
}