Skip to content

Commit 9389673

Browse files
authored
Support batch operation with list of workflow executions (#3812)
* Support batch operation with list of workflow executions
1 parent 5175ab6 commit 9389673

File tree

10 files changed

+211
-55
lines changed

10 files changed

+211
-55
lines changed

common/dynamicconfig/constants.go

+2
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ const (
245245
FrontendEnableSchedules = "frontend.enableSchedules"
246246
// FrontendMaxConcurrentBatchOperationPerNamespace is the max concurrent batch operation job count per namespace
247247
FrontendMaxConcurrentBatchOperationPerNamespace = "frontend.MaxConcurrentBatchOperationPerNamespace"
248+
// FrontendMaxExecutionCountBatchOperationPerNamespace is the max execution count batch operation supports per namespace
249+
FrontendMaxExecutionCountBatchOperationPerNamespace = "frontend.MaxExecutionCountBatchOperationPerNamespace"
248250
// FrontendEnableBatcher enables batcher-related RPCs in the frontend
249251
FrontendEnableBatcher = "frontend.enableBatcher"
250252

go.mod

+8-9
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ require (
4444
go.opentelemetry.io/otel/metric v0.33.0
4545
go.opentelemetry.io/otel/sdk v1.11.1
4646
go.opentelemetry.io/otel/sdk/metric v0.31.0
47-
go.temporal.io/api v1.13.1-0.20221110200459-6a3cb21a3415
47+
go.temporal.io/api v1.14.1-0.20230123181040-6d7a91e07c31
4848
go.temporal.io/sdk v1.19.0
4949
go.temporal.io/version v0.3.0
5050
go.uber.org/atomic v1.10.0
@@ -55,20 +55,19 @@ require (
5555
golang.org/x/oauth2 v0.2.0
5656
golang.org/x/time v0.2.0
5757
google.golang.org/api v0.103.0
58-
google.golang.org/grpc v1.51.0
58+
google.golang.org/grpc v1.52.0
5959
google.golang.org/grpc/examples v0.0.0-20221201195934-736197138d20
6060
gopkg.in/square/go-jose.v2 v2.6.0
6161
gopkg.in/validator.v2 v2.0.1
6262
gopkg.in/yaml.v3 v3.0.1
6363
modernc.org/sqlite v1.20.0
6464
)
6565

66-
require cloud.google.com/go/compute/metadata v0.2.2 // indirect
67-
6866
require (
6967
cloud.google.com/go v0.107.0 // indirect
7068
cloud.google.com/go/compute v1.13.0 // indirect
71-
cloud.google.com/go/iam v0.7.0 // indirect
69+
cloud.google.com/go/compute/metadata v0.2.2 // indirect
70+
cloud.google.com/go/iam v0.8.0 // indirect
7271
github.com/apache/thrift v0.17.0 // indirect
7372
github.com/benbjohnson/clock v1.3.0 // indirect
7473
github.com/beorn7/perks v1.0.1 // indirect
@@ -122,13 +121,13 @@ require (
122121
go.uber.org/dig v1.15.0 // indirect
123122
golang.org/x/crypto v0.3.0 // indirect
124123
golang.org/x/mod v0.7.0 // indirect
125-
golang.org/x/net v0.2.0 // indirect
126-
golang.org/x/sys v0.2.0 // indirect
127-
golang.org/x/text v0.4.0 // indirect
124+
golang.org/x/net v0.5.0 // indirect
125+
golang.org/x/sys v0.4.0 // indirect
126+
golang.org/x/text v0.6.0 // indirect
128127
golang.org/x/tools v0.3.0 // indirect
129128
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
130129
google.golang.org/appengine v1.6.7 // indirect
131-
google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3 // indirect
130+
google.golang.org/genproto v0.0.0-20230119192704-9d59e20e5cd1 // indirect
132131
google.golang.org/protobuf v1.28.1 // indirect
133132
gopkg.in/inf.v0 v0.9.1 // indirect
134133
lukechampine.com/uint128 v1.2.0 // indirect

go.sum

+41-13
Large diffs are not rendered by default.

service/frontend/errors.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,8 @@ var (
9898
errListNotAllowed = serviceerror.NewPermissionDenied("List is disabled on this namespace.", "")
9999
errSchedulesNotAllowed = serviceerror.NewPermissionDenied("Schedules are disabled on this namespace.", "")
100100

101-
errBatchAPINotAllowed = serviceerror.NewPermissionDenied("Batch operation feature are disabled on this namespace.", "")
101+
errBatchAPINotAllowed = serviceerror.NewPermissionDenied("Batch operation feature are disabled on this namespace.", "")
102+
errBatchOpsWorkflowFilterNotSet = serviceerror.NewInvalidArgument("Workflow executions and visibility filter are not set on request.")
103+
errBatchOpsWorkflowFiltersNotAllowed = serviceerror.NewInvalidArgument("Workflow executions and visibility filter are both set on request. Only one of them is allowed.")
104+
errBatchOpsMaxWorkflowExecutionCount = serviceerror.NewInvalidArgument("Workflow executions count exceeded.")
102105
)

service/frontend/service.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ type Config struct {
164164
// Enable batcher RPCs
165165
EnableBatcher dynamicconfig.BoolPropertyFnWithNamespaceFilter
166166
// Batch operation dynamic configs
167-
MaxConcurrentBatchOperation dynamicconfig.IntPropertyFnWithNamespaceFilter
167+
MaxConcurrentBatchOperation dynamicconfig.IntPropertyFnWithNamespaceFilter
168+
MaxExecutionCountBatchOperation dynamicconfig.IntPropertyFnWithNamespaceFilter
168169
}
169170

170171
// NewConfig returns new service config with default values
@@ -233,8 +234,9 @@ func NewConfig(dc *dynamicconfig.Collection, numHistoryShards int32, esIndexName
233234

234235
EnableSchedules: dc.GetBoolPropertyFnWithNamespaceFilter(dynamicconfig.FrontendEnableSchedules, true),
235236

236-
EnableBatcher: dc.GetBoolPropertyFnWithNamespaceFilter(dynamicconfig.FrontendEnableBatcher, true),
237-
MaxConcurrentBatchOperation: dc.GetIntPropertyFilteredByNamespace(dynamicconfig.FrontendMaxConcurrentBatchOperationPerNamespace, 1),
237+
EnableBatcher: dc.GetBoolPropertyFnWithNamespaceFilter(dynamicconfig.FrontendEnableBatcher, true),
238+
MaxConcurrentBatchOperation: dc.GetIntPropertyFilteredByNamespace(dynamicconfig.FrontendMaxConcurrentBatchOperationPerNamespace, 1),
239+
MaxExecutionCountBatchOperation: dc.GetIntPropertyFilteredByNamespace(dynamicconfig.FrontendMaxExecutionCountBatchOperationPerNamespace, 1000),
238240
}
239241
}
240242

service/frontend/workflow_handler.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -3712,8 +3712,14 @@ func (wh *WorkflowHandler) StartBatchOperation(
37123712
if len(request.Namespace) == 0 {
37133713
return nil, errNamespaceNotSet
37143714
}
3715-
if len(request.VisibilityQuery) == 0 {
3716-
return nil, errQueryNotSet
3715+
if len(request.VisibilityQuery) == 0 && len(request.Executions) == 0 {
3716+
return nil, errBatchOpsWorkflowFilterNotSet
3717+
}
3718+
if len(request.VisibilityQuery) != 0 && len(request.Executions) != 0 {
3719+
return nil, errBatchOpsWorkflowFiltersNotAllowed
3720+
}
3721+
if len(request.Executions) > wh.config.MaxExecutionCountBatchOperation(request.Namespace) {
3722+
return nil, errBatchOpsMaxWorkflowExecutionCount
37173723
}
37183724
if len(request.Reason) == 0 {
37193725
return nil, errReasonNotSet
@@ -3767,6 +3773,7 @@ func (wh *WorkflowHandler) StartBatchOperation(
37673773
input := &batcher.BatchParams{
37683774
Namespace: request.GetNamespace(),
37693775
Query: request.GetVisibilityQuery(),
3776+
Executions: request.GetExecutions(),
37703777
Reason: request.GetReason(),
37713778
BatchType: operationType,
37723779
TerminateParams: batcher.TerminateParams{},

service/frontend/workflow_handler_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -2097,6 +2097,71 @@ func (s *workflowHandlerSuite) TestStartBatchOperation_Signal() {
20972097
s.NoError(err)
20982098
}
20992099

2100+
func (s *workflowHandlerSuite) TestStartBatchOperation_WorkflowExecutions_Singal() {
2101+
testNamespace := namespace.Name("test-namespace")
2102+
namespaceID := namespace.ID(uuid.New())
2103+
executions := []*commonpb.WorkflowExecution{
2104+
{
2105+
WorkflowId: uuid.New(),
2106+
RunId: uuid.New(),
2107+
},
2108+
}
2109+
reason := "reason"
2110+
identity := "identity"
2111+
signalName := "signal name"
2112+
config := s.newConfig()
2113+
wh := s.getWorkflowHandler(config)
2114+
signalPayloads := payloads.EncodeString(signalName)
2115+
params := &batcher.BatchParams{
2116+
Namespace: testNamespace.String(),
2117+
Executions: executions,
2118+
Reason: reason,
2119+
BatchType: batcher.BatchTypeSignal,
2120+
SignalParams: batcher.SignalParams{
2121+
SignalName: signalName,
2122+
Input: signalPayloads,
2123+
},
2124+
}
2125+
inputPayload, err := payloads.Encode(params)
2126+
s.NoError(err)
2127+
s.mockNamespaceCache.EXPECT().GetNamespaceID(gomock.Any()).Return(namespaceID, nil).AnyTimes()
2128+
s.mockHistoryClient.EXPECT().StartWorkflowExecution(gomock.Any(), gomock.Any()).DoAndReturn(
2129+
func(
2130+
_ context.Context,
2131+
request *historyservice.StartWorkflowExecutionRequest,
2132+
_ ...grpc.CallOption,
2133+
) (*historyservice.StartWorkflowExecutionResponse, error) {
2134+
s.Equal(namespaceID.String(), request.NamespaceId)
2135+
s.Equal(batcher.BatchWFTypeName, request.StartRequest.WorkflowType.Name)
2136+
s.Equal(primitives.PerNSWorkerTaskQueue, request.StartRequest.TaskQueue.Name)
2137+
s.Equal(enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, request.StartRequest.WorkflowIdReusePolicy)
2138+
s.Equal(identity, request.StartRequest.Identity)
2139+
s.Equal(payload.EncodeString(batcher.BatchTypeSignal), request.StartRequest.Memo.Fields[batcher.BatchOperationTypeMemo])
2140+
s.Equal(payload.EncodeString(reason), request.StartRequest.Memo.Fields[batcher.BatchReasonMemo])
2141+
s.Equal(payload.EncodeString(identity), request.StartRequest.SearchAttributes.IndexedFields[searchattribute.BatcherUser])
2142+
s.Equal(inputPayload, request.StartRequest.Input)
2143+
return &historyservice.StartWorkflowExecutionResponse{}, nil
2144+
},
2145+
)
2146+
s.mockVisibilityMgr.EXPECT().CountWorkflowExecutions(gomock.Any(), gomock.Any()).Return(&manager.CountWorkflowExecutionsResponse{Count: 0}, nil)
2147+
request := &workflowservice.StartBatchOperationRequest{
2148+
Namespace: testNamespace.String(),
2149+
JobId: uuid.New(),
2150+
Operation: &workflowservice.StartBatchOperationRequest_SignalOperation{
2151+
SignalOperation: &batchpb.BatchOperationSignal{
2152+
Signal: signalName,
2153+
Input: signalPayloads,
2154+
Identity: identity,
2155+
},
2156+
},
2157+
Reason: reason,
2158+
Executions: executions,
2159+
}
2160+
2161+
_, err = wh.StartBatchOperation(context.Background(), request)
2162+
s.NoError(err)
2163+
}
2164+
21002165
func (s *workflowHandlerSuite) TestStartBatchOperation_InvalidRequest() {
21012166
request := &workflowservice.StartBatchOperationRequest{
21022167
Namespace: "",

service/worker/batcher/activities.go

+34-22
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,19 @@ func (a *activities) BatchActivity(ctx context.Context, batchParams BatchParams)
9191
}
9292

9393
if startOver {
94-
resp, err := sdkClient.CountWorkflow(ctx, &workflowservice.CountWorkflowExecutionsRequest{
95-
Query: batchParams.Query,
96-
})
97-
if err != nil {
98-
metricsHandler.Counter(metrics.BatcherOperationFailures.GetMetricName()).Record(1)
99-
logger.Error("Failed to get estimate workflow count", tag.Error(err))
100-
return HeartBeatDetails{}, err
94+
estimateCount := int64(len(batchParams.Executions))
95+
if len(batchParams.Query) > 0 {
96+
resp, err := sdkClient.CountWorkflow(ctx, &workflowservice.CountWorkflowExecutionsRequest{
97+
Query: batchParams.Query,
98+
})
99+
if err != nil {
100+
metricsHandler.Counter(metrics.BatcherOperationFailures.GetMetricName()).Record(1)
101+
logger.Error("Failed to get estimate workflow count", tag.Error(err))
102+
return HeartBeatDetails{}, err
103+
}
104+
estimateCount = resp.GetCount()
101105
}
102-
hbd.TotalEstimate = resp.GetCount()
106+
hbd.TotalEstimate = estimateCount
103107
}
104108
rps := a.getOperationRPS(batchParams.RPS)
105109
rateLimiter := rate.NewLimiter(rate.Limit(rps), rps)
@@ -110,25 +114,33 @@ func (a *activities) BatchActivity(ctx context.Context, batchParams BatchParams)
110114
}
111115

112116
for {
113-
resp, err := sdkClient.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
114-
PageSize: int32(pageSize),
115-
NextPageToken: hbd.PageToken,
116-
Query: batchParams.Query,
117-
})
118-
if err != nil {
119-
metricsHandler.Counter(metrics.BatcherOperationFailures.GetMetricName()).Record(1)
120-
logger.Error("Failed to list workflow executions", tag.Error(err))
121-
return HeartBeatDetails{}, err
117+
executions := batchParams.Executions
118+
pageToken := hbd.PageToken
119+
if len(batchParams.Query) > 0 {
120+
resp, err := sdkClient.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
121+
PageSize: int32(pageSize),
122+
NextPageToken: pageToken,
123+
Query: batchParams.Query,
124+
})
125+
if err != nil {
126+
metricsHandler.Counter(metrics.BatcherOperationFailures.GetMetricName()).Record(1)
127+
logger.Error("Failed to list workflow executions", tag.Error(err))
128+
return HeartBeatDetails{}, err
129+
}
130+
pageToken = resp.NextPageToken
131+
for _, wf := range resp.Executions {
132+
executions = append(executions, wf.Execution)
133+
}
122134
}
123-
batchCount := len(resp.Executions)
135+
136+
batchCount := len(executions)
124137
if batchCount <= 0 {
125138
break
126139
}
127-
128140
// send all tasks
129-
for _, wf := range resp.Executions {
141+
for _, wf := range executions {
130142
taskCh <- taskDetail{
131-
execution: *wf.Execution,
143+
execution: *wf,
132144
attempts: 1,
133145
hbd: hbd,
134146
}
@@ -157,7 +169,7 @@ func (a *activities) BatchActivity(ctx context.Context, batchParams BatchParams)
157169
}
158170

159171
hbd.CurrentPage++
160-
hbd.PageToken = resp.NextPageToken
172+
hbd.PageToken = pageToken
161173
hbd.SuccessCount += succCount
162174
hbd.ErrorCount += errCount
163175
activity.RecordHeartbeat(ctx, hbd)

service/worker/batcher/workflow.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ type (
9898
Namespace string
9999
// To get the target workflows for processing
100100
Query string
101+
// Target workflows for processing
102+
Executions []*commonpb.WorkflowExecution
101103
// Reason for the operation
102104
Reason string
103-
// Supporting: signal,cancel,terminate
105+
// Supporting: signal,cancel,terminate,delete
104106
BatchType string
105107

106108
// Below are all optional
@@ -207,16 +209,19 @@ func validateParams(params BatchParams) error {
207209
if params.BatchType == "" ||
208210
params.Reason == "" ||
209211
params.Namespace == "" ||
210-
params.Query == "" {
211-
return fmt.Errorf("must provide required parameters: BatchType/Reason/Namespace/Query")
212+
(params.Query == "" && len(params.Executions) == 0) {
213+
return fmt.Errorf("must provide required parameters: BatchType/Reason/Namespace/Query/Executions")
214+
}
215+
if len(params.Query) > 0 && len(params.Executions) > 0 {
216+
return fmt.Errorf("batch query and executions are mutually exclusive")
212217
}
213218
switch params.BatchType {
214219
case BatchTypeSignal:
215220
if params.SignalParams.SignalName == "" {
216221
return fmt.Errorf("must provide signal name")
217222
}
218223
return nil
219-
case BatchTypeCancel, BatchTypeTerminate:
224+
case BatchTypeCancel, BatchTypeTerminate, BatchTypeDelete:
220225
return nil
221226
default:
222227
return fmt.Errorf("not supported batch type: %v", params.BatchType)

service/worker/batcher/workflow_test.go

+34-1
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ import (
2828
"testing"
2929

3030
"github.com/golang/mock/gomock"
31+
"github.com/pborman/uuid"
3132
"github.com/stretchr/testify/mock"
3233
"github.com/stretchr/testify/suite"
34+
commonpb "go.temporal.io/api/common/v1"
3335
"go.temporal.io/sdk/testsuite"
3436
)
3537

@@ -62,7 +64,7 @@ func (s *batcherSuite) TestBatchWorkflow_MissingParams() {
6264
s.Contains(err.Error(), "must provide required parameters")
6365
}
6466

65-
func (s *batcherSuite) TestBatchWorkflow_ValidParams() {
67+
func (s *batcherSuite) TestBatchWorkflow_ValidParams_Query() {
6668
var ac *activities
6769
s.env.OnActivity(ac.BatchActivity, mock.Anything, mock.Anything).Return(HeartBeatDetails{
6870
SuccessCount: 42,
@@ -87,3 +89,34 @@ func (s *batcherSuite) TestBatchWorkflow_ValidParams() {
8789
err := s.env.GetWorkflowError()
8890
s.Require().NoError(err)
8991
}
92+
93+
func (s *batcherSuite) TestBatchWorkflow_ValidParams_Executions() {
94+
var ac *activities
95+
s.env.OnActivity(ac.BatchActivity, mock.Anything, mock.Anything).Return(HeartBeatDetails{
96+
SuccessCount: 42,
97+
ErrorCount: 27,
98+
}, nil)
99+
s.env.OnUpsertMemo(mock.Anything).Run(func(args mock.Arguments) {
100+
memo, ok := args.Get(0).(map[string]interface{})
101+
s.Require().True(ok)
102+
s.Equal(map[string]interface{}{
103+
"batch_operation_stats": BatchOperationStats{
104+
NumSuccess: 42,
105+
NumFailure: 27,
106+
},
107+
}, memo)
108+
}).Once()
109+
s.env.ExecuteWorkflow(BatchWorkflow, BatchParams{
110+
BatchType: BatchTypeTerminate,
111+
Reason: "test-reason",
112+
Namespace: "test-namespace",
113+
Executions: []*commonpb.WorkflowExecution{
114+
{
115+
WorkflowId: uuid.New(),
116+
RunId: uuid.New(),
117+
},
118+
},
119+
})
120+
err := s.env.GetWorkflowError()
121+
s.Require().NoError(err)
122+
}

0 commit comments

Comments
 (0)